From 1def3c413021cdc0d355eb988444fbc847181173 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Wed, 24 Aug 2022 10:59:30 -0400 Subject: [PATCH] chore: move script to src layout and make a package (#117) * chore: move functions and classes out of main.py * chore: move configuration stuff to separate module * chore: update configuration tests * chore: move encoder stuff to separate module * chore: move formatter stuff to separate module * chore: move string functions to separate module * chore: move utility functions to separate module * chore: move syntax functions to separate module * chore: make docformatter a package * test: update test suite for package * refactor: fix imports to prevent cyclic imports * chore: update dependencies * chore: rename main.py to __main__.py * ci: add workflow to lint code base * refactor: rename classes and modules * chore: remove Makefile; not really needed any longer --- .github/workflows/ci.yml | 1 + .github/workflows/do-lint.yml | 25 + MANIFEST.in | 10 - Makefile | 39 - docformatter.py | 1318 ------------------------- pyproject.toml | 45 +- setup.py | 51 - src/docformatter/__init__.py | 37 + src/docformatter/__main__.py | 82 ++ src/docformatter/__pkginfo__.py | 26 + src/docformatter/configuration.py | 299 ++++++ src/docformatter/encode.py | 113 +++ src/docformatter/format.py | 479 +++++++++ src/docformatter/strings.py | 189 ++++ src/docformatter/syntax.py | 274 +++++ src/docformatter/util.py | 141 +++ tests/conftest.py | 35 +- tests/test_configuration_functions.py | 52 +- tests/test_docformatter.py | 16 +- tests/test_encoding_functions.py | 26 +- tests/test_format_code.py | 74 +- tests/test_format_docstring.py | 86 +- tests/test_string_functions.py | 8 +- 23 files changed, 1839 insertions(+), 1587 deletions(-) create mode 100644 .github/workflows/do-lint.yml delete mode 100644 MANIFEST.in delete mode 100644 Makefile delete mode 100755 docformatter.py delete mode 100755 setup.py create mode 100644 src/docformatter/__init__.py create mode 100755 src/docformatter/__main__.py create mode 100644 src/docformatter/__pkginfo__.py create mode 100644 src/docformatter/configuration.py create mode 100644 src/docformatter/encode.py create mode 100644 src/docformatter/format.py create mode 100644 src/docformatter/strings.py create mode 100644 src/docformatter/syntax.py create mode 100644 src/docformatter/util.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be1a27f..3de23cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: pull_request: branches: - master + jobs: test: strategy: diff --git a/.github/workflows/do-lint.yml b/.github/workflows/do-lint.yml new file mode 100644 index 0000000..3e2fc9f --- /dev/null +++ b/.github/workflows/do-lint.yml @@ -0,0 +1,25 @@ +name: Lint +on: + push: + branches: + - "*" + pull_request: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + name: "Run linters on code base" + steps: + - name: Setup Python for linting + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install tox + run: python -m pip install tox tox-gh-actions + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Lint code base + run: tox -e style diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index df5d4f7..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,10 +0,0 @@ -include AUTHORS.rst -include LICENSE -include MANIFEST.in -include README.rst -include test_docformatter.py -recursive-include images *.png - -exclude .pre-commit-hooks.yaml -exclude .travis.yml -exclude Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 7475ea4..0000000 --- a/Makefile +++ /dev/null @@ -1,39 +0,0 @@ - -check: - pycodestyle docformatter.py setup.py - pydocstyle docformatter.py setup.py - pylint docformatter.py setup.py - check-manifest - rstcheck --report=1 README.rst - docformatter docformatter.py setup.py - python -m doctest docformatter.py - -coverage.unit: - @echo -e "\n\t\033[1;32mRunning docformatter unit tests with coverage ...\033[0m\n" - COVERAGE_FILE=".coverage.unit" py.test $(TESTOPTS) -m unit \ - --cov-config=pyproject.toml --cov=docformatter --cov-branch \ - --cov-report=term $(TESTFILE) - -coverage.system: - @echo -e "\n\t\033[1;32mRunning docformatter system tests with coverage ...\033[0m\n" - COVERAGE_FILE=".coverage.system" py.test $(TESTOPTS) -m system \ - --cov-config=pyproject.toml --cov=docformatter --cov-branch \ - --cov-report=term $(TESTFILE) - -coverage: - @echo -e "\n\t\033[1;32mRunning full docformatter test suite with coverage ...\033[0m\n" - @coverage erase - $(MAKE) coverage.unit - $(MAKE) coverage.system - @coverage combine .coverage.unit .coverage.system - @coverage xml --rcfile=pyproject.toml - -open_coverage: coverage - @coverage html - @python -m webbrowser -n "file://${PWD}/htmlcov/index.html" - -mutant: - @mut.py -t docformatter -u test_docformatter -mc - -readme: - @restview --long-description --strict diff --git a/docformatter.py b/docformatter.py deleted file mode 100755 index c7c93ff..0000000 --- a/docformatter.py +++ /dev/null @@ -1,1318 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (C) 2012-2019 Steven Myint -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# 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. - -"""Formats docstrings to follow PEP 257.""" - - -from __future__ import ( - absolute_import, - division, - print_function, - unicode_literals, -) - -# Standard Library Imports -import argparse -import collections -import contextlib -import io -import locale -import os -import re -import signal -import sys -import sysconfig -import textwrap -import tokenize -from configparser import ConfigParser -from typing import Dict, List, TextIO, Tuple, Union - -# Third Party Imports -import untokenize # type: ignore -from charset_normalizer import from_path # pylint: disable=import-error - -try: - # Third Party Imports - import tomli - - TOMLI_INSTALLED = True -except ImportError: - TOMLI_INSTALLED = False - -__version__ = "1.5.0" - -unicode = str - - -HEURISTIC_MIN_LIST_ASPECT_RATIO = 0.4 - -CR = "\r" -LF = "\n" -CRLF = "\r\n" - -_PYTHON_LIBS = set(sysconfig.get_paths().values()) - - -class FormatResult: - """Possible exit codes.""" - - ok = 0 - error = 1 - interrupted = 2 - check_failed = 3 - - -class Configurator: - """Read and store all the docformatter configuration information.""" - - parser = None - """Parser object.""" - - flargs_dct: Dict[str, Union[bool, float, int, str]] = {} - """Dictionary of configuration file arguments.""" - - configuration_file_lst = [ - "pyproject.toml", - "setup.cfg", - "tox.ini", - ] - """List of supported configuration files.""" - - args: argparse.Namespace = None - - def __init__(self, args: List[Union[bool, int, str]]) -> None: - """Initialize a Configurator class instance. - - Parameters - ---------- - args : list - Any command line arguments passed during invocation. - """ - self.args_lst = args - self.config_file = "" - self.parser = argparse.ArgumentParser( - description=__doc__, - prog="docformatter", - ) - - try: - self.config_file = self.args_lst[ - self.args_lst.index("--config") + 1 - ] - except ValueError: - for _configuration_file in self.configuration_file_lst: - if os.path.isfile(_configuration_file): - self.config_file = f"./{_configuration_file}" - break - - if os.path.isfile(self.config_file): - self._do_read_configuration_file() - - def do_parse_arguments(self) -> None: - """Parse configuration file and command line arguments.""" - changes = self.parser.add_mutually_exclusive_group() - changes.add_argument( - "-i", - "--in-place", - action="store_true", - help="make changes to files instead of printing diffs", - ) - changes.add_argument( - "-c", - "--check", - action="store_true", - help="only check and report incorrectly formatted files", - ) - self.parser.add_argument( - "-r", - "--recursive", - action="store_true", - default=bool(self.flargs_dct.get("recursive", False)), - help="drill down directories recursively", - ) - self.parser.add_argument( - "-e", - "--exclude", - nargs="*", - help="exclude directories and files by names", - ) - self.parser.add_argument( - "--wrap-summaries", - default=int(self.flargs_dct.get("wrap-summaries", 79)), - type=int, - metavar="length", - help="wrap long summary lines at this length; " - "set to 0 to disable wrapping (default: 79)", - ) - self.parser.add_argument( - "--wrap-descriptions", - default=int(self.flargs_dct.get("wrap-descriptions", 72)), - type=int, - metavar="length", - help="wrap descriptions at this length; " - "set to 0 to disable wrapping (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: False)", - ) - self.parser.add_argument( - "--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: 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: 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: 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: False)", - ) - self.parser.add_argument( - "--make-summary-multi-line", - action="store_true", - default=bool( - self.flargs_dct.get("make-summary-multi-line", False) - ), - help="add a newline before and after the summary of a one-line " - "docstring (default: False)", - ) - self.parser.add_argument( - "--close-quotes-on-newline", - action="store_true", - default=bool( - self.flargs_dct.get("close-quotes-on-newline", False) - ), - help="place closing triple quotes on a new-line when a " - "one-line docstring wraps to two or more lines " - "(default: False)", - ) - self.parser.add_argument( - "--range", - metavar="line", - dest="line_range", - default=self.flargs_dct.get("range", None), - type=int, - nargs=2, - help="apply docformatter to docstrings between these " - "lines; line numbers are indexed at 1 (default: None)", - ) - self.parser.add_argument( - "--docstring-length", - metavar="length", - dest="length_range", - default=self.flargs_dct.get("docstring-length", None), - type=int, - nargs=2, - help="apply docformatter to docstrings of given length range " - "(default: None)", - ) - self.parser.add_argument( - "--non-strict", - action="store_true", - default=bool(self.flargs_dct.get("non-strict", False)), - help="don't strictly follow reST syntax to identify lists (see " - "issue #67) (default: False)", - ) - self.parser.add_argument( - "--config", - default=self.config_file, - help="path to file containing docformatter options", - ) - self.parser.add_argument( - "--version", - action="version", - version=f"%(prog)s {__version__}", - ) - self.parser.add_argument( - "files", - nargs="+", - help="files to format or '-' for standard in", - ) - - self.args = self.parser.parse_args(self.args_lst[1:]) - - if self.args.line_range: - if self.args.line_range[0] <= 0: - self.parser.error("--range must be positive numbers") - if self.args.line_range[0] > self.args.line_range[1]: - self.parser.error( - "First value of --range should be less than or equal " - "to the second" - ) - - if self.args.length_range: - if self.args.length_range[0] <= 0: - self.parser.error( - "--docstring-length must be positive numbers" - ) - if self.args.length_range[0] > self.args.length_range[1]: - self.parser.error( - "First value of --docstring-length should be less " - "than or equal to the second" - ) - - def _do_read_configuration_file(self) -> None: - """Read docformatter options from a configuration file.""" - argfile = os.path.basename(self.config_file) - for f in self.configuration_file_lst: - if argfile == f: - break - - fullpath, ext = os.path.splitext(self.config_file) - filename = os.path.basename(fullpath) - - if ext == ".toml" and TOMLI_INSTALLED and filename == "pyproject": - self._do_read_toml_configuration() - - if (ext == ".cfg" and filename == "setup") or ( - ext == ".ini" and filename == "tox" - ): - self._do_read_parser_configuration() - - def _do_read_toml_configuration(self) -> None: - """Load configuration information from a *.toml file.""" - with open(self.config_file, "rb") as f: - config = tomli.load(f) - - result = config.get("tool", {}).get("docformatter", None) - if result is not None: - self.flargs_dct = { - k: v if isinstance(v, list) else str(v) - for k, v in result.items() - } - - def _do_read_parser_configuration(self) -> None: - """Load configuration information from a *.cfg or *.ini file.""" - config = ConfigParser() - config.read(self.config_file) - - for _section in [ - "tool.docformatter", - "tool:docformatter", - "docformatter", - ]: - if _section in config.sections(): - self.flargs_dct = { - k: v if isinstance(v, list) else str(v) - for k, v in config[_section].items() - } - - -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.""" - - 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 - - self.encodor = Encodor() - - 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 self.encodor.system_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 - ------ - result_code: int - One of the FormatResult codes. - """ - self.encodor.do_detect_encoding(filename) - - with self.encodor.do_open_with_encoding(filename) 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 self.encodor.do_open_with_encoding( - filename, - mode="w", - ) 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 = self.encodor.do_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_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(self.QUOTE_TYPES) - and ( - previous_token_type == tokenize.INDENT - or previous_token_type == tokenize.NEWLINE - 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 = " " * (len(line) - len(line.lstrip())) - 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_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 = 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(self.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 _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 - - -def has_correct_length(length_range, start, end): - """Return True if docstring's length is in range.""" - if length_range is None: - return True - min_length, max_length = length_range - - docstring_length = end + 1 - start - return min_length <= docstring_length <= max_length - - -def is_in_range(line_range, start, end): - """Return True if start/end is in line_range.""" - if line_range is None: - return True - return any( - line_range[0] <= line_no <= line_range[1] - for line_no in range(start, end + 1) - ) - - -def is_probably_beginning_of_sentence(line): - """Return True if this line begins a new sentence.""" - # Check heuristically for a parameter list. - for token in ["@", "-", r"\*"]: - if re.search(r"\s" + token + r"\s", line): - return True - - stripped_line = line.strip() - is_beginning_of_sentence = re.match(r'[^\w"\'`\(\)]', stripped_line) - is_pydoc_ref = re.match(r"^:\w+:", stripped_line) - - return is_beginning_of_sentence and not is_pydoc_ref - - -def is_some_sort_of_code(text: str) -> bool: - """Return True if text looks like code.""" - return any( - len(word) > 50 - and not re.match(r"<{0,1}(http:|https:|ftp:|sftp:)", word) - for word in text.split() - ) - - -def do_preserve_links( - text: str, - indentation: str, - wrap_length: int, -) -> List[str]: - """Rebuild links in docstring. - - Parameters - ---------- - text : str - The docstring description. - indentation : str - The indentation (number of spaces or tabs) to place in front of each - line. - wrap_length : int - The column to wrap each line at. - - Returns - ------- - lines : list - A list containing each line of the description with any links put - back together. - """ - lines = textwrap.wrap( - textwrap.dedent(text), - width=wrap_length, - initial_indent=indentation, - subsequent_indent=indentation, - ) - - url = next( - ( - line - for line in lines - if re.search(r")? We want to keep - # the '<' and '>' part of the link. - if re.search(r"<", url): - lines[url_idx] = f"{indentation}" + url.split(sep="<")[0].strip() - url = f"{indentation}<" + url.split(sep="<")[1] - url = url + lines[url_idx + 1].strip() - lines[url_idx + 1] = url - # Is this a link target definition (i.e., .. a link: https://)? We - # want to keep the .. a link: on the same line as the url. - elif re.search(r"(\.\. )", url): - url = url + lines[url_idx + 1].strip() - lines[url_idx] = url - lines.pop(url_idx + 1) - # Is this a simple link (i.e., just a link in the text) that should - # be unwrapped? We want to break the url out from the rest of the - # text. - elif len(lines[url_idx]) >= wrap_length: - lines[url_idx] = ( - f"{indentation}" + url.strip().split(sep=" ")[0].strip() - ) - url = f"{indentation}" + url.strip().split(sep=" ")[1].strip() - url = url + lines[url_idx + 1].strip().split(sep=" ")[0].strip() - lines.append( - indentation - + " ".join(lines[url_idx + 1].strip().split(sep=" ")[1:]) - ) - lines[url_idx + 1] = url - - with contextlib.suppress(IndexError): - if lines[url_idx + 2].strip() in [".", "?", "!", ";"] or re.search( - r">", lines[url_idx + 2] - ): - url = url + lines[url_idx + 2].strip() - lines[url_idx + 1] = url - lines.pop(url_idx + 2) - - return lines - - -def is_some_sort_of_list(text, strict) -> bool: - """Return True if text looks like a list.""" - split_lines = text.rstrip().splitlines() - - # TODO: Find a better way of doing this. - # Very large number of lines but short columns probably means a list of - # items. - if ( - len(split_lines) - / max([len(line.strip()) for line in split_lines] + [1]) - > HEURISTIC_MIN_LIST_ASPECT_RATIO - ) and not strict: - return True - - return any( - ( - re.match(r"\s*$", line) - or - # "1. item" - re.match(r"\s*\d\.", line) - or - # "@parameter" - re.match(r"\s*[\-*:=@]", line) - or - # "parameter - description" - re.match(r".*\s+[\-*:=@]\s+", line) - or - # "parameter: description" - re.match(r"\s*\S+[\-*:=@]\s+", line) - or - # "parameter:\n description" - re.match(r"\s*\S+:\s*$", line) - or - # "parameter -- description" - re.match(r"\s*\S+\s+--\s+", line) - ) - for line in split_lines - ) - - -def reindent(text, indentation): - """Return reindented text that matches indentation.""" - if "\t" not in indentation: - text = text.expandtabs() - - text = textwrap.dedent(text) - - return ( - "\n".join( - [(indentation + line).rstrip() for line in text.splitlines()] - ).rstrip() - + "\n" - ) - - -def _find_shortest_indentation(lines): - """Return shortest indentation.""" - assert not isinstance(lines, str) - - indentation = None - - for line in lines: - if line.strip(): - non_whitespace_index = len(line) - len(line.lstrip()) - _indent = line[:non_whitespace_index] - if indentation is None or len(_indent) < len(indentation): - indentation = _indent - - return indentation or "" - - -def split_summary_and_description(contents): - """Split docstring into summary and description. - - Return tuple (summary, description). - """ - split_lines = contents.rstrip().splitlines() - - for index in range(1, len(split_lines)): - found = False - - # Empty line separation would indicate the rest is the description or, - # symbol on second line probably is a description with a list. - if not split_lines[index].strip() or is_probably_beginning_of_sentence( - split_lines[index] - ): - found = True - - if found: - return ( - "\n".join(split_lines[:index]).strip(), - "\n".join(split_lines[index:]).rstrip(), - ) - - # Break on first sentence. - split = split_first_sentence(contents) - if split[0].strip() and split[1].strip(): - return ( - split[0].strip(), - _find_shortest_indentation(split[1].splitlines()[1:]) - + split[1].strip(), - ) - - return contents, "" - - -def split_first_sentence(text): - """Split text into first sentence and the rest. - - Return a tuple (sentence, rest). - """ - sentence = "" - rest = text - delimiter = "" - previous_delimiter = "" - - while rest: - split = re.split(r"(\s)", rest, maxsplit=1) - word = split[0] - if len(split) == 3: - delimiter = split[1] - rest = split[2] - else: - assert len(split) == 1 - delimiter = "" - rest = "" - - sentence += previous_delimiter + word - - if sentence.endswith(("e.g.", "i.e.", "Dr.", "Mr.", "Mrs.", "Ms.")): - # Ignore false end of sentence. - pass - elif sentence.endswith((".", "?", "!")): - break - elif sentence.endswith(":") and delimiter == "\n": - # Break on colon if it ends the line. This is a heuristic to detect - # the beginning of some parameter list afterwards. - break - - previous_delimiter = delimiter - delimiter = "" - - return sentence, delimiter + rest - - -def normalize_line(line, newline): - """Return line with fixed ending, if ending was present in line. - - Otherwise, does nothing. - """ - stripped = line.rstrip("\n\r") - return stripped + newline if stripped != line else line - - -def normalize_line_endings(lines, newline): - """Return fixed line endings. - - All lines will be modified to use the most common line ending. - """ - return "".join([normalize_line(line, newline) for line in lines]) - - -def unwrap_summary(summary): - """Return summary with newlines removed in preparation for wrapping.""" - return re.sub(r"\s*\n\s*", " ", summary) - - -def normalize_summary(summary): - """Return normalized docstring summary.""" - # remove trailing whitespace - summary = summary.rstrip() - - # Add period at end of sentence and capitalize the first word of the - # summary. - if ( - summary - and (summary[-1].isalnum() or summary[-1] in ['"', "'"]) - and (not summary.startswith("#")) - ): - summary += "." - summary = summary[0].upper() + summary[1:] - - return summary - - -def wrap_summary(summary, initial_indent, subsequent_indent, wrap_length): - """Return line-wrapped summary text.""" - if wrap_length > 0: - return textwrap.fill( - unwrap_summary(summary), - width=wrap_length, - initial_indent=initial_indent, - subsequent_indent=subsequent_indent, - ).strip() - else: - return summary - - -def wrap_description(text, indentation, wrap_length, force_wrap, strict): - """Return line-wrapped description text. - - We only wrap simple descriptions. We leave doctests, multi-paragraph - text, and bulleted lists alone. - """ - text = strip_leading_blank_lines(text) - - # Do not modify doctests at all. - if ">>>" in text: - return text - - text = reindent(text, indentation).rstrip() - - # Ignore possibly complicated cases. - if wrap_length <= 0 or ( - not force_wrap - and (is_some_sort_of_code(text) or is_some_sort_of_list(text, strict)) - ): - return text - - text = do_preserve_links(text, indentation, wrap_length) - - return indentation + "\n".join(text).strip() - - -def remove_section_header(text): - r"""Return text with section header removed. - - >>> remove_section_header('----\nfoo\nbar\n') - 'foo\nbar\n' - - >>> remove_section_header('===\nfoo\nbar\n') - 'foo\nbar\n' - """ - stripped = text.lstrip() - if not stripped: - return text - - first = stripped[0] - return ( - text - if ( - first.isalnum() - or first.isspace() - or stripped.splitlines()[0].strip(first).strip() - ) - else stripped.lstrip(first).lstrip() - ) - - -def strip_leading_blank_lines(text): - """Return text with leading blank lines removed.""" - split = text.splitlines() - - found = next( - (index for index, line in enumerate(split) if line.strip()), 0 - ) - - return "\n".join(split[found:]) - - -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: - formator.do_format_standard_in( - configurator.parser, - ) - else: - return formator.do_format_files() - - -def find_py_files(sources, recursive, exclude=None): - """Find Python source files. - - Parameters - - sources: iterable with paths as strings. - - recursive: drill down directories if True. - - exclude: string based on which directories and files are excluded. - - Return: yields paths to found files. - """ - - def not_hidden(name): - """Return True if file 'name' isn't .hidden.""" - return not name.startswith(".") - - def is_excluded(name, exclude): - """Return True if file 'name' is excluded.""" - return ( - any( - re.search(re.escape(str(e)), name, re.IGNORECASE) - for e in exclude - ) - if exclude - else False - ) - - for name in sorted(sources): - if recursive and os.path.isdir(name): - for root, dirs, children in os.walk(unicode(name)): - dirs[:] = [ - d - for d in dirs - if not_hidden(d) and not is_excluded(d, _PYTHON_LIBS) - ] - dirs[:] = sorted( - [d for d in dirs if not is_excluded(d, exclude)] - ) - files = sorted( - [ - f - for f in children - if not_hidden(f) and not is_excluded(f, exclude) - ] - ) - for filename in files: - if filename.endswith(".py") and not is_excluded( - root, exclude - ): - yield os.path.join(root, filename) - else: - yield name - - -def main(): - """Run main entry point.""" - # SIGPIPE is not available on Windows. - with contextlib.suppress(AttributeError): - # Exit on broken pipe. - signal.signal(signal.SIGPIPE, signal.SIG_DFL) - try: - return _main( - sys.argv, - standard_out=sys.stdout, - standard_error=sys.stderr, - standard_in=sys.stdin, - ) - except KeyboardInterrupt: # pragma: no cover - return FormatResult.interrupted # pragma: no cover - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index 86ce657..52f2641 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers=[ 'Programming Language :: Python :: Implementation :: CPython', 'License :: OSI Approved :: MIT License', ] +packages = [{include = "docformatter", from = "src"}] include = ["LICENSE"] [tool.poetry.dependencies] @@ -44,16 +45,31 @@ autopep8 = "^1.7.0" black = [ {version = "^22.0.0", python = ">=3.6.2"}, ] -coverage = {extras = ["toml"], version = "^6.2.0"} -isort = "^5.7.0" +coverage = [ + {extras = ["toml"], version = "^6.2.0", python = "<3.7"}, + {extras = ["toml"], version = "^6.4.0", python = ">=3.7"}, +] +isort = [ + {version = "<5.10.0", python = "<3.6.1"}, + {version = "^5.10.0", python = ">=3.6.1"}, +] mock = "^4.0.0" mypy = "0.971" pycodestyle = "^2.8.0" pydocstyle = "^6.1.1" -pylint = "^2.12.0" -pytest = "<7.0.0" +pylint = [ + {version = "^2.12.0", python = "<3.7.2"}, + {version = "^2.14.0", python = ">=3.7.2"}, +] +pytest = [ + {version = "<7.1.0", python = "<3.7"}, + {version = "^7.1.0", python = ">=3.7"}, +] pytest-cov = "^3.0.0" -rstcheck = "<6.0.0" +rstcheck = [ + {version = "<6.0.0", python = "<3.7"}, + {version = "^6.1.0", python = ">=3.7"}, +] tox = "^3.25.0" Sphinx = "^5.0.0" twine = [ @@ -65,7 +81,7 @@ twine = [ tomli = ["tomli"] [tool.poetry.scripts] -docformatter = "docformatter:main" +docformatter = "docformatter.__main__:main" [build-system] requires = ["poetry-core>=1.0.0"] @@ -206,12 +222,14 @@ deps = setenv = COVERAGE_FILE = {toxworkdir}/.coverage.{envname} commands = - pytest -s -x -c ./pyproject.toml \ + pip install -U pip + pip install --prefix={toxworkdir}/{envname} -e .[tomli] + pytest -s -x -c {toxinidir}/pyproject.toml \ --cache-clear \ --cov=docformatter \ --cov-config={toxinidir}/pyproject.toml \ --cov-branch \ - tests/ + {toxinidir}/tests/ [testenv:coverage] description = combine coverage data and create report @@ -239,10 +257,9 @@ deps = toml untokenize commands = - docformatter docformatter.py setup.py - pycodestyle docformatter.py setup.py - pydocstyle docformatter.py setup.py - pylint docformatter.py setup.py - rstcheck --report-level=1 README.rst - python -m doctest docformatter.py + docformatter --recursive {toxinidir}/src/docformatter + pycodestyle {toxinidir}/src/docformatter + pydocstyle {toxinidir}/src/docformatter + pylint --rcfile={toxinidir}/pyproject.toml {toxinidir}/src/docformatter + rstcheck --report-level=1 {toxinidir}/README.rst """ diff --git a/setup.py b/setup.py deleted file mode 100755 index 854aff2..0000000 --- a/setup.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python - -"""Setup for docformatter.""" - -from __future__ import (absolute_import, - division, - print_function, - unicode_literals) - -import ast -from pathlib import Path - -from setuptools import setup - - -def version(): - """Return version string.""" - with open('docformatter.py', encoding="ascii") as input_file: - for line in input_file: - if line.startswith('__version__'): - return ast.parse(line).body[0].value.s - - -setup(name='docformatter', - version=version(), - description='Formats docstrings to follow PEP 257.', - long_description=Path('README.rst').read_text(encoding="ascii"), - license='Expat License', - author='Steven Myint', - url='https://github.com/myint/docformatter', - classifiers=[ - 'Intended Audience :: Developers', - 'Environment :: Console', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: Implementation', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Programming Language :: Python :: Implementation :: CPython', - 'License :: OSI Approved :: MIT License', - ], - keywords='PEP 257, pep257, style, formatter, docstrings', - py_modules=['docformatter'], - entry_points={ - 'console_scripts': ['docformatter = docformatter:main']}, - install_requires=['untokenize'], - extras_require={"tomli": ["tomli"]}, - test_suite='test_docformatter') diff --git a/src/docformatter/__init__.py b/src/docformatter/__init__.py new file mode 100644 index 0000000..09f3441 --- /dev/null +++ b/src/docformatter/__init__.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2022 Steven Myint +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# 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. +"""This is the docformatter package.""" + +__all__ = ["__version__"] + +# docformatter Local Imports +from .__pkginfo__ import __version__ +from .strings import * +from .syntax import * +from .util import * + +# Have isort skip these they require the functions above. +from .configuration import Configurater # isort: skip +from .encode import Encoder # isort: skip +from .format import Formatter, FormatResult # isort: skip diff --git a/src/docformatter/__main__.py b/src/docformatter/__main__.py new file mode 100755 index 0000000..bf7a2e1 --- /dev/null +++ b/src/docformatter/__main__.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2022 Steven Myint +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# 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. +"""Formats docstrings to follow PEP 257.""" + + +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +# Standard Library Imports +import contextlib +import signal +import sys + +# docformatter Package Imports +import docformatter.configuration as _configuration +import docformatter.format as _format + + +def _main(argv, standard_out, standard_error, standard_in): + """Run internal main entry point.""" + configurator = _configuration.Configurater(argv) + configurator.do_parse_arguments() + + formator = _format.Formatter( + configurator.args, + stderror=standard_error, + stdin=standard_in, + stdout=standard_out, + ) + + if "-" in configurator.args.files: + formator.do_format_standard_in( + configurator.parser, + ) + else: + return formator.do_format_files() + + +def main(): + """Run main entry point.""" + # SIGPIPE is not available on Windows. + with contextlib.suppress(AttributeError): + # Exit on broken pipe. + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + try: + return _main( + sys.argv, + standard_out=sys.stdout, + standard_error=sys.stderr, + standard_in=sys.stdin, + ) + except KeyboardInterrupt: # pragma: no cover + return _format.FormatResult.interrupted # pragma: no cover + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/docformatter/__pkginfo__.py b/src/docformatter/__pkginfo__.py new file mode 100644 index 0000000..a65e0f6 --- /dev/null +++ b/src/docformatter/__pkginfo__.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2022 Steven Myint +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# 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. +"""Package information for docformatter.""" + +__version__ = "1.5.0" diff --git a/src/docformatter/configuration.py b/src/docformatter/configuration.py new file mode 100644 index 0000000..be1be5d --- /dev/null +++ b/src/docformatter/configuration.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2022 Steven Myint +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# 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. +"""This module provides docformatter's Configurater class.""" + +# Standard Library Imports +import argparse +import os +from configparser import ConfigParser +from typing import Dict, List, Union + +try: + # Third Party Imports + import tomli + + TOMLI_INSTALLED = True +except ImportError: + TOMLI_INSTALLED = False + +# docformatter Package Imports +from docformatter import __pkginfo__ + + +class Configurater: + """Read and store all the docformatter configuration information.""" + + parser = None + """Parser object.""" + + flargs_dct: Dict[str, Union[bool, float, int, str]] = {} + """Dictionary of configuration file arguments.""" + + configuration_file_lst = [ + "pyproject.toml", + "setup.cfg", + "tox.ini", + ] + """List of supported configuration files.""" + + args: argparse.Namespace = None + + def __init__(self, args: List[Union[bool, int, str]]) -> None: + """Initialize a Configurater class instance. + + Parameters + ---------- + args : list + Any command line arguments passed during invocation. + """ + self.args_lst = args + self.config_file = "" + self.parser = argparse.ArgumentParser( + description=__doc__, + prog="docformatter", + ) + + try: + self.config_file = self.args_lst[ + self.args_lst.index("--config") + 1 + ] + except ValueError: + for _configuration_file in self.configuration_file_lst: + if os.path.isfile(_configuration_file): + self.config_file = f"./{_configuration_file}" + break + + if os.path.isfile(self.config_file): + self._do_read_configuration_file() + + def do_parse_arguments(self) -> None: + """Parse configuration file and command line arguments.""" + changes = self.parser.add_mutually_exclusive_group() + changes.add_argument( + "-i", + "--in-place", + action="store_true", + help="make changes to files instead of printing diffs", + ) + changes.add_argument( + "-c", + "--check", + action="store_true", + help="only check and report incorrectly formatted files", + ) + self.parser.add_argument( + "-r", + "--recursive", + action="store_true", + default=bool(self.flargs_dct.get("recursive", False)), + help="drill down directories recursively", + ) + self.parser.add_argument( + "-e", + "--exclude", + nargs="*", + help="exclude directories and files by names", + ) + self.parser.add_argument( + "--wrap-summaries", + default=int(self.flargs_dct.get("wrap-summaries", 79)), + type=int, + metavar="length", + help="wrap long summary lines at this length; " + "set to 0 to disable wrapping (default: 79)", + ) + self.parser.add_argument( + "--wrap-descriptions", + default=int(self.flargs_dct.get("wrap-descriptions", 72)), + type=int, + metavar="length", + help="wrap descriptions at this length; " + "set to 0 to disable wrapping (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: False)", + ) + self.parser.add_argument( + "--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: 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: 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: 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: False)", + ) + self.parser.add_argument( + "--make-summary-multi-line", + action="store_true", + default=bool( + self.flargs_dct.get("make-summary-multi-line", False) + ), + help="add a newline before and after the summary of a one-line " + "docstring (default: False)", + ) + self.parser.add_argument( + "--close-quotes-on-newline", + action="store_true", + default=bool( + self.flargs_dct.get("close-quotes-on-newline", False) + ), + help="place closing triple quotes on a new-line when a " + "one-line docstring wraps to two or more lines " + "(default: False)", + ) + self.parser.add_argument( + "--range", + metavar="line", + dest="line_range", + default=self.flargs_dct.get("range", None), + type=int, + nargs=2, + help="apply docformatter to docstrings between these " + "lines; line numbers are indexed at 1 (default: None)", + ) + self.parser.add_argument( + "--docstring-length", + metavar="length", + dest="length_range", + default=self.flargs_dct.get("docstring-length", None), + type=int, + nargs=2, + help="apply docformatter to docstrings of given length range " + "(default: None)", + ) + self.parser.add_argument( + "--non-strict", + action="store_true", + default=bool(self.flargs_dct.get("non-strict", False)), + help="don't strictly follow reST syntax to identify lists (see " + "issue #67) (default: False)", + ) + self.parser.add_argument( + "--config", + default=self.config_file, + help="path to file containing docformatter options", + ) + self.parser.add_argument( + "--version", + action="version", + version=f"%(prog)s {__pkginfo__.__version__}", + ) + self.parser.add_argument( + "files", + nargs="+", + help="files to format or '-' for standard in", + ) + + self.args = self.parser.parse_args(self.args_lst[1:]) + + if self.args.line_range: + if self.args.line_range[0] <= 0: + self.parser.error("--range must be positive numbers") + if self.args.line_range[0] > self.args.line_range[1]: + self.parser.error( + "First value of --range should be less than or equal " + "to the second" + ) + + if self.args.length_range: + if self.args.length_range[0] <= 0: + self.parser.error( + "--docstring-length must be positive numbers" + ) + if self.args.length_range[0] > self.args.length_range[1]: + self.parser.error( + "First value of --docstring-length should be less " + "than or equal to the second" + ) + + def _do_read_configuration_file(self) -> None: + """Read docformatter options from a configuration file.""" + argfile = os.path.basename(self.config_file) + for f in self.configuration_file_lst: + if argfile == f: + break + + fullpath, ext = os.path.splitext(self.config_file) + filename = os.path.basename(fullpath) + + if ext == ".toml" and TOMLI_INSTALLED and filename == "pyproject": + self._do_read_toml_configuration() + + if (ext == ".cfg" and filename == "setup") or ( + ext == ".ini" and filename == "tox" + ): + self._do_read_parser_configuration() + + def _do_read_toml_configuration(self) -> None: + """Load configuration information from a *.toml file.""" + with open(self.config_file, "rb") as f: + config = tomli.load(f) + + result = config.get("tool", {}).get("docformatter", None) + if result is not None: + self.flargs_dct = { + k: v if isinstance(v, list) else str(v) + for k, v in result.items() + } + + def _do_read_parser_configuration(self) -> None: + """Load configuration information from a *.cfg or *.ini file.""" + config = ConfigParser() + config.read(self.config_file) + + for _section in [ + "tool.docformatter", + "tool:docformatter", + "docformatter", + ]: + if _section in config.sections(): + self.flargs_dct = { + k: v if isinstance(v, list) else str(v) + for k, v in config[_section].items() + } diff --git a/src/docformatter/encode.py b/src/docformatter/encode.py new file mode 100644 index 0000000..10f1f99 --- /dev/null +++ b/src/docformatter/encode.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2022 Steven Myint +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# 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. +"""This module provides docformatter's Encoder class.""" + +# Standard Library Imports +import collections +import io +import locale +import sys +from typing import Dict + +# Third Party Imports +from charset_normalizer import from_path # pylint: disable=import-error + +unicode = str + + +class Encoder: + """Encoding and decoding of files.""" + + CR = "\r" + LF = "\n" + CRLF = "\r\n" + + def __init__(self): + """Initialize an Encoder 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 diff --git a/src/docformatter/format.py b/src/docformatter/format.py new file mode 100644 index 0000000..506c182 --- /dev/null +++ b/src/docformatter/format.py @@ -0,0 +1,479 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2022 Steven Myint +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# 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. +"""This module provides docformatter's Formattor class.""" + +# Standard Library Imports +import argparse +import collections +import io +import tokenize +from typing import TextIO, Tuple + +# Third Party Imports +import untokenize + +# docformatter Package Imports +import docformatter.strings as _strings +import docformatter.syntax as _syntax +import docformatter.util as _util +import docformatter.encode as _encode + + +unicode = str + + +class FormatResult: + """Possible exit codes.""" + + ok = 0 + error = 1 + interrupted = 2 + check_failed = 3 + + +class Formatter: + """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.""" + + 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 + + self.encodor = _encode.Encoder() + + 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 self.encodor.system_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 _util.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 + ------ + result_code: int + One of the FormatResult codes. + """ + self.encodor.do_detect_encoding(filename) + + with self.encodor.do_open_with_encoding(filename) 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 self.encodor.do_open_with_encoding( + filename, + mode="w", + ) 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 = self.encodor.do_find_newline( + source.splitlines(True) + ) + code = self._format_code(source) + + return _strings.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_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(self.QUOTE_TYPES) + and ( + previous_token_type == tokenize.INDENT + or previous_token_type == tokenize.NEWLINE + or only_comments_so_far + ) + and _util.is_in_range( + self.args.line_range, start[0], end[0] + ) + and _util.has_correct_length( + self.args.length_range, start[0], end[0] + ) + ): + indentation = " " * (len(line) - len(line.lstrip())) + 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_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 = 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(self.QUOTE_TYPES[0]): + return docstring + + # Do not modify things that start with doctests. + if contents.lstrip().startswith(">>>"): + return docstring + + summary, description = _strings.split_summary_and_description(contents) + + # Leave docstrings with underlined summaries alone. + if ( + _syntax.remove_section_header(description).strip() + != description.strip() + ): + return docstring + + if not self.args.force_wrap and _syntax.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 = _syntax.wrap_summary( + _strings.normalize_summary(summary), + wrap_length=self.args.wrap_summaries, + initial_indent=initial_indent, + subsequent_indent=indentation, + ).lstrip() + description = _syntax.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 = _syntax.wrap_summary( + open_quote + _strings.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 = _syntax.wrap_summary( + _strings.normalize_summary(contents), + wrap_length=self.args.wrap_summaries, + initial_indent=indentation, + subsequent_indent=indentation, + ).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" + ) diff --git a/src/docformatter/strings.py b/src/docformatter/strings.py new file mode 100644 index 0000000..a5f9e09 --- /dev/null +++ b/src/docformatter/strings.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2022 Steven Myint +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# 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. +"""This module provides docformatter string functions.""" + +# Standard Library Imports +import re + + +def find_shortest_indentation(lines): + """Determine the shortest indentation in a list of lines. + + Parameters + ---------- + lines : list + A list of lines to check indentation. + + Returns + ------- + indentation : str + The shortest (smallest number of spaces) indentation in the list of + lines. + """ + assert not isinstance(lines, str) + + indentation = None + + for line in lines: + if line.strip(): + non_whitespace_index = len(line) - len(line.lstrip()) + _indent = line[:non_whitespace_index] + if indentation is None or len(_indent) < len(indentation): + indentation = _indent + + return indentation or "" + + +def is_probably_beginning_of_sentence(line): + """Determine if the line begins a sentence. + + Parameters + ---------- + line: + The line to be tested. + + Returns + ------- + is_beginning: bool + True if this token is the beginning of a sentence. + """ + # Check heuristically for a parameter list. + for token in ["@", "-", r"\*"]: + if re.search(r"\s" + token + r"\s", line): + return True + + stripped_line = line.strip() + is_beginning_of_sentence = re.match(r'[^\w"\'`\(\)]', stripped_line) + is_pydoc_ref = re.match(r"^:\w+:", stripped_line) + + return is_beginning_of_sentence and not is_pydoc_ref + + +def normalize_line(line, newline): + """Return line with fixed ending, if ending was present in line. + + Otherwise, does nothing. + """ + stripped = line.rstrip("\n\r") + return stripped + newline if stripped != line else line + + +def normalize_line_endings(lines, newline): + """Return fixed line endings. + + All lines will be modified to use the most common line ending. + """ + return "".join([normalize_line(line, newline) for line in lines]) + + +def normalize_summary(summary): + """Return normalized docstring summary.""" + # remove trailing whitespace + summary = summary.rstrip() + + # Add period at end of sentence and capitalize the first word of the + # summary. + if ( + summary + and (summary[-1].isalnum() or summary[-1] in ['"', "'"]) + and (not summary.startswith("#")) + ): + summary += "." + summary = summary[0].upper() + summary[1:] + + return summary + + +def split_first_sentence(text): + """Split text into first sentence and the rest. + + Return a tuple (sentence, rest). + """ + sentence = "" + rest = text + delimiter = "" + previous_delimiter = "" + + while rest: + split = re.split(r"(\s)", rest, maxsplit=1) + word = split[0] + if len(split) == 3: + delimiter = split[1] + rest = split[2] + else: + assert len(split) == 1 + delimiter = "" + rest = "" + + sentence += previous_delimiter + word + + if sentence.endswith(("e.g.", "i.e.", "Dr.", "Mr.", "Mrs.", "Ms.")): + # Ignore false end of sentence. + pass + elif sentence.endswith((".", "?", "!")): + break + elif sentence.endswith(":") and delimiter == "\n": + # Break on colon if it ends the line. This is a heuristic to detect + # the beginning of some parameter list afterwards. + break + + previous_delimiter = delimiter + delimiter = "" + + return sentence, delimiter + rest + + +def split_summary_and_description(contents): + """Split docstring into summary and description. + + Return tuple (summary, description). + """ + split_lines = contents.rstrip().splitlines() + + for index in range(1, len(split_lines)): + found = False + + # Empty line separation would indicate the rest is the description or, + # symbol on second line probably is a description with a list. + if not split_lines[index].strip() or is_probably_beginning_of_sentence( + split_lines[index] + ): + found = True + + if found: + return ( + "\n".join(split_lines[:index]).strip(), + "\n".join(split_lines[index:]).rstrip(), + ) + + # Break on first sentence. + split = split_first_sentence(contents) + if split[0].strip() and split[1].strip(): + return ( + split[0].strip(), + find_shortest_indentation(split[1].splitlines()[1:]) + + split[1].strip(), + ) + + return contents, "" diff --git a/src/docformatter/syntax.py b/src/docformatter/syntax.py new file mode 100644 index 0000000..0ef5aca --- /dev/null +++ b/src/docformatter/syntax.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2022 Steven Myint +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# 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. +"""This module provides docformatter's Syntaxor class.""" + +# Standard Library Imports +import contextlib +import re +import textwrap +from typing import List + +HEURISTIC_MIN_LIST_ASPECT_RATIO = 0.4 + + +def do_preserve_links( + text: str, + indentation: str, + wrap_length: int, +) -> List[str]: + """Rebuild links in docstring. + + Parameters + ---------- + text : str + The docstring description. + indentation : str + The indentation (number of spaces or tabs) to place in front of each + line. + wrap_length : int + The column to wrap each line at. + + Returns + ------- + lines : list + A list containing each line of the description with any links put + back together. + """ + lines = textwrap.wrap( + textwrap.dedent(text), + width=wrap_length, + initial_indent=indentation, + subsequent_indent=indentation, + ) + + url = next( + ( + line + for line in lines + if re.search(r")? We want to keep + # the '<' and '>' part of the link. + if re.search(r"<", url): + lines[url_idx] = f"{indentation}" + url.split(sep="<")[0].strip() + url = f"{indentation}<" + url.split(sep="<")[1] + url = url + lines[url_idx + 1].strip() + lines[url_idx + 1] = url + # Is this a link target definition (i.e., .. a link: https://)? We + # want to keep the .. a link: on the same line as the url. + elif re.search(r"(\.\. )", url): + url = url + lines[url_idx + 1].strip() + lines[url_idx] = url + lines.pop(url_idx + 1) + # Is this a simple link (i.e., just a link in the text) that should + # be unwrapped? We want to break the url out from the rest of the + # text. + elif len(lines[url_idx]) >= wrap_length: + lines[url_idx] = ( + f"{indentation}" + url.strip().split(sep=" ")[0].strip() + ) + url = f"{indentation}" + url.strip().split(sep=" ")[1].strip() + url = url + lines[url_idx + 1].strip().split(sep=" ")[0].strip() + lines.append( + indentation + + " ".join(lines[url_idx + 1].strip().split(sep=" ")[1:]) + ) + lines[url_idx + 1] = url + + with contextlib.suppress(IndexError): + if lines[url_idx + 2].strip() in [".", "?", "!", ";"] or re.search( + r">", lines[url_idx + 2] + ): + url = url + lines[url_idx + 2].strip() + lines[url_idx + 1] = url + lines.pop(url_idx + 2) + + return lines + + +# pylint: disable=line-too-long +def is_some_sort_of_list(text, strict) -> bool: + """Determine if docstring is a reST list. + + Notes + ----- + There are five types of lists in reST/docutils that need to be handled. + + * `Bullets lists + `_ + * `Enumerated lists + `_ + * `Definition lists + `_ + * `Field lists + `_ + * `Option lists + `_ + """ + split_lines = text.rstrip().splitlines() + + # TODO: Find a better way of doing this. + # Very large number of lines but short columns probably means a list of + # items. + if ( + len(split_lines) + / max([len(line.strip()) for line in split_lines] + [1]) + > HEURISTIC_MIN_LIST_ASPECT_RATIO + ) and not strict: + return True + + return any( + ( + re.match(r"\s*$", line) + or + # "1. item" + re.match(r"\s*\d\.", line) + or + # "@parameter" + re.match(r"\s*[\-*:=@]", line) + or + # "parameter - description" + re.match(r".*\s+[\-*:=@]\s+", line) + or + # "parameter: description" + re.match(r"\s*\S+[\-*:=@]\s+", line) + or + # "parameter:\n description" + re.match(r"\s*\S+:\s*$", line) + or + # "parameter -- description" + re.match(r"\s*\S+\s+--\s+", line) + ) + for line in split_lines + ) + + +def is_some_sort_of_code(text: str) -> bool: + """Return True if text looks like code.""" + return any( + len(word) > 50 + and not re.match(r"<{0,1}(http:|https:|ftp:|sftp:)", word) + for word in text.split() + ) + + +def reindent(text, indentation): + """Return reindented text that matches indentation.""" + if "\t" not in indentation: + text = text.expandtabs() + + text = textwrap.dedent(text) + + return ( + "\n".join( + [(indentation + line).rstrip() for line in text.splitlines()] + ).rstrip() + + "\n" + ) + + +def remove_section_header(text): + r"""Return text with section header removed. + + >>> remove_section_header('----\nfoo\nbar\n') + 'foo\nbar\n' + + >>> remove_section_header('===\nfoo\nbar\n') + 'foo\nbar\n' + """ + stripped = text.lstrip() + if not stripped: + return text + + first = stripped[0] + return ( + text + if ( + first.isalnum() + or first.isspace() + or stripped.splitlines()[0].strip(first).strip() + ) + else stripped.lstrip(first).lstrip() + ) + + +def strip_leading_blank_lines(text): + """Return text with leading blank lines removed.""" + split = text.splitlines() + + found = next( + (index for index, line in enumerate(split) if line.strip()), 0 + ) + + return "\n".join(split[found:]) + + +def unwrap_summary(summary): + """Return summary with newlines removed in preparation for wrapping.""" + return re.sub(r"\s*\n\s*", " ", summary) + + +def wrap_summary(summary, initial_indent, subsequent_indent, wrap_length): + """Return line-wrapped summary text.""" + if wrap_length > 0: + return textwrap.fill( + unwrap_summary(summary), + width=wrap_length, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + ).strip() + else: + return summary + + +def wrap_description(text, indentation, wrap_length, force_wrap, strict): + """Return line-wrapped description text. + + We only wrap simple descriptions. We leave doctests, multi-paragraph + text, and bulleted lists alone. + """ + text = strip_leading_blank_lines(text) + + # Do not modify doctests at all. + if ">>>" in text: + return text + + text = reindent(text, indentation).rstrip() + + # Ignore possibly complicated cases. + if wrap_length <= 0 or ( + not force_wrap + and (is_some_sort_of_code(text) or is_some_sort_of_list(text, strict)) + ): + return text + + text = do_preserve_links(text, indentation, wrap_length) + + return indentation + "\n".join(text).strip() diff --git a/src/docformatter/util.py b/src/docformatter/util.py new file mode 100644 index 0000000..b78376d --- /dev/null +++ b/src/docformatter/util.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2022 Steven Myint +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# 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. +"""This module provides docformatter utility functions.""" + +# Standard Library Imports +import os +import re +import sysconfig + +unicode = str + +_PYTHON_LIBS = set(sysconfig.get_paths().values()) + + +def find_py_files(sources, recursive, exclude=None): + """Find Python source files. + + Parameters + - sources: iterable with paths as strings. + - recursive: drill down directories if True. + - exclude: string based on which directories and files are excluded. + + Return: yields paths to found files. + """ + + def not_hidden(name): + """Return True if file 'name' isn't .hidden.""" + return not name.startswith(".") + + def is_excluded(name, exclude): + """Return True if file 'name' is excluded.""" + return ( + any( + re.search(re.escape(str(e)), name, re.IGNORECASE) + for e in exclude + ) + if exclude + else False + ) + + for name in sorted(sources): + if recursive and os.path.isdir(name): + for root, dirs, children in os.walk(unicode(name)): + dirs[:] = [ + d + for d in dirs + if not_hidden(d) and not is_excluded(d, _PYTHON_LIBS) + ] + dirs[:] = sorted( + [d for d in dirs if not is_excluded(d, exclude)] + ) + files = sorted( + [ + f + for f in children + if not_hidden(f) and not is_excluded(f, exclude) + ] + ) + for filename in files: + if filename.endswith(".py") and not is_excluded( + root, exclude + ): + yield os.path.join(root, filename) + else: + yield name + + +def has_correct_length(length_range, start, end): + """Determine if the line under test is within desired docstring length. + + This function is used with the --docstring-length min_rows max_rows + argument. + + Parameters + ---------- + length_range: list + The file row range passed to the --docstring-length argument. + start: int + The row number where the line under test begins in the source file. + end: int + The row number where the line under tests ends in the source file. + + Returns + ------- + correct_length: bool + True if is correct length or length range is None, else False + """ + if length_range is None: + return True + min_length, max_length = length_range + + docstring_length = end + 1 - start + return min_length <= docstring_length <= max_length + + +def is_in_range(line_range, start, end): + """Determine if ??? is within the desired range. + + This function is used with the --range start_row end_row argument. + + Parameters + ---------- + line_range: list + The line number range passed to the --range argument. + start: int + The row number where the line under test begins in the source file. + end: int + The row number where the line under tests ends in the source file. + + Returns + ------- + in_range : bool + True if in range or range is None, else False + """ + if line_range is None: + return True + return any( + line_range[0] <= line_no <= line_range[1] + for line_no in range(start, end + 1) + ) diff --git a/tests/conftest.py b/tests/conftest.py index c31d6e1..463af60 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,25 +40,6 @@ # Root directory is up one because we're in tests/. ROOT_DIRECTORY = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -if "DOCFORMATTER_COVERAGE" in os.environ and int( - os.environ["DOCFORMATTER_COVERAGE"] -): - DOCFORMATTER_COMMAND = [ - "coverage", - "run", - "--branch", - "--parallel", - "--omit=*/site-packages/*", - os.path.join(ROOT_DIRECTORY, "docformatter.py"), - ] -else: - # We need to specify the executable to make sure the correct Python - # interpreter gets used. - DOCFORMATTER_COMMAND = [ - sys.executable, - os.path.join(ROOT_DIRECTORY, "docformatter.py"), - ] # pragma: no cover - @pytest.fixture(scope="function") def temporary_directory(directory=".", prefix=""): @@ -90,6 +71,22 @@ def run_docformatter(arguments, temporary_file): Return subprocess object. """ + if "DOCFORMATTER_COVERAGE" in os.environ and int( + os.environ["DOCFORMATTER_COVERAGE"] + ): + DOCFORMATTER_COMMAND = [ + "coverage", + "run", + "--branch", + "--parallel", + "--omit=*/site-packages/*", + os.environ["VIRTUAL_ENV"] + "/bin/docformatter", + ] + else: + DOCFORMATTER_COMMAND = [ + os.environ["VIRTUAL_ENV"] + "/bin/docformatter", + ] # pragma: no cover + if "-" not in arguments: arguments.append(temporary_file) environ = os.environ.copy() diff --git a/tests/test_configuration_functions.py b/tests/test_configuration_functions.py index 8c4cd8e..0710e9d 100644 --- a/tests/test_configuration_functions.py +++ b/tests/test_configuration_functions.py @@ -4,7 +4,7 @@ # tests.test_configuration_functions.py is part of the docformatter # project # -# Copyright (C) 2012-2019 Steven Myint +# Copyright (C) 2012-2022 Steven Myint # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -25,13 +25,7 @@ # 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 functions used to control docformatter configuration. - -Configuration functions are: - - - find_config_file() - - read_configuration_file() -""" +"""Module for testing docformatter's Configurater class.""" # Standard Library Imports import io @@ -41,22 +35,21 @@ import pytest # docformatter Package Imports -import docformatter -from docformatter import Configurator +from docformatter import Configurater -class TestConfigurator: +class TestConfigurater: """Class for testing configuration functions.""" @pytest.mark.unit def test_initialize_configurator_with_default(self): - """Return a Configurator() instance using default pyproject.toml.""" + """Return a Configurater() instance using default pyproject.toml.""" argb = [ "/path/to/docformatter", "", ] - uut = Configurator(argb) + uut = Configurater(argb) uut.do_parse_arguments() assert uut.args_lst == argb @@ -64,7 +57,7 @@ def test_initialize_configurator_with_default(self): @pytest.mark.unit def test_initialize_configurator_with_pyproject_toml(self): - """Return a Configurator() instance loaded from a pyproject.toml.""" + """Return a Configurater() instance loaded from a pyproject.toml.""" argb = [ "/path/to/docformatter", "-c", @@ -73,7 +66,7 @@ def test_initialize_configurator_with_pyproject_toml(self): "", ] - uut = Configurator(argb) + uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.check @@ -101,7 +94,7 @@ def test_initialize_configurator_with_setup_cfg(self): "", ] - uut = Configurator(argb) + uut = Configurater(argb) uut.do_parse_arguments() assert uut.config_file == "./tests/_data/setup.cfg" @@ -122,7 +115,7 @@ def test_initialize_configurator_with_tox_ini(self): "", ] - uut = Configurator(argb) + uut = Configurater(argb) uut.do_parse_arguments() assert uut.config_file == "./tests/_data/tox.ini" @@ -143,7 +136,7 @@ def test_unsupported_config_file(self): "", ] - uut = Configurator(argb) + uut = Configurater(argb) uut.do_parse_arguments() assert uut.config_file == "./tests/conf.py" @@ -164,7 +157,7 @@ def test_cli_override_config_file(self): "", ] - uut = Configurator(argb) + uut = Configurater(argb) uut.do_parse_arguments() assert uut.config_file == "./tests/_data/tox.ini" @@ -200,7 +193,7 @@ def test_only_format_in_line_range(self, capsys): "", ] - uut = Configurator(argb) + uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.line_range == [1, 3] @@ -217,7 +210,7 @@ def test_low_line_range_is_zero(self, capsys): "", ] - uut = Configurator(argb) + uut = Configurater(argb) with pytest.raises(SystemExit): uut.do_parse_arguments() @@ -227,8 +220,7 @@ def test_low_line_range_is_zero(self, capsys): @pytest.mark.unit def test_low_line_range_greater_than_high_line_range(self, capsys): - """Raise parser error if the first value for the range is greater than - the second.""" + """Raise parser error if first value for range > than second.""" argb = [ "/path/to/docformatter", "-c", @@ -238,7 +230,7 @@ def test_low_line_range_greater_than_high_line_range(self, capsys): "", ] - uut = Configurator(argb) + uut = Configurater(argb) with pytest.raises(SystemExit): uut.do_parse_arguments() @@ -261,15 +253,14 @@ def test_only_format_in_length_range(self, capsys): "", ] - uut = Configurator(argb) + uut = Configurater(argb) uut.do_parse_arguments() assert uut.args.length_range == [25, 55] @pytest.mark.unit def test_low_length_range_is_zero(self, capsys): - """Raise parser error if the first value for the length range is - zero.""" + """Raise parser error if the first value for the length range = 0.""" argb = [ "/path/to/docformatter", "-c", @@ -279,7 +270,7 @@ def test_low_length_range_is_zero(self, capsys): "", ] - uut = Configurator(argb) + uut = Configurater(argb) with pytest.raises(SystemExit): uut.do_parse_arguments() @@ -289,8 +280,7 @@ def test_low_length_range_is_zero(self, capsys): @pytest.mark.unit def test_low_length_range_greater_than_high_length_range(self, capsys): - """Raise parser error if the first value for the range is greater than - the second.""" + """Raise parser error if first value for range > second value.""" argb = [ "/path/to/docformatter", "-c", @@ -300,7 +290,7 @@ def test_low_length_range_greater_than_high_length_range(self, capsys): "", ] - uut = Configurator(argb) + uut = Configurater(argb) with pytest.raises(SystemExit): uut.do_parse_arguments() diff --git a/tests/test_docformatter.py b/tests/test_docformatter.py index 4cbc213..20007ee 100644 --- a/tests/test_docformatter.py +++ b/tests/test_docformatter.py @@ -34,7 +34,7 @@ import pytest # docformatter Package Imports -import docformatter +from docformatter import __main__ as main class TestMain: @@ -55,7 +55,7 @@ def foo(): def test_diff(self, temporary_file, contents): """Should produce diff showing changes.""" output_file = io.StringIO() - docformatter._main( + main._main( argv=["my_fake_program", temporary_file], standard_out=output_file, standard_error=None, @@ -77,7 +77,7 @@ def foo(): def test_diff_with_nonexistent_file(self): """Should return error message when file doesn't exist.""" output_file = io.StringIO() - docformatter._main( + main._main( argv=["my_fake_program", "nonexistent_file"], standard_out=output_file, standard_error=output_file, @@ -100,7 +100,7 @@ def foo(): def test_in_place(self, temporary_file, contents): """Should make changes and save back to file.""" output_file = io.StringIO() - docformatter._main( + main._main( argv=["my_fake_program", "--in-place", temporary_file], standard_out=output_file, standard_error=None, @@ -141,7 +141,7 @@ def test_ignore_hidden_directories( ): """Ignore 'hidden' directories when recursing.""" output_file = io.StringIO() - docformatter._main( + main._main( argv=["my_fake_program", "--recursive", temporary_directory], standard_out=output_file, standard_error=None, @@ -153,7 +153,7 @@ def test_ignore_hidden_directories( def test_io_error_exit_code(self): """Return error code 1 when file does not exist.""" stderr = io.StringIO() - ret_code = docformatter._main( + ret_code = main._main( argv=["my_fake_program", "this_file_should_not_exist_please"], standard_out=None, standard_error=stderr, @@ -171,7 +171,7 @@ def test_check_mode_correct_docstring(self, temporary_file, contents): """""" stdout = io.StringIO() stderr = io.StringIO() - ret_code = docformatter._main( + ret_code = main._main( argv=["my_fake_program", "--check", temporary_file], standard_out=stdout, standard_error=stderr, @@ -197,7 +197,7 @@ def test_check_mode_incorrect_docstring(self, temporary_file, contents): """""" stdout = io.StringIO() stderr = io.StringIO() - ret_code = docformatter._main( + ret_code = main._main( argv=["my_fake_program", "--check", temporary_file], standard_out=stdout, standard_error=stderr, diff --git a/tests/test_encoding_functions.py b/tests/test_encoding_functions.py index e8281b9..2058683 100644 --- a/tests/test_encoding_functions.py +++ b/tests/test_encoding_functions.py @@ -41,7 +41,7 @@ import pytest # docformatter Package Imports -from docformatter import Encodor +from docformatter import Encoder SYSTEM_ENCODING = sys.getdefaultencoding() @@ -56,7 +56,7 @@ def test_detect_encoding_with_explicit_utf_8( self, temporary_file, contents ): """Return utf-8 when explicitely set in file.""" - uut = Encodor() + uut = Encoder() uut.do_detect_encoding(temporary_file) assert "utf_8" == uut.encoding @@ -69,7 +69,7 @@ def test_detect_encoding_with_non_explicit_setting( self, temporary_file, contents ): """Return default system encoding when encoding not explicitly set.""" - uut = Encodor() + uut = Encoder() uut.do_detect_encoding(temporary_file) assert "ascii" == uut.encoding @@ -78,7 +78,7 @@ def test_detect_encoding_with_non_explicit_setting( @pytest.mark.parametrize("contents", ["# -*- coding: blah -*-"]) def test_detect_encoding_with_bad_encoding(self, temporary_file, contents): """Default to latin-1 when unknown encoding detected.""" - uut = Encodor() + uut = Encoder() uut.do_detect_encoding(temporary_file) assert "ascii" == uut.encoding @@ -90,7 +90,7 @@ class TestFindNewline: @pytest.mark.unit def test_find_newline_only_cr(self): """Return carriage return as newline type.""" - uut = Encodor() + uut = Encoder() source = ["print 1\r", "print 2\r", "print3\r"] assert uut.CR == uut.do_find_newline(source) @@ -98,7 +98,7 @@ def test_find_newline_only_cr(self): @pytest.mark.unit def test_find_newline_only_lf(self): """Return line feed as newline type.""" - uut = Encodor() + uut = Encoder() source = ["print 1\n", "print 2\n", "print3\n"] assert uut.LF == uut.do_find_newline(source) @@ -106,7 +106,7 @@ def test_find_newline_only_lf(self): @pytest.mark.unit def test_find_newline_only_crlf(self): """Return carriage return, line feed as newline type.""" - uut = Encodor() + uut = Encoder() source = ["print 1\r\n", "print 2\r\n", "print3\r\n"] assert uut.CRLF == uut.do_find_newline(source) @@ -114,7 +114,7 @@ def test_find_newline_only_crlf(self): @pytest.mark.unit def test_find_newline_cr1_and_lf2(self): """Favor line feed over carriage return when both are found.""" - uut = Encodor() + uut = Encoder() source = ["print 1\n", "print 2\r", "print3\n"] assert uut.LF == uut.do_find_newline(source) @@ -122,7 +122,7 @@ def test_find_newline_cr1_and_lf2(self): @pytest.mark.unit def test_find_newline_cr1_and_crlf2(self): """Favor carriage return, line feed when mix of newline types.""" - uut = Encodor() + uut = Encoder() source = ["print 1\r\n", "print 2\r", "print3\r\n"] assert uut.CRLF == uut.do_find_newline(source) @@ -130,7 +130,7 @@ def test_find_newline_cr1_and_crlf2(self): @pytest.mark.unit def test_find_newline_should_default_to_lf(self): """Default to line feed when no newline type found.""" - uut = Encodor() + uut = Encoder() assert uut.LF == uut.do_find_newline([]) assert uut.LF == uut.do_find_newline(["", ""]) @@ -138,7 +138,7 @@ def test_find_newline_should_default_to_lf(self): @pytest.mark.unit def test_find_dominant_newline(self): """Should detect carriage return as the dominant line endings.""" - uut = Encodor() + uut = Encoder() goes_in = '''\ def foo():\r @@ -158,7 +158,7 @@ class TestOpenWithEncoding: @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) def test_open_with_utf_8_encoding(self, temporary_file, contents): """Return TextIOWrapper object when opening file with encoding.""" - uut = Encodor() + uut = Encoder() uut.do_detect_encoding(temporary_file) assert isinstance( @@ -170,7 +170,7 @@ def test_open_with_utf_8_encoding(self, temporary_file, contents): @pytest.mark.parametrize("contents", ["# -*- coding: utf-8 -*-\n"]) def test_open_with_wrong_encoding(self, temporary_file, contents): """Raise LookupError when passed unknown encoding.""" - uut = Encodor() + uut = Encoder() uut.encoding = "cr1252" with pytest.raises(LookupError): diff --git a/tests/test_format_code.py b/tests/test_format_code.py index 452219c..5cd5254 100644 --- a/tests/test_format_code.py +++ b/tests/test_format_code.py @@ -34,7 +34,7 @@ # docformatter Package Imports import docformatter -from docformatter import Formator +from docformatter import Formatter class TestFormatCode: @@ -44,7 +44,7 @@ class TestFormatCode: @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( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -63,7 +63,7 @@ def test_format_code_should_ignore_non_docstring(self, test_args, args): @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_empty_string(self, test_args, args): """Should do nothing with an empty string.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -77,7 +77,7 @@ def test_format_code_with_empty_string(self, test_args, args): @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_tabs(self, test_args, args): """Should retain tabbed indentation.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -104,7 +104,7 @@ def foo(): @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_mixed_tabs(self, test_args, args): """Should retain mixed tabbed and spaced indentation.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -131,7 +131,7 @@ def foo(): @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_escaped_newlines(self, test_args, args): """Should leave escaped newlines in code untouched.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -156,7 +156,7 @@ def test_format_code_with_escaped_newlines(self, test_args, args): @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_comments(self, test_args, args): """Should leave comments as is.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -187,7 +187,7 @@ def test_format_code_with_escaped_newline_in_inline_comment( self, test_args, args ): """Should leave code with inline comment as is.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -216,7 +216,7 @@ def test_format_code_raw_docstring_double_quotes(self, test_args, args): See requirement PEP_257_2. See issue #54 for request to handle raw docstrings. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -255,7 +255,7 @@ def test_format_code_raw_docstring_single_quotes(self, test_args, args): See requirement PEP_257_2. See issue #54 for request to handle raw docstrings. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -296,7 +296,7 @@ def test_format_code_unicode_docstring_double_quotes( See requirement PEP_257_3. See issue #54 for request to handle raw docstrings. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -337,7 +337,7 @@ def test_format_code_unicode_docstring_single_quotes( See requirement PEP_257_3. See issue #54 for request to handle raw docstrings. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -372,7 +372,7 @@ def foo(): @pytest.mark.parametrize("args", [[""]]) def test_format_code_skip_nested(self, test_args, args): """Should ignore nested triple quotes.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -390,7 +390,7 @@ def foo(): @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( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -419,7 +419,7 @@ def test_format_code_with_multiple_sentences_same_line( self, test_args, args ): """Should create multi-line docstring from multiple sentences.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -447,7 +447,7 @@ def test_format_code_with_multiple_sentences_multi_line_summary( self, test_args, args ): """Should put summary line on a single line.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -474,7 +474,7 @@ def foo(): @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( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -503,7 +503,7 @@ def foo(): @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_trailing_whitespace(self, test_args, args): """Should strip trailing whitespace.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -534,7 +534,7 @@ def foo(): @pytest.mark.parametrize("args", [[""]]) def test_format_code_with_parameters_list(self, test_args, args): """Should treat parameters list as elaborate description.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -568,7 +568,7 @@ def test_ignore_code_with_single_quote(self, test_args, args): See requirement PEP_257_1. See issue #66 for example of docformatter breaking code when encountering single quote. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -593,7 +593,7 @@ def test_ignore_code_with_double_quote(self, test_args, args): See requirement PEP_257_1. See issue #66 for example of docformatter breaking code when encountering single quote. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -616,7 +616,7 @@ def test_format_code_should_skip_nested_triple_quotes( self, test_args, args ): """Should ignore triple quotes nested in a string.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -633,7 +633,7 @@ def foo(): @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( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -654,7 +654,7 @@ def foo(): @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( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -695,7 +695,7 @@ def foo(): @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.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -708,7 +708,7 @@ def test_format_code_with_syntax_error(self, test_args, args): @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.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -723,7 +723,7 @@ 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.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -738,7 +738,7 @@ def test_format_code_dominant_line_ending_style_preserved( self, test_args, args ): """Should retain carriage return line endings.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -771,7 +771,7 @@ def test_format_code_additional_empty_line_before_doc( See issue #51. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -794,7 +794,7 @@ def test_format_code_extra_newline_following_comment( See issue #51. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -821,7 +821,7 @@ def test_format_code_no_docstring(self, test_args, args): See issue #97. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -845,7 +845,7 @@ def test_format_code_no_docstring(self, test_args, args): @pytest.mark.parametrize("args", [[""]]) def test_format_code_class_docstring(self, test_args, args): """Format class docstring.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -877,7 +877,7 @@ class TestFormatCodeRanges: @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( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -904,7 +904,7 @@ def g(x): @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( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -934,7 +934,7 @@ def g(x): @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( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -973,7 +973,7 @@ class TestDoFormatCode: @pytest.mark.parametrize("args", [[""]]) def test_do_format_code(self, test_args, args): """Should place one-liner on single line.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -996,7 +996,7 @@ def foo(): @pytest.mark.parametrize("args", [[""]]) def test_do_format_code_with_module_docstring(self, test_args, args): """Should format module docstrings.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, diff --git a/tests/test_format_docstring.py b/tests/test_format_docstring.py index 7f73a18..8e8dae6 100644 --- a/tests/test_format_docstring.py +++ b/tests/test_format_docstring.py @@ -37,7 +37,7 @@ # docformatter Package Imports import docformatter -from docformatter import Formator +from docformatter import Formatter # docformatter Local Imports from . import generate_random_docstring @@ -52,7 +52,7 @@ class TestFormatDocstring: @pytest.mark.parametrize("args", [[""]]) def test_format_docstring(self, test_args, args): """Return one-line docstring.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -75,7 +75,7 @@ def test_format_docstring_with_summary_that_ends_in_quote( self, test_args, args ): """Return one-line docstring with period after quote.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -96,7 +96,7 @@ def test_format_docstring_with_summary_that_ends_in_quote( @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( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -124,7 +124,7 @@ def test_format_docstring_with_bad_indentation(self, test_args, args): @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( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -157,7 +157,7 @@ def test_format_docstring_with_too_much_indentation(self, test_args, args): @pytest.mark.parametrize("args", [["--wrap-descriptions", "52", ""]]) def test_format_docstring_with_trailing_whitespace(self, test_args, args): """Remove trailing white space.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -185,7 +185,7 @@ def test_format_docstring_with_trailing_whitespace(self, test_args, args): @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_empty_docstring(self, test_args, args): """Do nothing with empty docstring.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -198,7 +198,7 @@ def test_format_docstring_with_empty_docstring(self, test_args, args): @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.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -219,7 +219,7 @@ def test_format_docstring_with_no_period(self, test_args, args): @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_single_quotes(self, test_args, args): """Replace single triple quotes with triple double quotes.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -242,7 +242,7 @@ def test_format_docstring_with_single_quotes_multi_line( self, test_args, args ): """Replace single triple quotes with triple double quotes.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -271,7 +271,7 @@ def test_format_docstring_leave_underlined_summaries_alone( self, test_args, args ): """Leave underlined summary lines as is.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -297,7 +297,7 @@ def test_format_docstring_should_ignore_numbered_lists( self, test_args, args ): """Ignore lists beginning with numbers.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -321,7 +321,7 @@ def test_format_docstring_should_ignore_parameter_lists( self, test_args, args ): """Ignore lists beginning with -.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -344,7 +344,7 @@ def test_format_docstring_should_ignore_colon_parameter_lists( self, test_args, args ): """Ignore lists beginning with :""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -364,7 +364,7 @@ def test_format_docstring_should_ignore_colon_parameter_lists( @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_should_leave_list_alone(self, test_args, args): - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -408,7 +408,7 @@ def test_format_docstring_with_wrap( args, ): """Wrap the docstring.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -445,7 +445,7 @@ def test_format_docstring_with_weird_indentation_and_punctuation( args, ): """Wrap and dedent docstring.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -479,7 +479,7 @@ def test_format_docstring_with_description_wrapping( args, ): """Wrap description at 72 characters.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -511,7 +511,7 @@ def test_format_docstring_should_ignore_multi_paragraph( args, ): """Ignore multiple paragraphs in elaborate description.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -541,7 +541,7 @@ def test_format_docstring_should_ignore_doctests( args, ): """Leave doctests alone.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -566,7 +566,7 @@ def test_format_docstring_should_ignore_doctests_in_summary( args, ): """Leave doctests alone if they're in the summary.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -591,7 +591,7 @@ def test_format_docstring_should_maintain_indentation_of_doctest( args, ): """Don't change indentation of doctest lines.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -634,7 +634,7 @@ def test_force_wrap( args, ): """Force even lists to be wrapped.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -673,7 +673,7 @@ def test_format_docstring_with_summary_only_and_wrap_and_tab_indentation( See PR #69. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -701,7 +701,7 @@ def test_format_docstring_for_multi_line_summary_alone( args, ): """Place closing quotes on newline when wrapping one-liner.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -735,7 +735,7 @@ def test_format_docstring_for_one_line_summary_alone_but_too_long( args, ): """""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -770,7 +770,7 @@ def test_format_docstring_with_inline_links( See issue #75. See requirement docformatter_10.1.3. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -811,7 +811,7 @@ def test_format_docstring_with_target_links( See issue #75. See requirement docformatter_10.1.3. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -848,7 +848,7 @@ def test_format_docstring_with_simple_link( See issue #75. See requirement docformatter_10.1.3. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -887,7 +887,7 @@ def test_format_docstring_with_short_link( See issue #75. See requirement docformatter_10.1.3. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -914,7 +914,7 @@ def test_format_docstring_with_short_link( @pytest.mark.parametrize("args", [[""]]) def test_format_docstring_with_class_attributes(self, test_args, args): """Wrap long class attribute docstrings.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -951,7 +951,7 @@ def test_format_docstring_with_no_post_description_blank( args, ): """Remove blank lines before closing triple quotes.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -983,7 +983,7 @@ def test_format_docstring_with_pre_summary_newline( args, ): """Remove blank line before summary.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -1016,7 +1016,7 @@ def test_format_docstring_make_summary_multi_line( args, ): """Place the one-line docstring between triple quotes.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -1047,7 +1047,7 @@ def test_format_docstring_pre_summary_space( args, ): """Place a space between the opening quotes and the summary.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -1075,7 +1075,7 @@ def test_strip_docstring( args, ): """Strip triple double quotes from docstring.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -1101,7 +1101,7 @@ def test_strip_docstring_with_single_quotes( args, ): """Strip triple single quotes from docstring.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -1127,7 +1127,7 @@ def test_strip_docstring_with_empty_string( args, ): """Return series of six double quotes when passed empty string.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -1146,7 +1146,7 @@ def test_strip_docstring_with_raw_string( args, ): """Return docstring and raw open quote.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -1169,7 +1169,7 @@ def test_strip_docstring_with_unicode_string( args, ): """Return docstring and unicode open quote.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -1192,7 +1192,7 @@ def test_strip_docstring_with_unknown( args, ): """Raise ValueError with single quotes.""" - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -1214,7 +1214,7 @@ def test_strip_docstring_with_single_quotes( See requirement PEP_257_1. See issue #66 for example of docformatter breaking code when encountering single quote. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, @@ -1236,7 +1236,7 @@ def test_strip_docstring_with_double_quotes( See requirement PEP_257_1. See issue #66 for example of docformatter breaking code when encountering single quote. """ - uut = Formator( + uut = Formatter( test_args, sys.stderr, sys.stdin, diff --git a/tests/test_string_functions.py b/tests/test_string_functions.py index 423a9d9..84f4f9f 100644 --- a/tests/test_string_functions.py +++ b/tests/test_string_functions.py @@ -30,7 +30,7 @@ those: reindent() - _find_shortest_indentation() + find_shortest_indentation() normalize_line() normalize_line_endings() normalize_summary() @@ -55,7 +55,7 @@ class TestIndenters: Includes tests for: - reindent() - - _find_shortest_indentation() + - find_shortest_indentation() """ @pytest.mark.unit @@ -143,9 +143,9 @@ def test_reindent_tab_indentation(self): ) @pytest.mark.unit - def test_find_shortest_indentation(self): + def testfind_shortest_indentation(self): """Should find the shortest indentation to be one space.""" - assert " " == docformatter._find_shortest_indentation( + assert " " == docformatter.find_shortest_indentation( [" ", " b", " a"], )