diff --git a/AUTHORS.rst b/AUTHORS.rst index 0c5b2fd..f51ecf6 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,7 +1,6 @@ .. This file is automatically generated/updated by a github actions workflow. .. Every manual change will be overwritten on push to main. -.. You can find it here: ``.github/workflows/update-authors.yaml`` -.. For more information see `https://github.com/rstcheck/rstcheck/graphs/contributors` +.. You can find it here: ``.github/workflows/do-update-authors.yaml`` Author ------ @@ -30,4 +29,3 @@ Additional contributions by (sorted by name) - Swen Kooij - happlebao - serhiy-yevtushenko - diff --git a/docformatter.py b/docformatter.py index e063968..13c3355 100755 --- a/docformatter.py +++ b/docformatter.py @@ -70,23 +70,6 @@ CR = "\r" LF = "\n" CRLF = "\r\n" -STR_QUOTE_TYPES = ( - '"""', - "'''", -) -RAW_QUOTE_TYPES = ( - 'r"""', - 'R"""', - "r'''", - "R'''", -) -UCODE_QUOTE_TYPES = ( - 'u"""', - 'U"""', - "u'''", - "U'''", -) -QUOTE_TYPES = STR_QUOTE_TYPES + RAW_QUOTE_TYPES + UCODE_QUOTE_TYPES _PYTHON_LIBS = set(sysconfig.get_paths().values()) @@ -180,8 +163,7 @@ def do_parse_arguments(self) -> None: type=int, metavar="length", help="wrap long summary lines at this length; " - "set to 0 to disable wrapping " - "(default: %(default)s)", + "set to 0 to disable wrapping (default: 79)", ) self.parser.add_argument( "--wrap-descriptions", @@ -190,14 +172,14 @@ def do_parse_arguments(self) -> None: metavar="length", help="wrap descriptions at this length; " "set to 0 to disable wrapping " - "(default: %(default)s)", + "(default: 72)", ) self.parser.add_argument( "--force-wrap", action="store_true", default=bool(self.flargs_dct.get("force-wrap", False)), help="force descriptions to be wrapped even if it may " - "result in a mess (default: %(default)s)", + "result in a mess (default: False)", ) self.parser.add_argument( "--tab-width", @@ -206,28 +188,28 @@ def do_parse_arguments(self) -> None: metavar="width", default=int(self.flargs_dct.get("tab-width", 1)), help="tabs in indentation are this many characters when " - "wrapping lines (default: %(default)s)", + "wrapping lines (default: 1)", ) self.parser.add_argument( "--blank", dest="post_description_blank", action="store_true", default=bool(self.flargs_dct.get("blank", False)), - help="add blank line after description (default: %(default)s)", + help="add blank line after description (default: False)", ) self.parser.add_argument( "--pre-summary-newline", action="store_true", default=bool(self.flargs_dct.get("pre-summary-newline", False)), help="add a newline before the summary of a multi-line docstring " - "(default: %(default)s)", + "(default: False)", ) self.parser.add_argument( "--pre-summary-space", action="store_true", default=bool(self.flargs_dct.get("pre-summary-space", False)), help="add a space after the opening triple quotes " - "(default: %(default)s)", + "(default: False)", ) self.parser.add_argument( "--make-summary-multi-line", @@ -236,7 +218,7 @@ def do_parse_arguments(self) -> None: self.flargs_dct.get("make-summary-multi-line", False) ), help="add a newline before and after the summary of a one-line " - "docstring (default: %(default)s)", + "docstring (default: False)", ) self.parser.add_argument( "--close-quotes-on-newline", @@ -246,7 +228,7 @@ def do_parse_arguments(self) -> None: ), help="place closing triple quotes on a new-line when a " "one-line docstring wraps to two or more lines " - "(default: %(default)s)", + "(default: False)", ) self.parser.add_argument( "--range", @@ -256,7 +238,7 @@ def do_parse_arguments(self) -> None: type=int, nargs=2, help="apply docformatter to docstrings between these " - "lines; line numbers are indexed at 1 (default: %(default)s)", + "lines; line numbers are indexed at 1 (default: None)", ) self.parser.add_argument( "--docstring-length", @@ -266,7 +248,7 @@ def do_parse_arguments(self) -> None: type=int, nargs=2, help="apply docformatter to docstrings of given length range " - "(default: %(default)s)", + "(default: None)", ) self.parser.add_argument( "--non-strict", @@ -353,6 +335,24 @@ def _do_read_parser_configuration(self) -> None: class Formator: """Format docstrings.""" + STR_QUOTE_TYPES = ( + '"""', + "'''", + ) + RAW_QUOTE_TYPES = ( + 'r"""', + 'R"""', + "r'''", + "R'''", + ) + UCODE_QUOTE_TYPES = ( + 'u"""', + 'U"""', + "u'''", + "U'''", + ) + QUOTE_TYPES = STR_QUOTE_TYPES + RAW_QUOTE_TYPES + UCODE_QUOTE_TYPES + parser = None """Parser object.""" @@ -558,7 +558,7 @@ def _format_code( ) in tokenize.generate_tokens(sio.readline): if ( token_type == tokenize.STRING - and token_string.startswith(QUOTE_TYPES) + and token_string.startswith(self.QUOTE_TYPES) and ( previous_token_type == tokenize.INDENT or only_comments_so_far @@ -625,13 +625,13 @@ def _do_format_docstring( docstring_formatted: str The docstring formatted according the various options. """ - contents, open_quote = strip_docstring(docstring) + contents, open_quote = self._do_strip_docstring(docstring) open_quote = ( f"{open_quote} " if self.args.pre_summary_space else open_quote ) # Skip if there are nested triple double quotes - if contents.count(QUOTE_TYPES[0]): + if contents.count(self.QUOTE_TYPES[0]): return docstring # Do not modify things that start with doctests. @@ -717,6 +717,122 @@ def _do_format_docstring( ).strip() return f"{beginning}{summary_wrapped}{ending}" + def _do_strip_docstring(self, docstring: str) -> Tuple[str, str]: + """Return contents of docstring and opening quote type. + + Strips the docstring of its triple quotes, trailing white space, + and line returns. Determines type of docstring quote (either string, + raw, or unicode) and returns the opening quotes, including the type + identifier, with single quotes replaced by double quotes. + + Parameters + ---------- + docstring: str + The docstring, including the opening and closing triple quotes. + + Returns + ------- + (docstring, open_quote) : tuple + The docstring with the triple quotes removed. + The opening quote type with single quotes replaced by double + quotes. + """ + docstring = docstring.strip() + + for quote in self.QUOTE_TYPES: + if quote in self.RAW_QUOTE_TYPES + self.UCODE_QUOTE_TYPES and ( + docstring.startswith(quote) and docstring.endswith(quote[1:]) + ): + return docstring.split(quote, 1)[1].rsplit(quote[1:], 1)[ + 0 + ].strip(), quote.replace("'", '"') + elif docstring.startswith(quote) and docstring.endswith(quote): + return docstring.split(quote, 1)[1].rsplit(quote, 1)[ + 0 + ].strip(), quote.replace("'", '"') + + raise ValueError( + "docformatter only handles triple-quoted (single or double) " + "strings" + ) + + +class Encodor: + """Encoding and decoding of files.""" + + CR = "\r" + LF = "\n" + CRLF = "\r\n" + + def __init__(self): + """Initialize an Encodor instance.""" + self.encoding = "latin-1" + self.system_encoding = ( + locale.getpreferredencoding() or sys.getdefaultencoding() + ) + + def do_detect_encoding(self, filename: str) -> None: + """Return the detected file encoding. + + Parameters + ---------- + filename : str + The full path name of the file whose encoding is to be detected. + """ + try: + self.encoding = from_path(filename).best().encoding + + # Check for correctness of encoding. + with self.do_open_with_encoding(filename) as check_file: + check_file.read() + except (SyntaxError, LookupError, UnicodeDecodeError): + self.encoding = "latin-1" + + def do_find_newline(self, source: str) -> Dict[int, int]: + """Return type of newline used in source. + + Paramaters + ---------- + source : list + A list of lines. + + Returns + ------- + counter : dict + A dict with the count of new line types found. + """ + assert not isinstance(source, unicode) + + counter = collections.defaultdict(int) + for line in source: + if line.endswith(self.CRLF): + counter[self.CRLF] += 1 + elif line.endswith(self.CR): + counter[self.CR] += 1 + elif line.endswith(self.LF): + counter[self.LF] += 1 + + return (sorted(counter, key=counter.get, reverse=True) or [self.LF])[0] + + def do_open_with_encoding(self, filename: str, mode: str = "r"): + """Return opened file with a specific encoding. + + Parameters + ---------- + filename : str + The full path name of the file to open. + mode : str + The mode to open the file in. Defaults to read-only. + + Returns + ------- + contents : TextIO + The contents of the file. + """ + return io.open( + filename, mode=mode, encoding=self.encoding, newline="" + ) # Preserve line endings + class Encodor: """Encoding and decoding of files.""" @@ -994,44 +1110,6 @@ def normalize_line_endings(lines, newline): return "".join([normalize_line(line, newline) for line in lines]) -def strip_docstring(docstring: str) -> Tuple[str, str]: - """Return contents of docstring and opening quote type. - - Strips the docstring of its triple quotes, trailing white space, - and line returns. Determines type of docstring quote (either string, - raw, or unicode) and returns the opening quotes, including the type - identifier, with single quotes replaced by double quotes. - - Parameters - ---------- - docstring: str - The docstring, including the opening and closing triple quotes. - - Returns - ------- - (docstring, open_quote) : tuple - The docstring with the triple quotes removed. - The opening quote type with single quotes replaced by double quotes. - """ - docstring = docstring.strip() - - for quote in QUOTE_TYPES: - if quote in RAW_QUOTE_TYPES + UCODE_QUOTE_TYPES and ( - docstring.startswith(quote) and docstring.endswith(quote[1:]) - ): - return docstring.split(quote, 1)[1].rsplit(quote[1:], 1)[ - 0 - ].strip(), quote.replace("'", '"') - elif docstring.startswith(quote) and docstring.endswith(quote): - return docstring.split(quote, 1)[1].rsplit(quote, 1)[ - 0 - ].strip(), quote.replace("'", '"') - - raise ValueError( - "docformatter only handles triple-quoted (single or double) strings" - ) - - def unwrap_summary(summary): """Return summary with newlines removed in preparation for wrapping.""" return re.sub(r"\s*\n\s*", " ", summary) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 88f4e24..f98139a 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -15,8 +15,8 @@ If no configuration file is explicitly passed, ``docformatter`` will search the current directory for the supported files and use the first one found. The order of precedence is ``pyproject.toml``, ``setup.cfg``, then ``tox.ini``. -In any of the configuration files, add a section ``[tool.docformatter]`` with -options listed using the same name as command line options. For example: +In ``pyproject.toml`` or ``tox.ini``, add a section ``[tool.docformatter]`` with +options listed using the same name as command line argument. For example: .. code-block:: yaml @@ -25,5 +25,30 @@ options listed using the same name as command line options. For example: wrap-summaries = 82 blank = true -The ``setup.cfg`` and ``tox.ini`` files will also support the -``[tool:docformatter]`` syntax. +In ``setup.cfg``, add a ``[docformatter]`` section. + +.. code-block:: yaml + + [docformatter] + recursive = true + wrap-summaries = 82 + blank = true + +Command line arguments will take precedence over configuration file settings. +For example, if the following is in your ``pyproject.toml`` + +.. code-block:: yaml + + [tool.docformatter] + recursive = true + wrap-summaries = 82 + wrap-descriptions = 81 + blank = true + +And you invoke docformatter as follows: + +.. code-block:: console + + $ docformatter --config ~/.secret/path/to/pyproject.toml --wrap-summaries 68 + +Summaries will be wrapped at 68, not 82. diff --git a/tests/test_format_docstring.py b/tests/test_format_docstring.py index 5bd49e4..476e2e8 100644 --- a/tests/test_format_docstring.py +++ b/tests/test_format_docstring.py @@ -677,7 +677,7 @@ def test_format_docstring_with_summary_only_and_wrap_and_tab_indentation( test_args, args, ): - """"Should account for length of tab when wrapping. + """Should account for length of tab when wrapping. See PR #69. """ @@ -902,3 +902,186 @@ def test_format_docstring_pre_summary_space( """This one-line docstring will have a leading space."""\ ''', ) + + +class TestStripDocstring: + """Class for testing _do_strip_docstring().""" + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_strip_docstring( + self, + test_args, + args, + ): + """Strip triple double quotes from docstring.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + docstring, open_quote = uut._do_strip_docstring( + ''' + """Hello. + + """ + + ''' + ) + assert docstring == "Hello." + assert open_quote == '"""' + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_strip_docstring_with_single_quotes( + self, + test_args, + args, + ): + """Strip triple single quotes from docstring.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + docstring, open_quote = uut._do_strip_docstring( + """ + '''Hello. + + ''' + + """ + ) + assert docstring == "Hello." + assert open_quote == '"""' + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_strip_docstring_with_empty_string( + self, + test_args, + args, + ): + """Return series of six double quotes when passed empty string.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + docstring, open_quote = uut._do_strip_docstring('""""""') + assert docstring == "" + assert open_quote == '"""' + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_strip_docstring_with_raw_string( + self, + test_args, + args, + ): + """Return docstring and raw open quote.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + docstring, open_quote = uut._do_strip_docstring('r"""foo"""') + assert docstring == "foo" + assert open_quote == 'r"""' + + docstring, open_quote = uut._do_strip_docstring("R'''foo'''") + assert docstring == "foo" + assert open_quote == 'R"""' + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_strip_docstring_with_unicode_string( + self, + test_args, + args, + ): + """Return docstring and unicode open quote.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + docstring, open_quote = uut._do_strip_docstring("u'''foo'''") + assert docstring == "foo" + assert open_quote == 'u"""' + + docstring, open_quote = uut._do_strip_docstring('U"""foo"""') + assert docstring == "foo" + assert open_quote == 'U"""' + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_strip_docstring_with_unknown( + self, + test_args, + args, + ): + """Raise ValueError with single quotes.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + with pytest.raises(ValueError): + uut._do_strip_docstring("foo") + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_strip_docstring_with_single_quotes( + self, + test_args, + args, + ): + """Raise ValueError when strings begin with single single quotes. + + See requirement PEP_257_1. See issue #66 for example of docformatter + breaking code when encountering single quote. + """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + with pytest.raises(ValueError): + uut._do_strip_docstring("'hello\\''") + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_strip_docstring_with_double_quotes( + self, + test_args, + args, + ): + """Raise ValueError when strings begin with single double quotes. + + See requirement PEP_257_1. See issue #66 for example of docformatter + breaking code when encountering single quote. + """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + with pytest.raises(ValueError): + uut._do_strip_docstring('"hello\\""') diff --git a/tests/test_string_functions.py b/tests/test_string_functions.py index 87163f8..a0ec0e2 100644 --- a/tests/test_string_functions.py +++ b/tests/test_string_functions.py @@ -447,86 +447,3 @@ def test_remove_section_header(self): line = " \nfoo\nbar\n" assert line == docformatter.remove_section_header(line) - - @pytest.mark.unit - def test_strip_docstring(self): - """Strip triple double quotes from docstring.""" - docstring, open_quote = docformatter.strip_docstring( - ''' - """Hello. - - """ - - ''' - ) - assert docstring == "Hello." - assert open_quote == '"""' - - @pytest.mark.unit - def test_strip_docstring_with_single_quotes(self): - """Strip triple single quotes from docstring.""" - docstring, open_quote == docformatter.strip_docstring( - """ - '''Hello. - - ''' - - """ - ) - assert docstring == "Hello." - assert open_quote == '"""' - - @pytest.mark.unit - def test_strip_docstring_with_empty_string(self): - """Return series of six double quotes when passed empty string.""" - docstring, open_quote = docformatter.strip_docstring('""""""') - assert docstring == "" - assert open_quote == '"""' - - @pytest.mark.unit - def test_strip_docstring_with_raw_string(self): - """Return docstring and raw open quote.""" - docstring, open_quote = docformatter.strip_docstring('r"""foo"""') - assert docstring == "foo" - assert open_quote == 'r"""' - - docstring, open_quote = docformatter.strip_docstring("R'''foo'''") - assert docstring == "foo" - assert open_quote == 'R"""' - - @pytest.mark.unit - def test_strip_docstring_with_unicode_string(self): - """Return docstring and unicode open quote.""" - docstring, open_quote = docformatter.strip_docstring("u'''foo'''") - assert docstring == "foo" - assert open_quote == 'u"""' - - docstring, open_quote = docformatter.strip_docstring('U"""foo"""') - assert docstring == "foo" - assert open_quote == 'U"""' - - @pytest.mark.unit - def test_strip_docstring_with_unknown(self): - """Raise ValueError with single quotes.""" - with pytest.raises(ValueError): - docformatter.strip_docstring("foo") - - @pytest.mark.unit - def test_strip_docstring_with_single_quotes(self): - """Raise ValueError when strings begin with single single quotes. - - See requirement #1. See issue #66 for example of docformatter breaking - code when encountering single quote. - """ - with pytest.raises(ValueError): - docformatter.strip_docstring("'hello\\''") - - @pytest.mark.unit - def test_strip_docstring_with_double_quotes(self): - """Raise ValueError when strings begin with single double quotes. - - See requirement #1. See issue #66 for example of docformatter - breaking code when encountering single quote. - """ - with pytest.raises(ValueError): - docformatter.strip_docstring('"hello\\""')