From 3ebb8f0a542874bb29ba3b5bd9b5ab5b890ae819 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Fri, 19 Aug 2022 15:48:07 -0400 Subject: [PATCH] refactor: move functions to classes (#110) * chore: update version number to 1.5.0 * docs: add REQUIREMENTS document * doc: create user documentation * docs: add license to documentation * chore: add workflow to update AUTHORS file * docs: split up usage.rst * docs: clean up README * chore: add COVERAGE_FILE env variable * chore: add conf.py to files for version update * docs: add link to RTD in the README * chore: update pyproject.toml dependencies * fix: AttributeError when no config file exists * test: update tests for _format_code now in class * test: update tests for _do_format_docstring now in class --- .github/workflows/do-update-authors.yml | 5 +- AUTHORS.rst | 2 +- docformatter.py | 711 +++++++++++++----------- pyproject.toml | 1 + tests/conftest.py | 125 +++++ tests/test_format_code.py | 570 ++++++++++++++----- tests/test_format_docstring.py | 564 +++++++++++++++---- 7 files changed, 1401 insertions(+), 577 deletions(-) diff --git a/.github/workflows/do-update-authors.yml b/.github/workflows/do-update-authors.yml index 274d090..d20c970 100644 --- a/.github/workflows/do-update-authors.yml +++ b/.github/workflows/do-update-authors.yml @@ -40,6 +40,7 @@ jobs: "pre-commit-ci", "github-action", "GitHub Actions", + "Sourcery AI", ) authors = [ author @@ -51,9 +52,7 @@ jobs: file_head = ( ".. This file is automatically generated/updated by a github actions workflow.\n" ".. Every manual change will be overwritten on push to main.\n" - ".. You can find it here: ``.github/workflows/update-authors.yaml``\n" - ".. For more information see " - "`https://github.com/rstcheck/rstcheck/graphs/contributors`\n\n" + ".. You can find it here: ``.github/workflows/do-update-authors.yaml``\n\n" "Author\n" "------\n" "Steven Myint \n\n" diff --git a/AUTHORS.rst b/AUTHORS.rst index 05273ba..0c5b2fd 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -27,7 +27,7 @@ Additional contributions by (sorted by name) - Paul Angerer <48882462+etimoz@users.noreply.github.com> - Peter Boothe - Sho Iwamoto -- Sourcery AI <> - Swen Kooij - happlebao - serhiy-yevtushenko + diff --git a/docformatter.py b/docformatter.py index 2e7ee98..0fcfffc 100755 --- a/docformatter.py +++ b/docformatter.py @@ -46,7 +46,7 @@ import textwrap import tokenize from configparser import ConfigParser -from typing import List, Tuple, Union +from typing import List, TextIO, Tuple, Union # Third Party Imports import untokenize @@ -92,7 +92,7 @@ _PYTHON_LIBS = set(sysconfig.get_paths().values()) -class FormatResult(object): +class FormatResult: """Possible exit codes.""" ok = 0 @@ -201,13 +201,13 @@ def do_parse_arguments(self) -> None: "result in a mess (default: %(default)s)", ) self.parser.add_argument( - "--tab_width", + "--tab-width", type=int, dest="tab_width", 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: %(default)s)", ) self.parser.add_argument( "--blank", @@ -351,6 +351,368 @@ def _do_read_parser_configuration(self) -> None: } +class Formator: + """Format docstrings.""" + + parser = None + """Parser object.""" + + args: argparse.Namespace = None + + def __init__( + self, + args: argparse.Namespace, + stderror: TextIO, + stdin: TextIO, + stdout: TextIO, + ) -> None: + """Initialize a Formattor instance. + + Parameters + ---------- + args : argparse.Namespace + Any command line arguments passed during invocation or + configuration file options. + stderror : TextIO + The standard error device. Typically, the screen. + stdin : TextIO + The standard input device. Typically, the keyboard. + stdout : TextIO + The standard output device. Typically, the screen. + + Returns + ------- + object + """ + self.args = args + self.stderror: TextIO = stderror + self.stdin: TextIO = stdin + self.stdout: TextIO = stdout + + def do_format_standard_in(self, parser: argparse.ArgumentParser): + """Print formatted text to standard out. + + Parameters + ---------- + parser: argparse.ArgumentParser + The argument parser containing the formatting options. + """ + if len(self.args.files) > 1: + parser.error("cannot mix standard in and regular files") + + if self.args.in_place: + parser.error("--in-place cannot be used with standard input") + + if self.args.recursive: + parser.error("--recursive cannot be used with standard input") + + encoding = None + source = self.stdin.read() + if not isinstance(source, unicode): + encoding = self.stdin.encoding or _get_encoding() + source = source.decode(encoding) + + formatted_source = self._do_format_code(source) + + if encoding: + formatted_source = formatted_source.encode(encoding) + + self.stdout.write(formatted_source) + + def do_format_files(self): + """Format multiple files. + + Return + ------ + code: int + One of the FormatResult codes. + """ + outcomes = collections.Counter() + for filename in find_py_files( + set(self.args.files), self.args.recursive, self.args.exclude + ): + try: + result = self._do_format_file(filename) + outcomes[result] += 1 + if result == FormatResult.check_failed: + print(unicode(filename), file=self.stderror) + except IOError as exception: + outcomes[FormatResult.error] += 1 + print(unicode(exception), file=self.stderror) + + return_codes = [ # in order of preference + FormatResult.error, + FormatResult.check_failed, + FormatResult.ok, + ] + + for code in return_codes: + if outcomes[code]: + return code + + def _do_format_file(self, filename): + """Run format_code() on a file. + + Parameters + ---------- + filename: str + The path to the file to be formatted. + + Return + ------ + code: int + One of the FormatResult codes. + """ + encoding = detect_encoding(filename) + with open_with_encoding(filename, encoding=encoding) as input_file: + source = input_file.read() + formatted_source = self._do_format_code(source) + + if source != formatted_source: + if self.args.check: + return FormatResult.check_failed + elif self.args.in_place: + with open_with_encoding( + filename, mode="w", encoding=encoding + ) as output_file: + output_file.write(formatted_source) + else: + # Standard Library Imports + import difflib + + diff = difflib.unified_diff( + source.splitlines(), + formatted_source.splitlines(), + f"before/{filename}", + f"after/{filename}", + lineterm="", + ) + self.stdout.write("\n".join(list(diff) + [""])) + + return FormatResult.ok + + def _do_format_code(self, source): + """Return source code with docstrings formatted. + + Parameters + ---------- + source: str + The text from the source file. + """ + try: + original_newline = find_newline(source.splitlines(True)) + code = self._format_code(source) + + return normalize_line_endings( + code.splitlines(True), original_newline + ) + except (tokenize.TokenError, IndentationError): + return source + + def _format_code( + self, + source, + ): + """Return source code with docstrings formatted. + + Parameters + ---------- + source: str + The source code string. + + Returns + ------- + formatted_source: str + The source code with formatted docstrings. + """ + if not source: + return source + + if self.args.line_range is not None: + assert self.args.line_range[0] > 0 and self.args.line_range[1] > 0 + + if self.args.length_range is not None: + assert ( + self.args.length_range[0] > 0 and self.args.length_range[1] > 0 + ) + + modified_tokens = [] + + sio = io.StringIO(source) + previous_token_string = "" + previous_token_type = None + only_comments_so_far = True + + try: + for ( + token_type, + token_string, + start, + end, + line, + ) in tokenize.generate_tokens(sio.readline): + if ( + token_type == tokenize.STRING + and token_string.startswith(QUOTE_TYPES) + and ( + previous_token_type == tokenize.INDENT + or only_comments_so_far + ) + and is_in_range(self.args.line_range, start[0], end[0]) + and has_correct_length( + self.args.length_range, start[0], end[0] + ) + ): + indentation = ( + "" if only_comments_so_far else previous_token_string + ) + token_string = self._do_format_docstring( + indentation, + token_string, + ) + + if token_type not in [ + tokenize.COMMENT, + tokenize.NEWLINE, + tokenize.NL, + ]: + only_comments_so_far = False + + previous_token_string = token_string + previous_token_type = token_type + + # If the current token is a newline, the previous token was a + # newline or a comment, and these two sequential newlines + # follow a function definition, ignore the blank line. + if ( + len(modified_tokens) <= 2 + or token_type not in {tokenize.NL, tokenize.NEWLINE} + or modified_tokens[-1][0] + not in {tokenize.NL, tokenize.NEWLINE} + or modified_tokens[-2][1] != ":" + and modified_tokens[-2][0] != tokenize.COMMENT + or modified_tokens[-2][4][:3] != "def" + ): + modified_tokens.append( + (token_type, token_string, start, end, line) + ) + + return untokenize.untokenize(modified_tokens) + except tokenize.TokenError: + return source + + def _do_format_docstring( + self, + indentation: str, + docstring: str, + ) -> str: + """Return formatted version of docstring. + + Parameters + ---------- + indentation: str + The indentation characters for the docstring. + docstring: str + The docstring itself. + + Returns + ------- + docstring_formatted: str + The docstring formatted according the various options. + """ + contents, open_quote = 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]): + return docstring + + # Do not modify things that start with doctests. + if contents.lstrip().startswith(">>>"): + return docstring + + summary, description = split_summary_and_description(contents) + + # Leave docstrings with underlined summaries alone. + if remove_section_header(description).strip() != description.strip(): + return docstring + + if not self.args.force_wrap and is_some_sort_of_list( + summary, + self.args.non_strict, + ): + # Something is probably not right with the splitting. + return docstring + + # Compensate for textwrap counting each tab in indentation as 1 + # character. + tab_compensation = indentation.count("\t") * (self.args.tab_width - 1) + self.args.wrap_summaries -= tab_compensation + self.args.wrap_descriptions -= tab_compensation + + if description: + # Compensate for triple quotes by temporarily prepending 3 spaces. + # This temporary prepending is undone below. + initial_indent = ( + indentation + if self.args.pre_summary_newline + else 3 * " " + indentation + ) + pre_summary = ( + "\n" + indentation if self.args.pre_summary_newline else "" + ) + summary = wrap_summary( + normalize_summary(summary), + wrap_length=self.args.wrap_summaries, + initial_indent=initial_indent, + subsequent_indent=indentation, + ).lstrip() + description = wrap_description( + description, + indentation=indentation, + wrap_length=self.args.wrap_descriptions, + force_wrap=self.args.force_wrap, + strict=self.args.non_strict, + ) + post_description = "\n" if self.args.post_description_blank else "" + return f'''\ +{open_quote}{pre_summary}{summary} + +{description}{post_description} +{indentation}"""\ +''' + else: + if not self.args.make_summary_multi_line: + summary_wrapped = wrap_summary( + open_quote + normalize_summary(contents) + '"""', + wrap_length=self.args.wrap_summaries, + initial_indent=indentation, + subsequent_indent=indentation, + ).strip() + if ( + self.args.close_quotes_on_newline + and "\n" in summary_wrapped + ): + summary_wrapped = ( + f"{summary_wrapped[:-3]}" + f"\n{indentation}" + f"{summary_wrapped[-3:]}" + ) + return summary_wrapped + else: + beginning = f"{open_quote}\n{indentation}" + ending = f'\n{indentation}"""' + summary_wrapped = wrap_summary( + normalize_summary(contents), + wrap_length=self.args.wrap_summaries, + initial_indent=indentation, + subsequent_indent=indentation, + ).strip() + return f"{beginning}{summary_wrapped}{ending}" + + def has_correct_length(length_range, start, end): """Return True if docstring's length is in range.""" if length_range is None: @@ -402,219 +764,6 @@ def _find_shortest_indentation(lines): return indentation or "" -def format_code(source, **kwargs): - """Return source code with docstrings formatted. - - Wrap summary lines if summary_wrap_length is greater than 0. - - See "_format_code()" for parameters. - """ - try: - original_newline = find_newline(source.splitlines(True)) - code = _format_code(source, **kwargs) - - return normalize_line_endings(code.splitlines(True), original_newline) - except (tokenize.TokenError, IndentationError): - return source - - -def _format_code( - source, - summary_wrap_length=79, - description_wrap_length=72, - force_wrap=False, - tab_width=1, - pre_summary_newline=False, - pre_summary_space=False, - make_summary_multi_line=False, - close_quotes_on_newline=False, - post_description_blank=False, - - line_range=None, - length_range=None, - strict=True, -): - """Return source code with docstrings formatted.""" - if not source: - return source - - if line_range is not None: - assert line_range[0] > 0 and line_range[1] > 0 - - if length_range is not None: - assert length_range[0] > 0 and length_range[1] > 0 - - modified_tokens = [] - - sio = io.StringIO(source) - previous_token_string = "" - previous_token_type = None - only_comments_so_far = True - - for ( - token_type, - token_string, - start, - end, - line, - ) in tokenize.generate_tokens(sio.readline): - if ( - token_type == tokenize.STRING - and token_string.startswith(QUOTE_TYPES) - and ( - previous_token_type == tokenize.INDENT or only_comments_so_far - ) - and is_in_range(line_range, start[0], end[0]) - and has_correct_length(length_range, start[0], end[0]) - ): - indentation = "" if only_comments_so_far else previous_token_string - - token_string = format_docstring( - indentation, - token_string, - summary_wrap_length=summary_wrap_length, - description_wrap_length=description_wrap_length, - force_wrap=force_wrap, - tab_width=tab_width, - pre_summary_newline=pre_summary_newline, - pre_summary_space=pre_summary_space, - make_summary_multi_line=make_summary_multi_line, - close_quotes_on_newline=close_quotes_on_newline, - post_description_blank=post_description_blank, - strict=strict, - ) - - if token_type not in [tokenize.COMMENT, tokenize.NEWLINE, tokenize.NL]: - only_comments_so_far = False - - previous_token_string = token_string - previous_token_type = token_type - - # If the current token is a newline, the previous token was a - # newline or a comment, and these two sequential newlines follow a - # function definition, ignore the blank line. - if ( - len(modified_tokens) <= 2 - or token_type not in {tokenize.NL, tokenize.NEWLINE} - or modified_tokens[-1][0] not in {tokenize.NL, tokenize.NEWLINE} - or modified_tokens[-2][1] != ":" - and modified_tokens[-2][0] != tokenize.COMMENT - or modified_tokens[-2][4][:3] != "def" - ): - modified_tokens.append( - (token_type, token_string, start, end, line) - ) - - return untokenize.untokenize(modified_tokens) - - -def format_docstring( - indentation, - docstring, - summary_wrap_length=0, - description_wrap_length=0, - force_wrap=False, - tab_width=1, - pre_summary_newline=False, - pre_summary_space=False, - make_summary_multi_line=False, - close_quotes_on_newline=False, - post_description_blank=False, - strict=True, -): - """Return formatted version of docstring. - - Wrap summary lines if summary_wrap_length is greater than 0. - - Relevant parts of PEP 257: - - For consistency, always use triple double quotes around docstrings. - - Triple quotes are used even though the string fits on one line. - - Multi-line docstrings consist of a summary line just like a one-line - docstring, followed by a blank line, followed by a more elaborate - description. - - Unless the entire docstring fits on a line, place the closing quotes - on a line by themselves. - """ - contents, open_quote = strip_docstring(docstring) - open_quote = f"{open_quote} " if pre_summary_space else open_quote - - # Skip if there are nested triple double quotes - if contents.count(QUOTE_TYPES[0]): - return docstring - - # Do not modify things that start with doctests. - if contents.lstrip().startswith(">>>"): - return docstring - - summary, description = split_summary_and_description(contents) - - # Leave docstrings with underlined summaries alone. - if remove_section_header(description).strip() != description.strip(): - return docstring - - if not force_wrap and is_some_sort_of_list(summary, strict): - # Something is probably not right with the splitting. - return docstring - - # Compensate for textwrap counting each tab in indentation as 1 character. - tab_compensation = indentation.count('\t') * (tab_width - 1) - summary_wrap_length -= tab_compensation - description_wrap_length -= tab_compensation - - if description: - # Compensate for triple quotes by temporarily prepending 3 spaces. - # This temporary prepending is undone below. - initial_indent = ( - indentation if pre_summary_newline else 3 * " " + indentation - ) - pre_summary = "\n" + indentation if pre_summary_newline else "" - summary = wrap_summary( - normalize_summary(summary), - wrap_length=summary_wrap_length, - initial_indent=initial_indent, - subsequent_indent=indentation, - ).lstrip() - description = wrap_description( - description, - indentation=indentation, - wrap_length=description_wrap_length, - force_wrap=force_wrap, - strict=strict, - ) - post_description = "\n" if post_description_blank else "" - return f'''\ -{open_quote}{pre_summary}{summary} - -{description}{post_description} -{indentation}"""\ -''' - else: - if not make_summary_multi_line: - summary_wrapped = wrap_summary( - open_quote + normalize_summary(contents) + '"""', - wrap_length=summary_wrap_length, - initial_indent=indentation, - subsequent_indent=indentation, - ).strip() - if close_quotes_on_newline and "\n" in summary_wrapped: - summary_wrapped = ( - f"{summary_wrapped[:-3]}" - f"\n{indentation}" - f"{summary_wrapped[-3:]}" - ) - return summary_wrapped - else: - beginning = f"{open_quote}\n{indentation}" - ending = f'\n{indentation}"""' - summary_wrapped = wrap_summary( - normalize_summary(contents), - wrap_length=summary_wrap_length, - initial_indent=indentation, - subsequent_indent=indentation, - ).strip() - return f"{beginning}{summary_wrapped}{ending}" - - def is_probably_beginning_of_sentence(line): """Return True if this line begins a new sentence.""" # Check heuristically for a parameter list. @@ -949,101 +1098,24 @@ def detect_encoding(filename): return "latin-1" -def format_file(filename, args, standard_out): - """Run format_code() on a file. - - Return: one of the FormatResult codes. - """ - encoding = detect_encoding(filename) - with open_with_encoding(filename, encoding=encoding) as input_file: - source = input_file.read() - formatted_source = _format_code_with_args(source, args) - - if source != formatted_source: - if args.check: - return FormatResult.check_failed - elif args.in_place: - with open_with_encoding( - filename, mode="w", encoding=encoding - ) as output_file: - output_file.write(formatted_source) - else: - # Standard Library Imports - import difflib - - diff = difflib.unified_diff( - source.splitlines(), - formatted_source.splitlines(), - f"before/{filename}", - f"after/{filename}", - lineterm="", - ) - standard_out.write("\n".join(list(diff) + [""])) - - return FormatResult.ok - - -def _format_code_with_args(source, args): - """Run format_code with parsed command-line arguments.""" - return format_code( - source, - summary_wrap_length=args.wrap_summaries, - description_wrap_length=args.wrap_descriptions, - force_wrap=args.force_wrap, - tab_width=args.tab_width, - pre_summary_newline=args.pre_summary_newline, - pre_summary_space=args.pre_summary_space, - make_summary_multi_line=args.make_summary_multi_line, - close_quotes_on_newline=args.close_quotes_on_newline, - post_description_blank=args.post_description_blank, - line_range=args.line_range, - strict=not args.non_strict, - ) - - def _main(argv, standard_out, standard_error, standard_in): """Run internal main entry point.""" configurator = Configurator(argv) configurator.do_parse_arguments() + formator = Formator( + configurator.args, + stderror=standard_error, + stdin=standard_in, + stdout=standard_out, + ) + if "-" in configurator.args.files: - _format_standard_in( - configurator.args, - parser=configurator.parser, - standard_out=standard_out, - standard_in=standard_in, + formator.do_format_standard_in( + configurator.parser, ) else: - return _format_files( - configurator.args, - standard_out=standard_out, - standard_error=standard_error, - ) - - -def _format_standard_in(args, parser, standard_out, standard_in): - """Print formatted text to standard out.""" - if len(args.files) > 1: - parser.error("cannot mix standard in and regular files") - - if args.in_place: - parser.error("--in-place cannot be used with standard input") - - if args.recursive: - parser.error("--recursive cannot be used with standard input") - - encoding = None - source = standard_in.read() - if not isinstance(source, unicode): - encoding = standard_in.encoding or _get_encoding() - source = source.decode(encoding) - - formatted_source = _format_code_with_args(source, args=args) - - if encoding: - formatted_source = formatted_source.encode(encoding) - - standard_out.write(formatted_source) + return formator.do_format_files() def _get_encoding(): @@ -1104,37 +1176,6 @@ def is_excluded(name, exclude): yield name -def _format_files(args, standard_out, standard_error): - """Format multiple files. - - Return: one of the FormatResult codes. - """ - outcomes = collections.Counter() - for filename in find_py_files( - set(args.files), args.recursive, args.exclude - ): - try: - result = format_file( - filename, args=args, standard_out=standard_out - ) - outcomes[result] += 1 - if result == FormatResult.check_failed: - print(unicode(filename), file=standard_error) - except IOError as exception: - outcomes[FormatResult.error] += 1 - print(unicode(exception), file=standard_error) - - return_codes = [ # in order of preference - FormatResult.error, - FormatResult.check_failed, - FormatResult.ok, - ] - - for code in return_codes: - if outcomes[code]: - return code - - def main(): """Run main entry point.""" # SIGPIPE is not available on Windows. diff --git a/pyproject.toml b/pyproject.toml index caeda9b..ec068f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -182,6 +182,7 @@ deps = setenv = COVERAGE_FILE={toxinidir}/.coverage commands = + pip install -U pip pip install .[tomli] pytest -s -x -c ./pyproject.toml --cache-clear \ --cov-config=pyproject.toml --cov=docformatter \ diff --git a/tests/conftest.py b/tests/conftest.py index d2b1c90..c31d6e1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ """docformatter test suite configuration file.""" # Standard Library Imports +import argparse import os import shutil import subprocess @@ -100,3 +101,127 @@ def run_docformatter(arguments, temporary_file): stdin=subprocess.PIPE, env=environ, ) + + +@pytest.fixture(scope="function") +def test_args(args): + """Create a set of arguments to use with tests. + + To pass no arguments, just an empty file name: + @pytest.mark.parametrize("args", [[""]]) + + To pass an argument AND empty file name: + @pytest.mark.parametrize("args", [["--wrap-summaries", "79", ""]]) + """ + parser = argparse.ArgumentParser( + description="parser object for docformatter tests", + prog="docformatter", + ) + + changes = parser.add_mutually_exclusive_group() + changes.add_argument( + "-i", + "--in-place", + action="store_true", + ) + changes.add_argument( + "-c", + "--check", + action="store_true", + ) + parser.add_argument( + "-r", + "--recursive", + action="store_true", + default=False, + ) + parser.add_argument( + "-e", + "--exclude", + nargs="*", + ) + parser.add_argument( + "--wrap-summaries", + default=79, + type=int, + metavar="length", + ) + parser.add_argument( + "--wrap-descriptions", + default=72, + type=int, + metavar="length", + ) + parser.add_argument( + "--force-wrap", + action="store_true", + default=False, + ) + parser.add_argument( + "--tab-width", + type=int, + dest="tab_width", + metavar="width", + default=1, + ) + parser.add_argument( + "--blank", + dest="post_description_blank", + action="store_true", + default=False, + ) + parser.add_argument( + "--pre-summary-newline", + action="store_true", + default=False, + ) + parser.add_argument( + "--pre-summary-space", + action="store_true", + default=False, + ) + parser.add_argument( + "--make-summary-multi-line", + action="store_true", + default=False, + ) + parser.add_argument( + "--close-quotes-on-newline", + action="store_true", + default=False, + ) + parser.add_argument( + "--range", + metavar="line", + dest="line_range", + default=None, + type=int, + nargs=2, + ) + parser.add_argument( + "--docstring-length", + metavar="length", + dest="length_range", + default=None, + type=int, + nargs=2, + ) + parser.add_argument( + "--non-strict", + action="store_true", + default=False, + ) + parser.add_argument( + "--config", + ) + parser.add_argument( + "--version", + action="version", + version="test version", + ) + parser.add_argument( + "files", + nargs="+", + ) + + yield parser.parse_args(args) diff --git a/tests/test_format_code.py b/tests/test_format_code.py index 706e35d..a830e32 100644 --- a/tests/test_format_code.py +++ b/tests/test_format_code.py @@ -24,91 +24,72 @@ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -"""Module for testing the format_code() function.""" +"""Module for testing the Formattor._do_format_code() function.""" + +# Standard Library Imports +import sys # Third Party Imports import pytest # docformatter Package Imports import docformatter +from docformatter import Formator class TestFormatCode: - """Class for testing format_code() with no arguments.""" + """Class for testing _format_code() with no arguments.""" @pytest.mark.unit - def test_format_code(self): - """Should place one-liner on single line.""" - assert '''\ -def foo(): - """Hello foo.""" -''' == docformatter.format_code( - '''\ -def foo(): - """ - Hello foo. - """ -''' - ) - - @pytest.mark.unit - def test_format_code_with_module_docstring(self): - """Should format module docstrings.""" - assert '''\ -#!/usr/env/bin python -"""This is a module docstring. - -1. One -2. Two -""" - -"""But -this -is -not.""" -''' == docformatter.format_code( - '''\ -#!/usr/env/bin python -"""This is -a module -docstring. - -1. One -2. Two -""" - -"""But -this -is -not.""" -''' + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_should_ignore_non_docstring(self, test_args, args): + """Should ignore triple quoted strings that are assigned values.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, ) - @pytest.mark.unit - def test_format_code_should_ignore_non_docstring(self): - """Shoud ignore triple quoted strings that are assigned values.""" source = '''\ x = """This is -not.""" +not a +docstring.""" ''' - assert source == docformatter.format_code(source) + assert source == uut._format_code(source) @pytest.mark.unit - def test_format_code_with_empty_string(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_empty_string(self, test_args, args): """Should do nothing with an empty string.""" - assert "" == docformatter.format_code("") - assert "" == docformatter.format_code("") + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert "" == uut._format_code("") + assert "" == uut._format_code("") @pytest.mark.unit - def test_format_code_with_tabs(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_tabs(self, test_args, args): """Should retain tabbed indentation.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): \t"""Hello foo.""" \tif True: \t\tx = 1 -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): \t""" @@ -120,14 +101,22 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_mixed_tabs(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_mixed_tabs(self, test_args, args): """Should retain mixed tabbed and spaced indentation.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): \t"""Hello foo.""" \tif True: \t x = 1 -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): \t""" @@ -139,13 +128,21 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_escaped_newlines(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_escaped_newlines(self, test_args, args): """Should leave escaped newlines in code untouched.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert r'''def foo(): """Hello foo.""" x = \ 1 -''' == docformatter.format_code( +''' == uut._format_code( r'''def foo(): """ Hello foo. @@ -156,15 +153,23 @@ def test_format_code_with_escaped_newlines(self): ) @pytest.mark.unit - def test_format_code_with_comments(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_comments(self, test_args, args): """Should leave comments as is.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert r''' def foo(): """Hello foo.""" # My comment # My comment with escape \ 123 -'''.lstrip() == docformatter.format_code( +'''.lstrip() == uut._format_code( r''' def foo(): """ @@ -177,13 +182,23 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_escaped_newline_in_inline_comment(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_escaped_newline_in_inline_comment( + self, test_args, args + ): """Should leave code with inline comment as is.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert r''' def foo(): """Hello foo.""" def test_method_no_chr_92(): the501(92) # \ -'''.lstrip() == docformatter.format_code( +'''.lstrip() == uut._format_code( r''' def foo(): """ @@ -194,16 +209,24 @@ def test_method_no_chr_92(): the501(92) # \ ) @pytest.mark.unit - def test_format_code_raw_docstring_double_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_raw_docstring_double_quotes(self, test_args, args): """Should format raw docstrings with triple double quotes. See requirement PEP_257_2. See issue #54 for request to handle raw docstrings. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): r"""Hello raw foo.""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): r""" @@ -215,7 +238,7 @@ def foo(): assert '''\ def foo(): R"""Hello Raw foo.""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): R""" @@ -225,16 +248,24 @@ def foo(): ) @pytest.mark.unit - def test_format_code_raw_docstring_single_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_raw_docstring_single_quotes(self, test_args, args): """Should format raw docstrings with triple single quotes. See requirement PEP_257_2. See issue #54 for request to handle raw docstrings. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): r"""Hello raw foo.""" -''' == docformatter.format_code( +''' == uut._format_code( """\ def foo(): r''' @@ -246,7 +277,7 @@ def foo(): assert '''\ def foo(): R"""Hello Raw foo.""" -''' == docformatter.format_code( +''' == uut._format_code( """\ def foo(): R''' @@ -256,16 +287,26 @@ def foo(): ) @pytest.mark.unit - def test_format_code_unicode_docstring_double_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_unicode_docstring_double_quotes( + self, test_args, args + ): """Should format unicode docstrings with triple double quotes. See requirement PEP_257_3. See issue #54 for request to handle raw docstrings. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): u"""Hello unicode foo.""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): u""" @@ -277,7 +318,7 @@ def foo(): assert '''\ def foo(): U"""Hello Unicode foo.""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): U""" @@ -287,16 +328,26 @@ def foo(): ) @pytest.mark.unit - def test_format_code_unicode_docstring_single_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_unicode_docstring_single_quotes( + self, test_args, args + ): """Should format unicode docstrings with triple single quotes. See requirement PEP_257_3. See issue #54 for request to handle raw docstrings. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): u"""Hello unicode foo.""" -''' == docformatter.format_code( +''' == uut._format_code( """\ def foo(): u''' @@ -308,7 +359,7 @@ def foo(): assert '''\ def foo(): U"""Hello Unicode foo.""" -''' == docformatter.format_code( +''' == uut._format_code( """\ def foo(): U''' @@ -318,25 +369,41 @@ def foo(): ) @pytest.mark.unit - def test_format_code_skip_nested(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_skip_nested(self, test_args, args): """Should ignore nested triple quotes.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + code = """\ def foo(): '''Hello foo. \"\"\"abc\"\"\" ''' """ - assert code == docformatter.format_code(code) + assert code == uut._format_code(code) @pytest.mark.unit - def test_format_code_with_multiple_sentences(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_multiple_sentences(self, test_args, args): """Should create multi-line docstring from multiple sentences.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo. This is a docstring. """ -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): """ @@ -347,15 +414,25 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_multiple_sentences_same_line(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_multiple_sentences_same_line( + self, test_args, args + ): """Should create multi-line docstring from multiple sentences.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo. This is a docstring. """ -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): """ @@ -365,15 +442,25 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_multiple_sentences_multi_line_summary(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_multiple_sentences_multi_line_summary( + self, test_args, args + ): """Should put summary line on a single line.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo. This is a docstring. """ -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): """ @@ -384,15 +471,23 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_empty_lines(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_empty_lines(self, test_args, args): """Summary line on one line when wrapped, followed by empty line.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo and this is a docstring. More stuff. """ -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): """ @@ -405,15 +500,23 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_trailing_whitespace(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_trailing_whitespace(self, test_args, args): """Should strip trailing whitespace.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo and this is a docstring. More stuff. """ -''' == docformatter.format_code( +''' == uut._format_code( ( '''\ def foo(): @@ -428,8 +531,16 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_parameters_list(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_parameters_list(self, test_args, args): """Should treat parameters list as elaborate description.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Test. @@ -437,7 +548,7 @@ def foo(): one - first two - second """ -''' == docformatter.format_code( +''' == uut._format_code( ( '''\ def foo(): @@ -450,16 +561,24 @@ def foo(): ) @pytest.mark.unit - def test_ignore_code_with_single_quote(self): + @pytest.mark.parametrize("args", [[""]]) + def test_ignore_code_with_single_quote(self, test_args, args): """Single single quote on first line of code should remain untouched. 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, + ) + assert """\ def foo(): 'Just a regular string' -""" == docformatter.format_code( +""" == uut._format_code( """\ def foo(): 'Just a regular string' @@ -467,16 +586,24 @@ def foo(): ) @pytest.mark.unit - def test_ignore_code_with_double_quote(self): + @pytest.mark.parametrize("args", [[""]]) + def test_ignore_code_with_double_quote(self, test_args, args): """Single double quotes on first line of code should remain untouched. 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, + ) + assert """\ def foo(): "Just a regular string" -""" == docformatter.format_code( +""" == uut._format_code( """\ def foo(): "Just a regular string" @@ -484,21 +611,39 @@ def foo(): ) @pytest.mark.unit - def test_format_code_should_skip_nested_triple_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_should_skip_nested_triple_quotes( + self, test_args, args + ): """Should ignore triple quotes nested in a string.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + line = '''\ def foo(): 'Just a """foo""" string' ''' - assert line == docformatter.format_code(line) + assert line == uut._format_code(line) @pytest.mark.unit - def test_format_code_with_assignment_on_first_line(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_assignment_on_first_line(self, test_args, args): """Should ignore triple quotes in variable assignment.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): x = """Just a regular string. Alpha.""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): x = """Just a regular string. Alpha.""" @@ -506,8 +651,16 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_regular_strings_too(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_regular_strings_too(self, test_args, args): """Should ignore triple quoted strings after the docstring.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo and this is a docstring. @@ -520,7 +673,7 @@ def foo(): """More stuff that should not be touched\t""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): """ @@ -539,23 +692,59 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_syntax_error(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_syntax_error(self, test_args, args): """Should ignore single set of triple quotes followed by newline.""" - assert '"""\n' == docformatter.format_code('"""\n') + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""\n' == uut._format_code('"""\n') @pytest.mark.unit - def test_format_code_with_syntax_error_case_slash_r(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_syntax_error_case_slash_r(self, test_args, args): """Should ignore single set of triple quotes followed by return.""" - assert '"""\r' == docformatter.format_code('"""\r') + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""\r' == uut._format_code('"""\r') @pytest.mark.unit - def test_format_code_with_syntax_error_case_slash_r_slash_n(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_syntax_error_case_slash_r_slash_n( + self, test_args, args + ): """Should ignore single triple quote followed by return, newline.""" - assert '"""\r\n' == docformatter.format_code('"""\r\n') + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""\r\n' == uut._format_code('"""\r\n') @pytest.mark.unit - def test_format_code_dominant_line_ending_style_preserved(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_dominant_line_ending_style_preserved( + self, test_args, args + ): """Should retain carriage return line endings.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + goes_in = '''\ def foo():\r """\r @@ -572,29 +761,49 @@ def foo():\r \r This is a docstring.\r """\r -''' == docformatter.format_code( +''' == uut._do_format_code( goes_in ) @pytest.mark.unit - def test_format_code_additional_empty_line_before_doc(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_additional_empty_line_before_doc( + self, test_args, args + ): """Should remove empty line between function def and docstring. See issue #51. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( '\n\n\ndef my_func():\n"""Summary of my function."""\npass' - == docformatter._format_code( + == uut._do_format_code( '\n\n\ndef my_func():\n\n"""Summary of my function."""\npass' ) ) @pytest.mark.unit - def test_format_code_extra_newline_following_comment(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_extra_newline_following_comment( + self, test_args, args + ): """Should remove extra newline following in-line comment. See issue #51. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''\ def crash_rocket(location): # pragma: no cover @@ -604,53 +813,80 @@ def crash_rocket(location): # pragma: no cover assert '''\ def crash_rocket(location): # pragma: no cover """This is a docstring following an in-line comment.""" - return location''' == docformatter._format_code( + return location''' == uut._do_format_code( docstring ) @pytest.mark.unit - def test_format_code_no_docstring(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_no_docstring(self, test_args, args): """Should leave code as is if there is no docstring. See issue #97. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = ( "def pytest_addoption(parser: pytest.Parser) -> " "None:\n register_toggle.pytest_addoption(parser)\n" ) - assert docstring == docformatter._format_code(docstring) + assert docstring == uut._do_format_code(docstring) docstring = ( "def pytest_addoption(parser: pytest.Parser) -> " "None: # pragma: no cover\n " "register_toggle.pytest_addoption(parser)\n" ) - assert docstring == docformatter._format_code(docstring) + assert docstring == uut._do_format_code(docstring) + class TestFormatCodeRanges: - """Class for testing format_code() with the line_range or - length_range arguments.""" + """Class for testing _format_code() with the line_range or length_range + arguments.""" @pytest.mark.unit - def test_format_code_range_miss(self): + @pytest.mark.parametrize("args", [["--range", "1", "1", ""]]) + def test_format_code_range_miss(self, test_args, args): """Should leave docstrings outside line range as is.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" - pass''' == docformatter.format_code('''\ + pass''' == uut._format_code( + '''\ def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" - pass''', line_range=[1, 1]) + pass''' + ) @pytest.mark.unit - def test_format_code_range_hit(self): + @pytest.mark.parametrize("args", [["--range", "1", "2", ""]]) + def test_format_code_range_hit(self, test_args, args): """Should format docstrings within line_range.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def f(x): """This is a docstring. @@ -660,17 +896,27 @@ def f(x): pass def g(x): """ Badly indented docstring""" - pass''' == docformatter.format_code('''\ + pass''' == uut._format_code( + '''\ def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" - pass''', line_range=[1, 2]) + pass''' + ) @pytest.mark.unit - def test_format_code_docstring_length(self): + @pytest.mark.parametrize("args", [["--docstring-length", "1", "1", ""]]) + def test_format_code_docstring_length(self, test_args, args): """Should leave docstrings outside length_range as is.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def f(x): """This is a docstring. @@ -681,7 +927,8 @@ def f(x): pass def g(x): """Badly indented docstring.""" - pass''' == docformatter.format_code('''\ + pass''' == uut._format_code( + '''\ def f(x): """This is a docstring. @@ -691,4 +938,73 @@ def f(x): pass def g(x): """ Badly indented docstring""" - pass''', length_range=[1, 1]) + pass''' + ) + + +class TestDoFormatCode: + """Class for testing _do_format_code() with no arguments.""" + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_do_format_code(self, test_args, args): + """Should place one-liner on single line.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '''\ +def foo(): + """Hello foo.""" +''' == uut._do_format_code( + '''\ +def foo(): + """ + Hello foo. + """ +''' + ) + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_do_format_code_with_module_docstring(self, test_args, args): + """Should format module docstrings.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '''\ +#!/usr/env/bin python +"""This is a module docstring. + +1. One +2. Two +""" + +"""But +this +is +not.""" +''' == uut._do_format_code( + '''\ +#!/usr/env/bin python +"""This is +a module +docstring. + +1. One +2. Two +""" + +"""But +this +is +not.""" +''' + ) diff --git a/tests/test_format_docstring.py b/tests/test_format_docstring.py index f5394e0..5bd49e4 100644 --- a/tests/test_format_docstring.py +++ b/tests/test_format_docstring.py @@ -30,25 +30,37 @@ # Standard Library Imports import itertools import random +import sys # Third Party Imports import pytest # docformatter Package Imports import docformatter +from docformatter import Formator # docformatter Local Imports from . import generate_random_docstring +INDENTATION = " " + class TestFormatDocstring: - """Class for testing format_docstring() with no arguments.""" + """Class for testing _do_format_docstring() with no arguments.""" @pytest.mark.unit - def test_format_docstring(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring(self, test_args, args): """Return one-line docstring.""" - assert '"""Hello."""' == docformatter.format_docstring( - " ", + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""Hello."""' == uut._do_format_docstring( + INDENTATION, ''' """ @@ -58,10 +70,20 @@ def test_format_docstring(self): ) @pytest.mark.unit - def test_format_docstring_with_summary_that_ends_in_quote(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_summary_that_ends_in_quote( + self, test_args, args + ): """Return one-line docstring with period after quote.""" - assert '''""""Hello"."""''' == docformatter.format_docstring( - " ", + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '''""""Hello"."""''' == uut._do_format_docstring( + INDENTATION, ''' """ @@ -71,15 +93,23 @@ def test_format_docstring_with_summary_that_ends_in_quote(self): ) @pytest.mark.unit - def test_format_docstring_with_bad_indentation(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "44", ""]]) + def test_format_docstring_with_bad_indentation(self, test_args, args): """Add spaces to indentation when too few.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Hello. - This should be indented but it is not. The - next line should be indented too. And - this too. - """''' == docformatter.format_docstring( - " ", + This should be indented but it is not. + The next line should be indented too. + And this too. + """''' == uut._do_format_docstring( + INDENTATION, ''' """Hello. @@ -91,8 +121,16 @@ def test_format_docstring_with_bad_indentation(self): ) @pytest.mark.unit - def test_format_docstring_with_too_much_indentation(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_too_much_indentation(self, test_args, args): """Remove spaces from indentation when too many.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Hello. This should be dedented. @@ -100,8 +138,8 @@ def test_format_docstring_with_too_much_indentation(self): 1. This too. 2. And this. 3. And this. - """''' == docformatter.format_docstring( - " ", + """''' == uut._do_format_docstring( + INDENTATION, ''' """Hello. @@ -116,14 +154,23 @@ def test_format_docstring_with_too_much_indentation(self): ) @pytest.mark.unit - def test_format_docstring_with_trailing_whitespace(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "52", ""]]) + def test_format_docstring_with_trailing_whitespace(self, test_args, args): """Remove trailing white space.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Hello. This should be not have trailing whitespace. The - next line should not have trailing whitespace either. - """''' == docformatter.format_docstring( - " ", + next line should not have trailing whitespace + either. + """''' == uut._do_format_docstring( + INDENTATION, ''' """Hello.\t \t @@ -135,15 +182,31 @@ def test_format_docstring_with_trailing_whitespace(self): ) @pytest.mark.unit - def test_format_docstring_with_empty_docstring(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_empty_docstring(self, test_args, args): """Do nothing with empty docstring.""" - assert '""""""' == docformatter.format_docstring(" ", '""""""') + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '""""""' == uut._do_format_docstring(INDENTATION, '""""""') @pytest.mark.unit - def test_format_docstring_with_no_period(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_no_period(self, test_args, args): """Add period to end of one-line and summary line.""" - assert '"""Hello."""' == docformatter.format_docstring( - " ", + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""Hello."""' == uut._do_format_docstring( + INDENTATION, ''' """ @@ -153,10 +216,18 @@ def test_format_docstring_with_no_period(self): ) @pytest.mark.unit - def test_format_docstring_with_single_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_single_quotes(self, test_args, args): """Replace single triple quotes with triple double quotes.""" - assert '"""Hello."""' == docformatter.format_docstring( - " ", + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""Hello."""' == uut._do_format_docstring( + INDENTATION, """ ''' @@ -166,15 +237,25 @@ def test_format_docstring_with_single_quotes(self): ) @pytest.mark.unit - def test_format_docstring_with_single_quotes_multi_line(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_single_quotes_multi_line( + self, test_args, args + ): """Replace single triple quotes with triple double quotes.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ''' """Return x factorial. This uses math.factorial. """ -'''.strip() == docformatter.format_docstring( - " ", +'''.strip() == uut._do_format_docstring( + INDENTATION, """ ''' Return x factorial. @@ -185,7 +266,18 @@ def test_format_docstring_with_single_quotes_multi_line(self): ) @pytest.mark.unit - def test_format_docstring_leave_underlined_summaries_alone(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_leave_underlined_summaries_alone( + self, test_args, args + ): + """Leave underlined summary lines as is.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''""" Foo bar ------- @@ -193,51 +285,92 @@ def test_format_docstring_leave_underlined_summaries_alone(self): This is more. """''' - assert docstring == docformatter.format_docstring(" ", docstring) + assert docstring == uut._do_format_docstring(INDENTATION, docstring) class TestFormatLists: """Class for testing format_docstring() with lists in the docstring.""" @pytest.mark.unit - def test_format_docstring_should_ignore_numbered_lists(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_numbered_lists( + self, test_args, args + ): """Ignore lists beginning with numbers.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''"""Hello. 1. This should be indented but it is not. The next line should be indented too. But this is okay. """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_ignore_parameter_lists(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_parameter_lists( + self, test_args, args + ): """Ignore lists beginning with -.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''"""Hello. foo - This is a foo. This is a foo. This is a foo. This is a foo. This is. bar - This is a bar. This is a bar. This is a bar. This is a bar. This is. """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_ignore_colon_parameter_lists(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_colon_parameter_lists( + self, test_args, args + ): """Ignore lists beginning with :""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''"""Hello. foo: This is a foo. This is a foo. This is a foo. This is a foo. This is. bar: This is a bar. This is a bar. This is a bar. This is a bar. This is. """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_leave_list_alone(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_should_leave_list_alone(self, test_args, args): + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''""" one two @@ -251,13 +384,14 @@ def test_format_docstring_should_leave_list_alone(self): ten eleven """''' - assert docstring == docformatter.format_docstring( - " ", docstring, strict=False + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) class TestFormatWrap: - """Class for testing format_docstring() when requesting line wrapping.""" + """Class for testing _do_format_docstring() with line wrapping.""" @pytest.mark.unit def test_unwrap_summary(self): @@ -267,8 +401,20 @@ def test_unwrap_summary(self): ) @pytest.mark.unit - def test_format_docstring_with_wrap(self): - """Wrap docstring.""" + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_wrap( + self, + test_args, + args, + ): + """Wrap the docstring.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + # This function uses `random` so make sure each run of this test is # repeatable. random.seed(0) @@ -278,12 +424,12 @@ def test_format_docstring_with_wrap(self): range(min_line_length, 100), range(20) ): indentation = " " * num_indents - formatted_text = indentation + docformatter.format_docstring( + uut.args.wrap_summaries = max_length + formatted_text = indentation + uut._do_format_docstring( indentation=indentation, docstring=generate_random_docstring( max_word_length=min_line_length // 2 ), - summary_wrap_length=max_length, ) for line in formatted_text.split("\n"): # It is not the formatter's fault if a word is too long to @@ -292,18 +438,29 @@ def test_format_docstring_with_wrap(self): assert len(line) <= max_length @pytest.mark.unit - def test_format_docstring_with_weird_indentation_and_punctuation(self): - """""" + @pytest.mark.parametrize("args", [["--wrap-summaries", "79", ""]]) + def test_format_docstring_with_weird_indentation_and_punctuation( + self, + test_args, + args, + ): + """Wrap and dedent docstring.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ''' """Creates and returns four was awakens to was created tracked ammonites was the fifty, arithmetical four was pyrotechnic to pyrotechnic physicists. - `four' falsified x falsified ammonites - to awakens to. `created' to ancestor was four to x dynamo to was - four ancestor to physicists(). + `four' falsified x falsified ammonites to awakens to. `created' to + ancestor was four to x dynamo to was four ancestor to physicists(). """ - '''.strip() == docformatter.format_docstring( - " ", + '''.strip() == uut._do_format_docstring( + INDENTATION, ''' """Creates and returns four was awakens to was created tracked ammonites was the fifty, arithmetical four was pyrotechnic to @@ -312,18 +469,29 @@ def test_format_docstring_with_weird_indentation_and_punctuation(self): four ancestor to physicists(). """ '''.strip(), - summary_wrap_length=79, ) @pytest.mark.unit - def test_format_docstring_with_description_wrapping(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_with_description_wrapping( + self, + test_args, + args, + ): """Wrap description at 72 characters.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Hello. This should be indented but it is not. The next line should be indented too. But this is okay. - """''' == docformatter.format_docstring( - " ", + """''' == uut._do_format_docstring( + INDENTATION, ''' """Hello. @@ -333,12 +501,23 @@ def test_format_docstring_with_description_wrapping(self): """ '''.strip(), - description_wrap_length=72, ) @pytest.mark.unit - def test_format_docstring_should_ignore_multi_paragraph(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_multi_paragraph( + self, + test_args, + args, + ): """Ignore multiple paragraphs in elaborate description.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''"""Hello. This should be indented but it is not. The @@ -349,44 +528,83 @@ def test_format_docstring_should_ignore_multi_paragraph(self): next line should be indented too. But this is okay. """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_ignore_doctests(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_doctests( + self, + test_args, + args, + ): """Leave doctests alone.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''"""Hello. >>> 4 4 """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_ignore_doctests_in_summary(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_doctests_in_summary( + self, + test_args, + args, + ): """Leave doctests alone if they're in the summary.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''""" >>> 4 4 """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_maintain_indentation_of_doctest(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_maintain_indentation_of_doctest( + self, + test_args, + args, + ): """Don't change indentation of doctest lines.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Foo bar bing bang. >>> tests = DocTestFinder().find(_TestClass) >>> runner = DocTestRunner(verbose=False) >>> tests.sort(key = lambda test: test.name) - """''' == docformatter.format_docstring( - " ", + """''' == uut._do_format_docstring( + INDENTATION, docstring='''"""Foo bar bing bang. >>> tests = DocTestFinder().find(_TestClass) @@ -394,12 +612,35 @@ def test_format_docstring_should_maintain_indentation_of_doctest(self): >>> tests.sort(key = lambda test: test.name) """''', - description_wrap_length=72, ) @pytest.mark.unit - def test_force_wrap(self): + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-descriptions", + "72", + "--wrap-summaries", + "50", + "--force-wrap", + "", + ] + ], + ) + def test_force_wrap( + self, + test_args, + args, + ): """Force even lists to be wrapped.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( ( '''\ @@ -408,42 +649,80 @@ def test_force_wrap(self): convergence."""\ ''' ) - == docformatter.format_docstring( - " ", + == uut._do_format_docstring( + INDENTATION, '''\ """ num_iterations is the number of updates - instead of a better definition of convergence. """\ ''', - description_wrap_length=50, - summary_wrap_length=50, - force_wrap=True, ) ) @pytest.mark.unit + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-summaries", + "30", + "--tab-width", + "4", + "", + ] + ], + ) def test_format_docstring_with_summary_only_and_wrap_and_tab_indentation( self, + test_args, + args, ): """"Should account for length of tab when wrapping. See PR #69. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ''' \t\t"""Some summary x x x \t\tx.""" -'''.strip() == docformatter.format_docstring( +'''.strip() == uut._do_format_docstring( "\t\t", ''' \t\t"""Some summary x x x x.""" '''.strip(), - summary_wrap_length=30, - tab_width=4, ) @pytest.mark.unit - def test_format_docstring_for_multi_line_summary_alone(self): + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-summaries", + "69", + "--close-quotes-on-newline", + "", + ] + ], + ) + def test_format_docstring_for_multi_line_summary_alone( + self, + test_args, + args, + ): """Place closing quotes on newline when wrapping one-liner.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( ( '''\ @@ -452,33 +731,51 @@ def test_format_docstring_for_multi_line_summary_alone(self): """\ ''' ) - == docformatter.format_docstring( - " ", + == uut._do_format_docstring( + INDENTATION, '''\ """This one-line docstring will be multi-line because it's quite long."""\ ''', - summary_wrap_length=69, - close_quotes_on_newline=True, ) ) @pytest.mark.unit - def test_format_docstring_for_one_line_summary_alone_but_too_long(self): + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-summaries", + "88", + "--close-quotes-on-newline", + "", + ] + ], + ) + def test_format_docstring_for_one_line_summary_alone_but_too_long( + self, + test_args, + args, + ): """""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( ( '''\ """This one-line docstring will not be wrapped and quotes will be in-line."""\ ''' ) - == docformatter.format_docstring( - " ", + == uut._do_format_docstring( + INDENTATION, '''\ """This one-line docstring will not be wrapped and quotes will be in-line."""\ ''', - summary_wrap_length=88, - close_quotes_on_newline=True, ) ) @@ -487,13 +784,25 @@ class TestFormatStyleOptions: """Class for testing format_docstring() when requesting style options.""" @pytest.mark.unit - def test_format_docstring_with_no_post_description_blank(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_no_post_description_blank( + self, + test_args, + args, + ): """Remove blank lines before closing triple quotes.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Hello. Description. - """''' == docformatter.format_docstring( - " ", + """''' == uut._do_format_docstring( + INDENTATION, ''' """ @@ -504,18 +813,29 @@ def test_format_docstring_with_no_post_description_blank(self): """ '''.strip(), - post_description_blank=False, ) @pytest.mark.unit - def test_format_docstring_with_pre_summary_newline(self): + @pytest.mark.parametrize("args", [["--pre-summary-newline", ""]]) + def test_format_docstring_with_pre_summary_newline( + self, + test_args, + args, + ): """Remove blank line before summary.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''""" Hello. Description. - """''' == docformatter.format_docstring( - " ", + """''' == uut._do_format_docstring( + INDENTATION, ''' """ @@ -526,11 +846,23 @@ def test_format_docstring_with_pre_summary_newline(self): """ '''.strip(), - pre_summary_newline=True, ) @pytest.mark.unit - def test_format_docstring_make_summary_multi_line(self): + @pytest.mark.parametrize("args", [["--make-summary-multi-line", ""]]) + def test_format_docstring_make_summary_multi_line( + self, + test_args, + args, + ): + """Place the one-line docstring between triple quotes.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( ( '''\ @@ -539,24 +871,34 @@ def test_format_docstring_make_summary_multi_line(self): """\ ''' ) - == docformatter.format_docstring( - " ", + == uut._do_format_docstring( + INDENTATION, '''\ """This one-line docstring will be multi-line"""\ ''', - make_summary_multi_line=True, ) ) @pytest.mark.unit - def test_format_docstring_pre_summary_space(self): - """""" + @pytest.mark.parametrize("args", [["--pre-summary-space", ""]]) + def test_format_docstring_pre_summary_space( + self, + test_args, + args, + ): + """Place a space between the opening quotes and the summary.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( '''""" This one-line docstring will have a leading space."""''' - ) == docformatter.format_docstring( - " ", + ) == uut._do_format_docstring( + INDENTATION, '''\ """This one-line docstring will have a leading space."""\ ''', - pre_summary_space=True, )