From ae708c4cf78d6c04763ee2dc744c490c0aab9392 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 22 Nov 2021 17:02:43 -0600 Subject: [PATCH 1/7] inaccessible --- src/pydocstyle/checker.py | 6 ++++-- src/pydocstyle/parser.py | 27 ++++++++++++++++++++----- src/pydocstyle/violations.py | 8 ++++++++ src/tests/parser_test.py | 38 ++++++++++++++++++++++++++++-------- 4 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index be74867f..ff4876f9 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -14,10 +14,11 @@ Class, Definition, Function, + InaccessibleClass, + InaccessibleFunction, Method, Module, NestedClass, - NestedFunction, Package, ParseError, Parser, @@ -206,6 +207,7 @@ def check_docstring_missing(self, definition, docstring): Module: violations.D100, Class: violations.D101, NestedClass: violations.D106, + # InaccessibleClass: violations.D121, # currently Method: lambda: violations.D105() if definition.is_magic else ( @@ -217,7 +219,7 @@ def check_docstring_missing(self, definition, docstring): else None ) ), - NestedFunction: violations.D103, + InaccessibleFunction: violations.D109, Function: ( lambda: violations.D103() if not definition.is_overload diff --git a/src/pydocstyle/parser.py b/src/pydocstyle/parser.py index 7165767a..fc1be5dc 100644 --- a/src/pydocstyle/parser.py +++ b/src/pydocstyle/parser.py @@ -17,10 +17,11 @@ 'Module', 'Package', 'Function', - 'NestedFunction', + 'InaccessibleFunction' 'Method', 'Class', 'NestedClass', + 'InaccessibleClass', 'AllError', 'StringIO', 'ParseError', @@ -199,8 +200,9 @@ class Function(Definition): """A Python source code function.""" _nest = staticmethod( - lambda s: {'def': NestedFunction, 'class': NestedClass}[s] + lambda s: {'def': InaccessibleFunction, 'class': InaccessibleClass}[s] ) + is_accessible = True @property def is_public(self): @@ -236,11 +238,18 @@ def is_test(self): return self.name.startswith('test') or self.name == 'runTest' -class NestedFunction(Function): - """A Python source code nested function.""" +class InaccessibleFunction(Function): + """A Python source code function which is inaccessible. - is_public = False + A function is inaccessible if it is defined inside another function. + Publicness is still evaluated based on the name, to allow devs to signal between public and + private if they so wish. (E.g. if a function returns another function, they may want the inner + function to be documented. Conversely a purely helper inner function might not need to be + documented) + """ + + is_accessible = False class Method(Function): """A Python source code method.""" @@ -289,6 +298,7 @@ class Class(Definition): _nest = staticmethod(lambda s: {'def': Method, 'class': NestedClass}[s]) is_public = Function.is_public is_class = True + is_accessible = True class NestedClass(Class): @@ -303,6 +313,13 @@ def is_public(self): and self.parent.is_public ) +class InaccessibleClass(Class): + """A Python source code class, which is inaccessible. + + An class is inaccessible if it is defined inside of a function. + """ + + is_accessible = False class Decorator(Value): """A decorator for function, method or class.""" diff --git a/src/pydocstyle/violations.py b/src/pydocstyle/violations.py index 60fc064e..c88930f9 100644 --- a/src/pydocstyle/violations.py +++ b/src/pydocstyle/violations.py @@ -222,6 +222,14 @@ def to_rst(cls) -> str: 'D107', 'Missing docstring in __init__', ) +D121 = D1xx.create_error( + 'D121', + 'Missing docstring in inaccessible class', +) +D123 = D1xx.create_error( + 'D123', + 'Missing docstring in inaccessible function', +) D2xx = ErrorRegistry.create_group('D2', 'Whitespace Issues') D200 = D2xx.create_error( diff --git a/src/tests/parser_test.py b/src/tests/parser_test.py index 582c6cde..515118f2 100644 --- a/src/tests/parser_test.py +++ b/src/tests/parser_test.py @@ -47,6 +47,7 @@ def do_something(pos_param0, pos_param1, kw_param0="default"): assert function.error_lineno == 2 assert function.source == code.getvalue() assert function.is_public + assert function.is_accessible assert str(function) == 'in public function `do_something`' @@ -76,6 +77,7 @@ def do_something(pos_param0, pos_param1, kw_param0="default"): assert function.error_lineno == 2 assert function.source == code.getvalue() assert function.is_public + assert function.is_accessible assert str(function) == 'in public function `do_something`' @@ -111,6 +113,7 @@ def do_something(pos_param0, pos_param1, kw_param0="default"): return None """) assert function.is_public + assert function.is_accessible assert str(function) == 'in public function `do_something`' @@ -140,6 +143,7 @@ def do_something(): return None """) assert function.is_public + assert function.is_accessible assert str(function) == 'in public function `do_something`' @@ -167,6 +171,7 @@ def inner_function(): assert outer_function.error_lineno == 2 assert outer_function.source == code.getvalue() assert outer_function.is_public + assert outer_function.is_accessible assert str(outer_function) == 'in public function `outer_function`' inner_function, = outer_function.children @@ -183,8 +188,9 @@ def inner_function(): '''This is the inner function.''' return None """) - assert not inner_function.is_public - assert str(inner_function) == 'in private nested function `inner_function`' + assert inner_function.is_public + assert not inner_function.is_accessible + assert str(inner_function) == 'in public inaccessible function `inner_function`' def test_conditional_nested_function(): @@ -211,6 +217,7 @@ def inner_function(): assert outer_function.end == 7 assert outer_function.source == code.getvalue() assert outer_function.is_public + assert outer_function.is_accessible assert str(outer_function) == 'in public function `outer_function`' inner_function, = outer_function.children @@ -226,8 +233,9 @@ def inner_function(): '''This is the inner function.''' return None """) - assert not inner_function.is_public - assert str(inner_function) == 'in private nested function `inner_function`' + assert inner_function.is_public + assert not inner_function.is_accessible + assert str(inner_function) == 'in public inaccessible function `inner_function`' def test_doubly_nested_function(): @@ -254,6 +262,7 @@ def inner_function(): assert outer_function.end == 7 assert outer_function.source == code.getvalue() assert outer_function.is_public + assert outer_function.is_accessible assert str(outer_function) == 'in public function `outer_function`' middle_function, = outer_function.children @@ -270,9 +279,10 @@ def inner_function(): '''This is the inner function.''' return None """) - assert not middle_function.is_public + assert middle_function.is_public + assert not middle_function.is_accessible assert (str(middle_function) == - 'in private nested function `middle_function`') + 'in public inaccessible function `middle_function`') inner_function, = middle_function.children assert inner_function.name == 'inner_function' @@ -287,9 +297,12 @@ def inner_function(): '''This is the inner function.''' return None """) - assert not inner_function.is_public - assert str(inner_function) == 'in private nested function `inner_function`' + assert inner_function.is_public + assert not inner_function.is_accessible + assert str(inner_function) == 'in public inaccessible function `inner_function`' + # @TODO: Test private inaccessible function + # @TODO: Test public/private inaccessible classes def test_class(): """Test parsing of a class.""" @@ -313,6 +326,7 @@ class TestedClass(object): assert klass.error_lineno == 3 assert klass.source == code.getvalue() assert klass.is_public + assert klass.is_accessible assert str(klass) == 'in public class `TestedClass`' @@ -339,6 +353,7 @@ def do_it(param): assert klass.error_lineno == 1 assert klass.source == code.getvalue() assert klass.is_public + assert klass.is_accessible assert str(klass) == 'in public class `TestedClass`' method, = klass.children @@ -357,6 +372,7 @@ def do_it(param): return None """) assert method.is_public + assert method.is_accessible assert not method.is_magic assert str(method) == 'in public method `do_it`' @@ -384,6 +400,7 @@ def _do_it(param): assert klass.error_lineno == 1 assert klass.source == code.getvalue() assert klass.is_public + assert klass.is_accessible assert str(klass) == 'in public class `TestedClass`' method, = klass.children @@ -402,6 +419,7 @@ def _do_it(param): return None """) assert not method.is_public + assert method.is_accessible assert not method.is_magic assert str(method) == 'in private method `_do_it`' @@ -427,6 +445,7 @@ def __str__(self): assert klass.error_lineno == 1 assert klass.source == code.getvalue() assert klass.is_public + assert klass.is_accessible assert str(klass) == 'in public class `TestedClass`' method, = klass.children[0] @@ -443,6 +462,7 @@ def __str__(self): return "me" """) assert method.is_public + assert method.is_accessible assert method.is_magic assert str(method) == 'in public method `__str__`' @@ -469,6 +489,7 @@ class InnerClass(object): assert outer_class.error_lineno == 2 assert outer_class.source == code.getvalue() assert outer_class.is_public + assert outer_class.is_accessible assert str(outer_class) == 'in public class `OuterClass`' inner_class, = outer_class.children @@ -486,6 +507,7 @@ class InnerClass(object): "An inner docstring." """) assert inner_class.is_public + assert inner_class.is_accessible assert str(inner_class) == 'in public nested class `InnerClass`' From 29bde244c61a1caac1d482281bcf59e35b9a816e Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 22 Nov 2021 19:45:28 -0600 Subject: [PATCH 2/7] inaccessible --- src/pydocstyle/checker.py | 4 ++-- src/pydocstyle/config.py | 11 +++++++---- src/pydocstyle/parser.py | 6 ++++-- src/pydocstyle/violations.py | 4 ++-- src/tests/test_cases/functions.py | 13 +++++++++---- src/tests/test_cases/test.py | 6 ++++-- src/tests/test_integration.py | 2 +- 7 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index ff4876f9..ad2a4a62 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -207,7 +207,7 @@ def check_docstring_missing(self, definition, docstring): Module: violations.D100, Class: violations.D101, NestedClass: violations.D106, - # InaccessibleClass: violations.D121, # currently + InaccessibleClass: violations.D121, Method: lambda: violations.D105() if definition.is_magic else ( @@ -219,7 +219,7 @@ def check_docstring_missing(self, definition, docstring): else None ) ), - InaccessibleFunction: violations.D109, + InaccessibleFunction: violations.D123, Function: ( lambda: violations.D103() if not definition.is_overload diff --git a/src/pydocstyle/config.py b/src/pydocstyle/config.py index fe3afb3f..0d6d0da7 100644 --- a/src/pydocstyle/config.py +++ b/src/pydocstyle/config.py @@ -183,6 +183,7 @@ class ConfigurationParser: ) BASE_ERROR_SELECTION_OPTIONS = ('ignore', 'select', 'convention') + DEFAULT_IGNORE = {"D121", "D123"} DEFAULT_MATCH_RE = r'(?!test_).*\.py' DEFAULT_MATCH_DIR_RE = r'[^\.].*' DEFAULT_IGNORE_DECORATORS_RE = '' @@ -594,10 +595,12 @@ def _get_exclusive_error_codes(cls, options): if options.ignore is not None: ignored = cls._expand_error_codes(options.ignore) checked_codes = codes - ignored - elif options.select is not None: - checked_codes = cls._expand_error_codes(options.select) - elif options.convention is not None: - checked_codes = getattr(conventions, options.convention) + else: + codes -= cls.DEFAULT_IGNORE + if options.select is not None: + checked_codes = cls._expand_error_codes(options.select) + elif options.convention is not None: + checked_codes = getattr(conventions, options.convention) # To not override the conventions nor the options - copy them. return copy.deepcopy(checked_codes) diff --git a/src/pydocstyle/parser.py b/src/pydocstyle/parser.py index fc1be5dc..86bc1619 100644 --- a/src/pydocstyle/parser.py +++ b/src/pydocstyle/parser.py @@ -17,8 +17,7 @@ 'Module', 'Package', 'Function', - 'InaccessibleFunction' - 'Method', + 'InaccessibleFunction' 'Method', 'Class', 'NestedClass', 'InaccessibleClass', @@ -251,6 +250,7 @@ class InaccessibleFunction(Function): is_accessible = False + class Method(Function): """A Python source code method.""" @@ -313,6 +313,7 @@ def is_public(self): and self.parent.is_public ) + class InaccessibleClass(Class): """A Python source code class, which is inaccessible. @@ -321,6 +322,7 @@ class InaccessibleClass(Class): is_accessible = False + class Decorator(Value): """A decorator for function, method or class.""" diff --git a/src/pydocstyle/violations.py b/src/pydocstyle/violations.py index c88930f9..de799026 100644 --- a/src/pydocstyle/violations.py +++ b/src/pydocstyle/violations.py @@ -224,11 +224,11 @@ def to_rst(cls) -> str: ) D121 = D1xx.create_error( 'D121', - 'Missing docstring in inaccessible class', + 'Missing docstring in inaccessible public class', ) D123 = D1xx.create_error( 'D123', - 'Missing docstring in inaccessible function', + 'Missing docstring in inaccessible public function', ) D2xx = ErrorRegistry.create_group('D2', 'Whitespace Issues') diff --git a/src/tests/test_cases/functions.py b/src/tests/test_cases/functions.py index db7b9fab..c6b0ded9 100644 --- a/src/tests/test_cases/functions.py +++ b/src/tests/test_cases/functions.py @@ -28,15 +28,17 @@ def inner(): pass +expect("inner", "D123: Missing docstring in inaccessible public function") def func_with_inner_async_func_after(): """Test a function with inner async function after docstring.""" - async def inner(): + async def inner_async(): pass pass +expect("inner_async", "D123: Missing docstring in inaccessible public function") def fake_decorator(decorated): """Fake decorator used to test decorated inner func.""" @@ -47,30 +49,33 @@ def func_with_inner_decorated_func_after(): """Test a function with inner decorated function after docstring.""" @fake_decorator - def inner(): + def inner_decorated(): pass pass +expect("inner_decorated", "D123: Missing docstring in inaccessible public function") def func_with_inner_decorated_async_func_after(): """Test a function with inner decorated async function after docstring.""" @fake_decorator - async def inner(): + async def inner_decorated_async(): pass pass +expect("inner_decorated_async", "D123: Missing docstring in inaccessible public function") def func_with_inner_class_after(): """Test a function with inner class after docstring.""" - class inner(): + class inner_class(): pass pass +expect("inner_class", "D121: Missing docstring in inaccessible public class") def func_with_weird_backslash(): """Test a function with a weird backslash.\ diff --git a/src/tests/test_cases/test.py b/src/tests/test_cases/test.py index 8154c960..a2afd1ac 100644 --- a/src/tests/test_cases/test.py +++ b/src/tests/test_cases/test.py @@ -67,10 +67,11 @@ def __call__(self=None, x=None, y=None, z=None): @expect('D103: Missing docstring in public function') def function(): """ """ + @expect('D123: Missing docstring in inaccessible public function') def ok_since_nested(): pass - @expect('D103: Missing docstring in public function') + @expect('D123: Missing docstring in inaccessible public function') def nested(): '' @@ -94,7 +95,8 @@ def nested_overloaded_func(a): expect('nested_overloaded_func', "D418: Function/ Method decorated with @overload" " shouldn't contain a docstring") - +expect('nested_overloaded_func', + "D123: Missing docstring in inaccessible public function") @overload def overloaded_func(a: int) -> str: diff --git a/src/tests/test_integration.py b/src/tests/test_integration.py index 7e399cce..1f5a88a6 100644 --- a/src/tests/test_integration.py +++ b/src/tests/test_integration.py @@ -769,7 +769,7 @@ def overloaded_func(a): """Foo bar documentation.""" return str(a) ''')) - env.write_config(ignore="D100") + env.write_config(ignore="D100,D123") out, err, code = env.invoke() assert code == 0 From 1584eb879eb1c63b95bb74340c8ef00d09406cb7 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 22 Nov 2021 19:48:09 -0600 Subject: [PATCH 3/7] class --- src/pydocstyle/parser.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pydocstyle/parser.py b/src/pydocstyle/parser.py index 86bc1619..d36372cb 100644 --- a/src/pydocstyle/parser.py +++ b/src/pydocstyle/parser.py @@ -318,6 +318,11 @@ class InaccessibleClass(Class): """A Python source code class, which is inaccessible. An class is inaccessible if it is defined inside of a function. + + Publicness is still evaluated based on the name, to allow devs to signal between public and + private if they so wish. (E.g. if a function returns a class, they may want the returned + class to be documented. Conversely a purely helper inner class might not need to be + documented) """ is_accessible = False From a8a25f205ebbb24dee781bbbe28c5b7d82e52fd2 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Mon, 22 Nov 2021 20:19:52 -0600 Subject: [PATCH 4/7] more tests --- src/tests/parser_test.py | 128 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/src/tests/parser_test.py b/src/tests/parser_test.py index 515118f2..8b4b2905 100644 --- a/src/tests/parser_test.py +++ b/src/tests/parser_test.py @@ -301,8 +301,49 @@ def inner_function(): assert not inner_function.is_accessible assert str(inner_function) == 'in public inaccessible function `inner_function`' - # @TODO: Test private inaccessible function - # @TODO: Test public/private inaccessible classes +def test_private_nested_function(): + """Test parsing of a nested function which looks private.""" + parser = Parser() + code = CodeSnippet("""\ + def outer_function(): + \"""This is the outer function.\""" + if True: + def _inner_function(): + '''This is the inner function.''' + return None + return None + """) + module = parser.parse(code, 'file_path') + + outer_function, = module.children + assert outer_function.name == 'outer_function' + assert outer_function.decorators == [] + assert outer_function.docstring == '"""This is the outer function."""' + assert outer_function.kind == 'function' + assert outer_function.parent == module + assert outer_function.start == 1 + assert outer_function.end == 7 + assert outer_function.source == code.getvalue() + assert outer_function.is_public + assert outer_function.is_accessible + assert str(outer_function) == 'in public function `outer_function`' + + inner_function, = outer_function.children + assert inner_function.name == '_inner_function' + assert inner_function.decorators == [] + assert inner_function.docstring == "'''This is the inner function.'''" + assert inner_function.kind == 'function' + assert inner_function.parent == outer_function + assert inner_function.start == 4 + assert inner_function.end == 6 + assert textwrap.dedent(inner_function.source) == textwrap.dedent("""\ + def _inner_function(): + '''This is the inner function.''' + return None + """) + assert not inner_function.is_public + assert not inner_function.is_accessible + assert str(inner_function) == 'in private inaccessible function `_inner_function`' def test_class(): """Test parsing of a class.""" @@ -510,6 +551,89 @@ class InnerClass(object): assert inner_class.is_accessible assert str(inner_class) == 'in public nested class `InnerClass`' +def test_public_inaccessible_class(): + """Test parsing of a class inside a function.""" + parser = Parser() + code = CodeSnippet("""\ + def outer_function(): + ' an outer docstring' + class InnerClass(object): + "An inner docstring." + """) + module = parser.parse(code, 'file_path') + + outer_function, = module.children + assert outer_function.name == 'outer_function' + assert outer_function.decorators == [] + assert outer_function.docstring == "' an outer docstring'" + assert outer_function.kind == 'function' + assert outer_function.parent == module + assert outer_function.start == 1 + assert outer_function.end == 4 + assert outer_function.source == code.getvalue() + assert outer_function.is_public + assert outer_function.is_accessible + assert str(outer_function) == 'in public function `outer_function`' + + inner_class, = outer_function.children + assert inner_class.name == 'InnerClass' + assert inner_class.decorators == [] + assert inner_class.children == [] + assert inner_class.docstring == '"An inner docstring."' + assert inner_class.kind == 'class' + assert inner_class.parent == outer_function + assert inner_class.start == 3 + assert inner_class.end == 4 + assert inner_class.error_lineno == 4 + assert textwrap.dedent(inner_class.source) == textwrap.dedent("""\ + class InnerClass(object): + "An inner docstring." + """) + assert inner_class.is_public + assert not inner_class.is_accessible + assert str(inner_class) == 'in public inaccessible class `InnerClass`' + +def test_private_inaccessible_class(): + """Test parsing of a class inside a function which looks private.""" + parser = Parser() + code = CodeSnippet("""\ + def outer_function(): + ' an outer docstring' + class _InnerClass(object): + "An inner docstring." + """) + module = parser.parse(code, 'file_path') + + outer_function, = module.children + assert outer_function.name == 'outer_function' + assert outer_function.decorators == [] + assert outer_function.docstring == "' an outer docstring'" + assert outer_function.kind == 'function' + assert outer_function.parent == module + assert outer_function.start == 1 + assert outer_function.end == 4 + assert outer_function.source == code.getvalue() + assert outer_function.is_public + assert outer_function.is_accessible + assert str(outer_function) == 'in public function `outer_function`' + + inner_class, = outer_function.children + assert inner_class.name == '_InnerClass' + assert inner_class.decorators == [] + assert inner_class.children == [] + assert inner_class.docstring == '"An inner docstring."' + assert inner_class.kind == 'class' + assert inner_class.parent == outer_function + assert inner_class.start == 3 + assert inner_class.end == 4 + assert inner_class.error_lineno == 4 + assert textwrap.dedent(inner_class.source) == textwrap.dedent("""\ + class _InnerClass(object): + "An inner docstring." + """) + assert not inner_class.is_public + assert not inner_class.is_accessible + assert str(inner_class) == 'in private inaccessible class `_InnerClass`' def test_raise_from(): """Make sure 'raise x from y' doesn't trip the parser.""" From 127990ff2171ba0fe723b0812ff0e80fe59fb806 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Tue, 23 Nov 2021 08:33:42 -0600 Subject: [PATCH 5/7] private --- src/pydocstyle/checker.py | 37 +++++++++++++-------------- src/pydocstyle/config.py | 2 ++ src/pydocstyle/violations.py | 24 +++++++++++++++++ src/tests/test_cases/all_import_as.py | 2 +- src/tests/test_cases/functions.py | 24 +++++++++++++---- src/tests/test_cases/nested_class.py | 12 +++++++++ src/tests/test_cases/sections.py | 2 +- src/tests/test_cases/test.py | 24 ++++++++++++++--- 8 files changed, 98 insertions(+), 29 deletions(-) diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index be74867f..ebcd0aba 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -184,7 +184,7 @@ def checks(self): @check_for(Definition, terminal=True) def check_docstring_missing(self, definition, docstring): - """D10{0,1,2,3}: Public definitions should have docstrings. + """D1XX: Definitions should have docstrings. All modules should normally have docstrings. [...] all functions and classes exported by a module should also have docstrings. Public @@ -196,36 +196,35 @@ def check_docstring_missing(self, definition, docstring): with a single underscore. """ + + def violation(code): + code = code if definition.is_public else code + 50 + return getattr(violations, f"D{code}")() + if ( not docstring - and definition.is_public or docstring and is_blank(ast.literal_eval(docstring)) ): codes = { - Module: violations.D100, - Class: violations.D101, - NestedClass: violations.D106, - Method: lambda: violations.D105() + Module: lambda: 100, + Class: lambda: 101, + NestedClass: lambda: 106, + Method: lambda: 105 if definition.is_magic else ( - violations.D107() + 107 if definition.is_init - else ( - violations.D102() - if not definition.is_overload - else None - ) + else (102 if not definition.is_overload else None) ), - NestedFunction: violations.D103, - Function: ( - lambda: violations.D103() - if not definition.is_overload - else None + NestedFunction: lambda: 103, + Function: lambda: ( + 103 if not definition.is_overload else None ), - Package: violations.D104, + Package: lambda: 104, } - return codes[type(definition)]() + code = codes[type(definition)]() + return violation(code) if code is not None else None @check_for(Definition) def check_one_liners(self, definition, docstring): diff --git a/src/pydocstyle/config.py b/src/pydocstyle/config.py index fe3afb3f..74f5683a 100644 --- a/src/pydocstyle/config.py +++ b/src/pydocstyle/config.py @@ -183,6 +183,7 @@ class ConfigurationParser: ) BASE_ERROR_SELECTION_OPTIONS = ('ignore', 'select', 'convention') + DEFAULT_IGNORE="D15" DEFAULT_MATCH_RE = r'(?!test_).*\.py' DEFAULT_MATCH_DIR_RE = r'[^\.].*' DEFAULT_IGNORE_DECORATORS_RE = '' @@ -544,6 +545,7 @@ def _create_check_config(cls, options, use_defaults=True): kwargs = dict(checked_codes=checked_codes) defaults = { + 'ignore': 'IGNORE', 'match': "MATCH_RE", 'match_dir': "MATCH_DIR_RE", 'ignore_decorators': "IGNORE_DECORATORS_RE", diff --git a/src/pydocstyle/violations.py b/src/pydocstyle/violations.py index 60fc064e..0d9d25f3 100644 --- a/src/pydocstyle/violations.py +++ b/src/pydocstyle/violations.py @@ -222,6 +222,30 @@ def to_rst(cls) -> str: 'D107', 'Missing docstring in __init__', ) +D190 = D1xx.create_error( + 'D150', + 'Missing docstring in private module', +) +D191 = D1xx.create_error( + 'D151', + 'Missing docstring in private class', +) +D192 = D1xx.create_error( + 'D152', + 'Missing docstring in private method', +) +D193 = D1xx.create_error( + 'D153', + 'Missing docstring in private function', +) +D194 = D1xx.create_error( + 'D154', + 'Missing docstring in private package', +) +D196 = D1xx.create_error( + 'D156', + 'Missing docstring in private nested class', +) D2xx = ErrorRegistry.create_group('D2', 'Whitespace Issues') D200 = D2xx.create_error( diff --git a/src/tests/test_cases/all_import_as.py b/src/tests/test_cases/all_import_as.py index ea25ba62..069b9128 100644 --- a/src/tests/test_cases/all_import_as.py +++ b/src/tests/test_cases/all_import_as.py @@ -13,6 +13,6 @@ def public_func(): pass - +@expect("D153: Missing docstring in private function") def private_func(): pass diff --git a/src/tests/test_cases/functions.py b/src/tests/test_cases/functions.py index db7b9fab..bd2c6acc 100644 --- a/src/tests/test_cases/functions.py +++ b/src/tests/test_cases/functions.py @@ -20,19 +20,24 @@ def func_with_space_after(): pass +@expect() def func_with_inner_func_after(): """Test a function with inner function after docstring.""" - def inner(): + @expect("D193: Missing docstring in private function") + def inner_func(): pass pass + +@expect() def func_with_inner_async_func_after(): """Test a function with inner async function after docstring.""" - async def inner(): + @expect("D193: Missing docstring in private function") + async def inner_async_func(): pass pass @@ -43,30 +48,39 @@ def fake_decorator(decorated): return decorated +@expect() def func_with_inner_decorated_func_after(): """Test a function with inner decorated function after docstring.""" @fake_decorator - def inner(): + @expect("D193: Missing docstring in private function") + def inner_decorated_func(): pass pass + + +@expect() def func_with_inner_decorated_async_func_after(): """Test a function with inner decorated async function after docstring.""" @fake_decorator - async def inner(): + @expect("D193: Missing docstring in private function") + async def inner_decorated_async_func(): pass pass + +@expect() def func_with_inner_class_after(): """Test a function with inner class after docstring.""" + expect("inner_class", "D196: Missing docstring in private nested class") - class inner(): + class inner_class(): pass pass diff --git a/src/tests/test_cases/nested_class.py b/src/tests/test_cases/nested_class.py index 07257918..66a05f03 100644 --- a/src/tests/test_cases/nested_class.py +++ b/src/tests/test_cases/nested_class.py @@ -21,13 +21,25 @@ class PublicNestedClass: class PublicNestedClassInPublicNestedClass: pass + expect('_PrivateNestedClassInPublicNestedClass', + 'D196: Missing docstring in private nested class') + class _PrivateNestedClassInPublicNestedClass: pass + expect('_PrivateNestedClass', + 'D196: Missing docstring in private nested class') + class _PrivateNestedClass: + expect('PublicNestedClassInPrivateNestedClass', + 'D196: Missing docstring in private nested class') + class PublicNestedClassInPrivateNestedClass: pass + expect('_PrivateNestedClassInPrivateNestedClass', + 'D196: Missing docstring in private nested class') + class _PrivateNestedClassInPrivateNestedClass: pass diff --git a/src/tests/test_cases/sections.py b/src/tests/test_cases/sections.py index d671102b..02bef7cb 100644 --- a/src/tests/test_cases/sections.py +++ b/src/tests/test_cases/sections.py @@ -277,7 +277,7 @@ def missing_colon_google_style_section(): # noqa: D406, D407 @expect("D417: Missing argument descriptions in the docstring " "(argument(s) y are missing descriptions in " "'bar' docstring)", func_name="bar") -def _test_nested_functions(): +def _test_nested_functions(): # noqa: D193 x = 1 def bar(y=2): # noqa: D207, D213, D406, D407 diff --git a/src/tests/test_cases/test.py b/src/tests/test_cases/test.py index 8154c960..31b964ff 100644 --- a/src/tests/test_cases/test.py +++ b/src/tests/test_cases/test.py @@ -22,7 +22,16 @@ class meta: def method(self=None): pass - def _ok_since_private(self=None): + @expect('D192: Missing docstring in private method') + def _private_method(self=None): + pass + + @expect('D192: Missing docstring in private method') + def __private_mangled_method(self=None): + pass + + @expect('D192: Missing docstring in private method') + def _private_fancy_method_(self=None): pass @overload @@ -67,17 +76,19 @@ def __call__(self=None, x=None, y=None, z=None): @expect('D103: Missing docstring in public function') def function(): """ """ + @expect("D193: Missing docstring in private function") def ok_since_nested(): pass - @expect('D103: Missing docstring in public function') + @expect('D193: Missing docstring in private function') def nested(): '' - +@expect() def function_with_nesting(): """Foo bar documentation.""" @overload + @expect("D193: Missing docstring in private function", arg_count=1) def nested_overloaded_func(a: int) -> str: ... @@ -532,3 +543,10 @@ def __init__(self, x): expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]), 'D100: Missing docstring in public module') + +expect('_PrivateClass', 'D191: Missing docstring in private class') + +class _PrivateClass: + pass + + From eee5ceff67f839c32431dcdd1b4167be3a083291 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Tue, 23 Nov 2021 09:11:10 -0600 Subject: [PATCH 6/7] wrap it up --- src/pydocstyle/checker.py | 4 +-- src/pydocstyle/config.py | 6 ++-- src/pydocstyle/parser.py | 3 +- src/pydocstyle/violations.py | 28 +++++++++++++-- src/tests/test_cases/functions.py | 52 ++++++++++++++++++---------- src/tests/test_cases/nested_class.py | 8 ++--- src/tests/test_cases/sections.py | 2 +- src/tests/test_cases/test.py | 32 +++++++++++++---- 8 files changed, 95 insertions(+), 40 deletions(-) diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index 99ecccf3..eb768787 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -221,9 +221,7 @@ def violation(code): ), InaccessibleFunction: lambda: 123, Function: ( - lambda: 103 - if not definition.is_overload - else None + lambda: 103 if not definition.is_overload else None ), Package: lambda: 104, } diff --git a/src/pydocstyle/config.py b/src/pydocstyle/config.py index e2d0696e..99bea4dc 100644 --- a/src/pydocstyle/config.py +++ b/src/pydocstyle/config.py @@ -11,7 +11,7 @@ from re import compile as re from .utils import __version__, log -from .violations import ErrorRegistry, conventions +from .violations import ErrorRegistry, conventions, default_ignored try: import toml @@ -183,7 +183,6 @@ class ConfigurationParser: ) BASE_ERROR_SELECTION_OPTIONS = ('ignore', 'select', 'convention') - DEFAULT_IGNORE = {"D121", "D123", "D150", "D151", "D152", "D153", "D154", "D156", "D171", "D173"} DEFAULT_MATCH_RE = r'(?!test_).*\.py' DEFAULT_MATCH_DIR_RE = r'[^\.].*' DEFAULT_IGNORE_DECORATORS_RE = '' @@ -545,7 +544,6 @@ def _create_check_config(cls, options, use_defaults=True): kwargs = dict(checked_codes=checked_codes) defaults = { - 'ignore': 'IGNORE', 'match': "MATCH_RE", 'match_dir': "MATCH_DIR_RE", 'ignore_decorators': "IGNORE_DECORATORS_RE", @@ -597,7 +595,7 @@ def _get_exclusive_error_codes(cls, options): ignored = cls._expand_error_codes(options.ignore) checked_codes = codes - ignored else: - codes -= cls.DEFAULT_IGNORE + codes -= default_ignored if options.select is not None: checked_codes = cls._expand_error_codes(options.select) elif options.convention is not None: diff --git a/src/pydocstyle/parser.py b/src/pydocstyle/parser.py index d36372cb..5b2da36c 100644 --- a/src/pydocstyle/parser.py +++ b/src/pydocstyle/parser.py @@ -17,7 +17,8 @@ 'Module', 'Package', 'Function', - 'InaccessibleFunction' 'Method', + 'InaccessibleFunction', + 'Method', 'Class', 'NestedClass', 'InaccessibleClass', diff --git a/src/pydocstyle/violations.py b/src/pydocstyle/violations.py index cb1b20cb..64d74b2e 100644 --- a/src/pydocstyle/violations.py +++ b/src/pydocstyle/violations.py @@ -230,7 +230,7 @@ def to_rst(cls) -> str: 'D123', 'Missing docstring in inaccessible public function', ) -# For private docstrings, we add 50 to the public flavors +# For private docstrings, we add 50 to the public codes D150 = D1xx.create_error( 'D150', 'Missing docstring in private module', @@ -251,10 +251,18 @@ def to_rst(cls) -> str: 'D154', 'Missing docstring in private package', ) +D155 = D1xx.create_error( + 'D155', + 'Missing docstring in private magic method', +) D156 = D1xx.create_error( 'D156', 'Missing docstring in private nested class', ) +D157 = D1xx.create_error( + 'D157', + 'Missing docstring in private __init__', +) D171 = D1xx.create_error( 'D171', 'Missing docstring in inaccessible private class', @@ -464,11 +472,25 @@ def __getattr__(self, item: str) -> Any: all_errors = set(ErrorRegistry.get_error_codes()) - +default_ignored = { + "D121", + "D123", + "D150", + "D151", + "D152", + "D153", + "D154", + "D155", + "D156", + "D157", + "D171", + "D173", +} conventions = AttrDict( { 'pep257': all_errors + - default_ignored - { 'D203', 'D212', @@ -490,6 +512,7 @@ def __getattr__(self, item: str) -> Any: 'D418', }, 'numpy': all_errors + - default_ignored - { 'D107', 'D203', @@ -502,6 +525,7 @@ def __getattr__(self, item: str) -> Any: 'D417', }, 'google': all_errors + - default_ignored - { 'D203', 'D204', diff --git a/src/tests/test_cases/functions.py b/src/tests/test_cases/functions.py index 6bf35087..b53b3522 100644 --- a/src/tests/test_cases/functions.py +++ b/src/tests/test_cases/functions.py @@ -20,73 +20,89 @@ def func_with_space_after(): pass -@expect() def func_with_inner_func_after(): """Test a function with inner function after docstring.""" - @expect("D193: Missing docstring in private function") - def inner_func(): + def public_inner(): + pass + + def _private_inner(): pass pass -expect("inner", "D123: Missing docstring in inaccessible public function") +expect("public_inner", "D123: Missing docstring in inaccessible public function") +expect("_private_inner", "D173: Missing docstring in inaccessible private function") -@expect() def func_with_inner_async_func_after(): """Test a function with inner async function after docstring.""" - async def inner_async(): + async def public_inner_async(): + pass + + async def _private_inner_async(): pass pass -expect("inner_async", "D123: Missing docstring in inaccessible public function") +expect("public_inner_async", "D123: Missing docstring in inaccessible public function") +expect("_private_inner_async", "D173: Missing docstring in inaccessible private function") + def fake_decorator(decorated): """Fake decorator used to test decorated inner func.""" return decorated -@expect() def func_with_inner_decorated_func_after(): """Test a function with inner decorated function after docstring.""" @fake_decorator - def inner_decorated(): + def public_inner_decorated(): pass - pass + @fake_decorator + def _private_inner_decorated(): + pass -expect("inner_decorated", "D123: Missing docstring in inaccessible public function") + pass +expect("public_inner_decorated", "D123: Missing docstring in inaccessible public function") +expect("_private_inner_decorated", "D173: Missing docstring in inaccessible private function") -@expect() def func_with_inner_decorated_async_func_after(): """Test a function with inner decorated async function after docstring.""" @fake_decorator - async def inner_decorated_async(): + async def public_inner_decorated_async(): + pass + + @fake_decorator + async def _prviate_inner_decorated_async(): pass pass -expect("inner_decorated_async", "D123: Missing docstring in inaccessible public function") +expect("public_inner_decorated_async", "D123: Missing docstring in inaccessible public function") +expect("_prviate_inner_decorated_async", "D173: Missing docstring in inaccessible private function") -@expect() def func_with_inner_class_after(): """Test a function with inner class after docstring.""" - expect("inner_class", "D196: Missing docstring in private nested class") - class inner_class(): + class public_inner_class(): + pass + + class _private_inner_class(): pass pass -expect("inner_class", "D121: Missing docstring in inaccessible public class") +expect("public_inner_class", "D121: Missing docstring in inaccessible public class") +expect("_private_inner_class", "D171: Missing docstring in inaccessible private class") + def func_with_weird_backslash(): """Test a function with a weird backslash.\ diff --git a/src/tests/test_cases/nested_class.py b/src/tests/test_cases/nested_class.py index 66a05f03..2865b780 100644 --- a/src/tests/test_cases/nested_class.py +++ b/src/tests/test_cases/nested_class.py @@ -22,24 +22,24 @@ class PublicNestedClassInPublicNestedClass: pass expect('_PrivateNestedClassInPublicNestedClass', - 'D196: Missing docstring in private nested class') + 'D156: Missing docstring in private nested class') class _PrivateNestedClassInPublicNestedClass: pass expect('_PrivateNestedClass', - 'D196: Missing docstring in private nested class') + 'D156: Missing docstring in private nested class') class _PrivateNestedClass: expect('PublicNestedClassInPrivateNestedClass', - 'D196: Missing docstring in private nested class') + 'D156: Missing docstring in private nested class') class PublicNestedClassInPrivateNestedClass: pass expect('_PrivateNestedClassInPrivateNestedClass', - 'D196: Missing docstring in private nested class') + 'D156: Missing docstring in private nested class') class _PrivateNestedClassInPrivateNestedClass: pass diff --git a/src/tests/test_cases/sections.py b/src/tests/test_cases/sections.py index 02bef7cb..9289cc92 100644 --- a/src/tests/test_cases/sections.py +++ b/src/tests/test_cases/sections.py @@ -277,7 +277,7 @@ def missing_colon_google_style_section(): # noqa: D406, D407 @expect("D417: Missing argument descriptions in the docstring " "(argument(s) y are missing descriptions in " "'bar' docstring)", func_name="bar") -def _test_nested_functions(): # noqa: D193 +def _test_nested_functions(): # noqa: D153 x = 1 def bar(y=2): # noqa: D207, D213, D406, D407 diff --git a/src/tests/test_cases/test.py b/src/tests/test_cases/test.py index e9708b1c..32385bbe 100644 --- a/src/tests/test_cases/test.py +++ b/src/tests/test_cases/test.py @@ -22,15 +22,15 @@ class meta: def method(self=None): pass - @expect('D192: Missing docstring in private method') + @expect('D152: Missing docstring in private method') def _private_method(self=None): pass - @expect('D192: Missing docstring in private method') + @expect('D152: Missing docstring in private method') def __private_mangled_method(self=None): pass - @expect('D192: Missing docstring in private method') + @expect('D152: Missing docstring in private method') def _private_fancy_method_(self=None): pass @@ -84,11 +84,10 @@ def ok_since_nested(): def nested(): '' -@expect() + def function_with_nesting(): """Foo bar documentation.""" @overload - @expect("D193: Missing docstring in private function", arg_count=1) def nested_overloaded_func(a: int) -> str: ... @@ -545,9 +544,28 @@ def __init__(self, x): expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]), 'D100: Missing docstring in public module') -expect('_PrivateClass', 'D191: Missing docstring in private class') +expect('_PrivateClass', 'D151: Missing docstring in private class') class _PrivateClass: - pass + @expect("D157: Missing docstring in private __init__") + def __init__(self=None): + pass + + @expect("D152: Missing docstring in private method") + def public_method_private_class(self=None): + pass + + @expect("D152: Missing docstring in private method") + def private_method_private_class(self=None): + pass + + @expect("D155: Missing docstring in private magic method") + def __str__(self=None): + pass + + class PublicNestedClass: + pass + + expect('PublicNestedClass', 'D156: Missing docstring in private nested class') From a35fa5b34b4ff27aa84e77d81ee5b2236ccb9d68 Mon Sep 17 00:00:00 2001 From: Joshua Cannon Date: Tue, 23 Nov 2021 09:14:59 -0600 Subject: [PATCH 7/7] release notes --- docs/release_notes.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 72332114..9575ac25 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -12,6 +12,7 @@ New Features * Add support for `property_decorators` config to ignore D401. * Add support for Python 3.10 (#554). +# Add reporting for private definitions (#562) 6.1.1 - May 17th, 2021 ---------------------------