diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8808fb78..9fe23070 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 diff --git a/docs/release_notes.rst b/docs/release_notes.rst index d0182a83..a9060aef 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -10,8 +10,10 @@ Current Development Version New Features +* Allow for hanging indent when documenting args in Google style. (#449) * Add support for `property_decorators` config to ignore D401. * Add support for Python 3.10 (#554). +* Replace D10X errors with D419 if docstring exists but is empty (#559). Bug Fixes diff --git a/requirements/tests.txt b/requirements/tests.txt index 42538dd4..d6bf398b 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,4 +1,6 @@ pytest==6.2.5 -mypy==0.782 -black==20.8b1 +mypy==0.930 +black==21.12b0 isort==5.4.2 +types-toml +types-setuptools diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index be74867f..0b456444 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -6,6 +6,7 @@ from collections import namedtuple from itertools import chain, takewhile from re import compile as re +from textwrap import dedent from . import violations from .config import IllegalConfiguration @@ -122,6 +123,8 @@ class ConventionChecker: r"\s*" # Followed by a colon r":" + # Might have a new line and leading whitespace + r"\n?\s*" # Followed by 1 or more characters - which is the docstring for the parameter ".+" ) @@ -196,12 +199,7 @@ def check_docstring_missing(self, definition, docstring): with a single underscore. """ - if ( - not docstring - and definition.is_public - or docstring - and is_blank(ast.literal_eval(docstring)) - ): + if not docstring and definition.is_public: codes = { Module: violations.D100, Class: violations.D101, @@ -227,6 +225,18 @@ def check_docstring_missing(self, definition, docstring): } return codes[type(definition)]() + @check_for(Definition, terminal=True) + def check_docstring_empty(self, definition, docstring): + """D419: Docstring is empty. + + If the user provided a docstring but it was empty, it is like they never provided one. + + NOTE: This used to report as D10X errors. + + """ + if docstring and is_blank(ast.literal_eval(docstring)): + return violations.D419() + @check_for(Definition) def check_one_liners(self, definition, docstring): """D200: One-liner docstrings should fit on one line with quotes. @@ -836,10 +846,38 @@ def _check_args_section(docstring, definition, context): * The section documents all function arguments (D417) except `self` or `cls` if it is a method. + Documentation for each arg should start at the same indentation + level. For example, in this case x and y are distinguishable:: + + Args: + x: Lorem ipsum dolor sit amet + y: Ut enim ad minim veniam + + In the case below, we only recognize x as a documented parameter + because the rest of the content is indented as if it belongs + to the description for x:: + + Args: + x: Lorem ipsum dolor sit amet + y: Ut enim ad minim veniam """ docstring_args = set() - for line in context.following_lines: - match = ConventionChecker.GOOGLE_ARGS_REGEX.match(line) + # normalize leading whitespace + args_content = dedent("\n".join(context.following_lines)).strip() + + args_sections = [] + for line in args_content.splitlines(keepends=True): + if not line[:1].isspace(): + # This line is the start of documentation for the next + # parameter because it doesn't start with any whitespace. + args_sections.append(line) + else: + # This is a continuation of documentation for the last + # parameter because it does start with whitespace. + args_sections[-1] += line + + for section in args_sections: + match = ConventionChecker.GOOGLE_ARGS_REGEX.match(section) if match: docstring_args.add(match.group(1)) yield from ConventionChecker._check_missing_args( diff --git a/src/pydocstyle/violations.py b/src/pydocstyle/violations.py index 60fc064e..8156921a 100644 --- a/src/pydocstyle/violations.py +++ b/src/pydocstyle/violations.py @@ -415,6 +415,10 @@ def to_rst(cls) -> str: 'D418', 'Function/ Method decorated with @overload shouldn\'t contain a docstring', ) +D419 = D4xx.create_error( + 'D419', + 'Docstring is empty', +) class AttrDict(dict): diff --git a/src/tests/test_cases/capitalization.py b/src/tests/test_cases/capitalization.py index 91ecf45c..a15c79f3 100644 --- a/src/tests/test_cases/capitalization.py +++ b/src/tests/test_cases/capitalization.py @@ -13,7 +13,7 @@ def not_capitalized(): # Make sure empty docstrings don't generate capitalization errors. -@expect("D103: Missing docstring in public function") +@expect("D419: Docstring is empty") def empty_docstring(): """""" diff --git a/src/tests/test_cases/sections.py b/src/tests/test_cases/sections.py index d671102b..b4932606 100644 --- a/src/tests/test_cases/sections.py +++ b/src/tests/test_cases/sections.py @@ -367,10 +367,7 @@ def test_missing_docstring(a, b): # noqa: D213, D407 """ @staticmethod - @expect("D417: Missing argument descriptions in the docstring " - "(argument(s) skip, verbose are missing descriptions in " - "'test_missing_docstring_another' docstring)", arg_count=2) - def test_missing_docstring_another(skip, verbose): # noqa: D213, D407 + def test_hanging_indent(skip, verbose): # noqa: D213, D407 """Do stuff. Args: diff --git a/src/tests/test_cases/test.py b/src/tests/test_cases/test.py index 8154c960..1cbd8ec4 100644 --- a/src/tests/test_cases/test.py +++ b/src/tests/test_cases/test.py @@ -13,7 +13,7 @@ class class_: - expect('meta', 'D106: Missing docstring in public nested class') + expect('meta', 'D419: Docstring is empty') class meta: """""" @@ -64,13 +64,13 @@ def __call__(self=None, x=None, y=None, z=None): pass -@expect('D103: Missing docstring in public function') +@expect('D419: Docstring is empty') def function(): """ """ def ok_since_nested(): pass - @expect('D103: Missing docstring in public function') + @expect('D419: Docstring is empty') def nested(): ''