From 3c39d2e09226450bb61914e1f6ded07a892f1055 Mon Sep 17 00:00:00 2001 From: jsh9 <25124332+jsh9@users.noreply.github.com> Date: Thu, 4 Jul 2024 19:26:16 -0700 Subject: [PATCH] Add an option for private class attributes (#149) --- CHANGELOG.md | 6 +++++ pydoclint/flake8_entry.py | 22 +++++++++++++++ pydoclint/main.py | 24 +++++++++++++++++ pydoclint/utils/visitor_helper.py | 27 +++++++++++++++++-- pydoclint/visitor.py | 7 +++++ .../11_private_class_attr/google.py | 16 +++++++++++ tests/test_main.py | 22 +++++++++++++++ 7 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 tests/data/edge_cases/11_private_class_attr/google.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b2cdac..45a4df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [unpublished] - 2024-07-04 + +- Added + - An option `--should-document-private-class-attributes` (if False, private + class attributes should not appear in the docstring) + ## [0.5.3] - 2024-06-26 - Changed diff --git a/pydoclint/flake8_entry.py b/pydoclint/flake8_entry.py index 2d1db15..a76bc1c 100644 --- a/pydoclint/flake8_entry.py +++ b/pydoclint/flake8_entry.py @@ -167,6 +167,18 @@ def add_options(cls, parser): # noqa: D102 ' "class MyClass:") are checked against the docstring.' ), ) + parser.add_option( + '-sdpca', + '--should-document-private-class-attributes', + action='store', + default='False', + parse_from_config=True, + help=( + 'If True, private class attributes (the ones starting with _)' + ' should be documented in the docstring. If False, private' + ' class attributes should not appear in the docstring.' + ), + ) @classmethod def parse_options(cls, options): # noqa: D102 @@ -193,6 +205,9 @@ def parse_options(cls, options): # noqa: D102 cls.check_yield_types = options.check_yield_types cls.ignore_underscore_args = options.ignore_underscore_args cls.check_class_attributes = options.check_class_attributes + cls.should_document_private_class_attributes = ( + options.should_document_private_class_attributes + ) cls.style = options.style def run(self) -> Generator[Tuple[int, int, str, Any], None, None]: @@ -262,6 +277,10 @@ def run(self) -> Generator[Tuple[int, int, str, Any], None, None]: '--check-class-attributes', self.check_class_attributes, ) + shouldDocumentPrivateClassAttributes = self._bool( + '--should-document-private-class-attributes', + self.should_document_private_class_attributes, + ) if self.style not in {'numpy', 'google', 'sphinx'}: raise ValueError( @@ -285,6 +304,9 @@ def run(self) -> Generator[Tuple[int, int, str, Any], None, None]: checkYieldTypes=checkYieldTypes, ignoreUnderscoreArgs=ignoreUnderscoreArgs, checkClassAttributes=checkClassAttributes, + shouldDocumentPrivateClassAttributes=( + shouldDocumentPrivateClassAttributes + ), style=self.style, ) v.visit(self._tree) diff --git a/pydoclint/main.py b/pydoclint/main.py index 7916add..628e4fc 100644 --- a/pydoclint/main.py +++ b/pydoclint/main.py @@ -206,6 +206,18 @@ def validateStyleValue( ' "class MyClass:") are checked against the docstring.' ), ) +@click.option( + '-sdpca', + '--should-document-private-class-attributes', + type=bool, + show_default=True, + default=False, + help=( + 'If True, private class attributes (the ones starting with _)' + ' should be documented in the docstring. If False, private' + ' class attributes should not appear in the docstring.' + ), +) @click.option( '--baseline', type=click.Path( @@ -291,6 +303,7 @@ def main( # noqa: C901 check_yield_types: bool, ignore_underscore_args: bool, check_class_attributes: bool, + should_document_private_class_attributes: bool, require_return_section_when_returning_none: bool, require_return_section_when_returning_nothing: bool, require_yield_section_when_yielding_nothing: bool, @@ -376,6 +389,9 @@ def main( # noqa: C901 checkYieldTypes=check_yield_types, ignoreUnderscoreArgs=ignore_underscore_args, checkClassAttributes=check_class_attributes, + shouldDocumentPrivateClassAttributes=( + should_document_private_class_attributes + ), requireReturnSectionWhenReturningNothing=( require_return_section_when_returning_nothing ), @@ -491,6 +507,7 @@ def _checkPaths( checkYieldTypes: bool = True, ignoreUnderscoreArgs: bool = True, checkClassAttributes: bool = True, + shouldDocumentPrivateClassAttributes: bool = False, requireReturnSectionWhenReturningNothing: bool = False, requireYieldSectionWhenYieldingNothing: bool = False, quiet: bool = False, @@ -537,6 +554,9 @@ def _checkPaths( checkYieldTypes=checkYieldTypes, ignoreUnderscoreArgs=ignoreUnderscoreArgs, checkClassAttributes=checkClassAttributes, + shouldDocumentPrivateClassAttributes=( + shouldDocumentPrivateClassAttributes + ), requireReturnSectionWhenReturningNothing=( requireReturnSectionWhenReturningNothing ), @@ -562,6 +582,7 @@ def _checkFile( checkYieldTypes: bool = True, ignoreUnderscoreArgs: bool = True, checkClassAttributes: bool = True, + shouldDocumentPrivateClassAttributes: bool = False, requireReturnSectionWhenReturningNothing: bool = False, requireYieldSectionWhenYieldingNothing: bool = False, ) -> List[Violation]: @@ -581,6 +602,9 @@ def _checkFile( checkYieldTypes=checkYieldTypes, ignoreUnderscoreArgs=ignoreUnderscoreArgs, checkClassAttributes=checkClassAttributes, + shouldDocumentPrivateClassAttributes=( + shouldDocumentPrivateClassAttributes + ), requireReturnSectionWhenReturningNothing=( requireReturnSectionWhenReturningNothing ), diff --git a/pydoclint/utils/visitor_helper.py b/pydoclint/utils/visitor_helper.py index 15c6dc2..f052161 100644 --- a/pydoclint/utils/visitor_helper.py +++ b/pydoclint/utils/visitor_helper.py @@ -36,9 +36,15 @@ def checkClassAttributesAgainstClassDocstring( argTypeHintsInSignature: bool, argTypeHintsInDocstring: bool, skipCheckingShortDocstrings: bool, + shouldDocumentPrivateClassAttributes: bool, ) -> None: """Check class attribute list against the attribute list in docstring""" - classAttributes = _collectClassAttributes(node) + classAttributes = _collectClassAttributes( + node=node, + shouldDocumentPrivateClassAttributes=( + shouldDocumentPrivateClassAttributes + ), + ) actualArgs: ArgList = _convertClassAttributesIntoArgList(classAttributes) classDocstring: str = getDocstring(node) @@ -113,7 +119,9 @@ def checkClassAttributesAgainstClassDocstring( def _collectClassAttributes( + *, node: ast.ClassDef, + shouldDocumentPrivateClassAttributes: bool, ) -> List[Union[ast.Assign, ast.AnnAssign]]: if 'body' not in node.__dict__ or len(node.body) == 0: return [] @@ -121,11 +129,26 @@ def _collectClassAttributes( attributes: List[Union[ast.Assign, ast.AnnAssign]] = [] for item in node.body: if isinstance(item, (ast.Assign, ast.AnnAssign)): - attributes.append(item) + classAttrName: str = _getClassAttrName(item) + if shouldDocumentPrivateClassAttributes: + attributes.append(item) + else: + if not classAttrName.startswith('_'): + attributes.append(item) return attributes +def _getClassAttrName(attrItem: Union[ast.Assign, ast.AnnAssign]) -> str: + if isinstance(attrItem, ast.Assign): + return attrItem.targets[0].id + + if isinstance(attrItem, ast.AnnAssign): + return attrItem.target.id + + raise InternalError(f'Unrecognized attrItem type: {type(attrItem)}') + + def _convertClassAttributesIntoArgList( classAttributes: List[Union[ast.Assign, ast.AnnAssign]], ) -> ArgList: diff --git a/pydoclint/visitor.py b/pydoclint/visitor.py index 7159901..a41ced4 100644 --- a/pydoclint/visitor.py +++ b/pydoclint/visitor.py @@ -59,6 +59,7 @@ def __init__( checkYieldTypes: bool = True, ignoreUnderscoreArgs: bool = True, checkClassAttributes: bool = True, + shouldDocumentPrivateClassAttributes: bool = False, requireReturnSectionWhenReturningNothing: bool = False, requireYieldSectionWhenYieldingNothing: bool = False, ) -> None: @@ -73,6 +74,9 @@ def __init__( self.checkYieldTypes: bool = checkYieldTypes self.ignoreUnderscoreArgs: bool = ignoreUnderscoreArgs self.checkClassAttributes: bool = checkClassAttributes + self.shouldDocumentPrivateClassAttributes: bool = ( + shouldDocumentPrivateClassAttributes + ) self.requireReturnSectionWhenReturningNothing: bool = ( requireReturnSectionWhenReturningNothing ) @@ -98,6 +102,9 @@ def visit_ClassDef(self, node: ast.ClassDef): # noqa: D102 argTypeHintsInSignature=self.argTypeHintsInSignature, argTypeHintsInDocstring=self.argTypeHintsInDocstring, skipCheckingShortDocstrings=self.skipCheckingShortDocstrings, + shouldDocumentPrivateClassAttributes=( + self.shouldDocumentPrivateClassAttributes + ), ) self.generic_visit(node) diff --git a/tests/data/edge_cases/11_private_class_attr/google.py b/tests/data/edge_cases/11_private_class_attr/google.py new file mode 100644 index 0000000..c61b6be --- /dev/null +++ b/tests/data/edge_cases/11_private_class_attr/google.py @@ -0,0 +1,16 @@ +class MyClass: + """ + My Class. + + This edge case comes from: https://github.com/jsh9/pydoclint/issues/148 + + Attributes: + attr_1 (str): The first attribute + attr_2: The 2nd attribute + attr_3 (float): The 3rd attribute + """ + + attr_1: str = 'hello' + attr_2 = 4 + attr_3: float + _hidden_attr: bool diff --git a/tests/test_main.py b/tests/test_main.py index 4c9d016..8d300eb 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1304,6 +1304,28 @@ def testNonAscii() -> None: 'has 1 type(s).', ], ), + ( + '11_private_class_attr/google.py', + {'style': 'google', 'shouldDocumentPrivateClassAttributes': False}, + [], + ), + ( + '11_private_class_attr/google.py', + {'style': 'google', 'shouldDocumentPrivateClassAttributes': True}, + [ + 'DOC601: Class `MyClass`: Class docstring contains fewer class attributes ' + 'than actual class attributes. (Please read ' + 'https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to ' + 'correctly document class attributes.)', + 'DOC603: Class `MyClass`: Class docstring attributes are different from ' + 'actual class attributes. (Or could be other formatting issues: ' + 'https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). ' + 'Attributes in the class definition but not in the docstring: [_hidden_attr: ' + 'bool]. (Please read ' + 'https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to ' + 'correctly document class attributes.)', + ], + ), ], ) def testEdgeCases(