Skip to content

Commit

Permalink
Add an option for private class attributes (#149)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsh9 authored Jul 5, 2024
1 parent 98f7a5c commit 3c39d2e
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 2 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
22 changes: 22 additions & 0 deletions pydoclint/flake8_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]:
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions pydoclint/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -537,6 +554,9 @@ def _checkPaths(
checkYieldTypes=checkYieldTypes,
ignoreUnderscoreArgs=ignoreUnderscoreArgs,
checkClassAttributes=checkClassAttributes,
shouldDocumentPrivateClassAttributes=(
shouldDocumentPrivateClassAttributes
),
requireReturnSectionWhenReturningNothing=(
requireReturnSectionWhenReturningNothing
),
Expand All @@ -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]:
Expand All @@ -581,6 +602,9 @@ def _checkFile(
checkYieldTypes=checkYieldTypes,
ignoreUnderscoreArgs=ignoreUnderscoreArgs,
checkClassAttributes=checkClassAttributes,
shouldDocumentPrivateClassAttributes=(
shouldDocumentPrivateClassAttributes
),
requireReturnSectionWhenReturningNothing=(
requireReturnSectionWhenReturningNothing
),
Expand Down
27 changes: 25 additions & 2 deletions pydoclint/utils/visitor_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -113,19 +119,36 @@ 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 []

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:
Expand Down
7 changes: 7 additions & 0 deletions pydoclint/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
)
Expand All @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions tests/data/edge_cases/11_private_class_attr/google.py
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 3c39d2e

Please sign in to comment.