Skip to content

Commit

Permalink
Add option to treat property methods as class attributes (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsh9 authored Jul 14, 2024
1 parent 3c39d2e commit 0b750d5
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 15 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Change Log

## [unpublished] - 2024-07-04
## [0.5.4] - 2024-07-14

- Added
- An option `--should-document-private-class-attributes` (if False, private
class attributes should not appear in the docstring)
- An option `--treat-property-methods-as-class-attributes` (if True,
`@property` methods are treated like class attributes and need to be
documented in the class docstring)

## [0.5.3] - 2024-06-26

Expand Down
31 changes: 23 additions & 8 deletions docs/config_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ page:
- [12. `--check-yield-types` (shortform: `-cyt`, default: `True`)](#12---check-yield-types-shortform--cyt-default-true)
- [13. `--ignore-underscore-args` (shortform: `-iua`, default: `True`)](#13---ignore-underscore-args-shortform--iua-default-true)
- [14. `--check-class-attributes` (shortform: `-cca`, default: `True`)](#14---check-class-attributes-shortform--cca-default-true)
- [15. `--baseline`](#15---baseline)
- [16. `--generate-baseline` (default: `False`)](#16---generate-baseline-default-false)
- [17. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)](#17---show-filenames-in-every-violation-message-shortform--sfn-default-false)
- [18. `--config` (default: `pyproject.toml`)](#18---config-default-pyprojecttoml)
- [15. `--should-document-private-class-attributes` (shortform: `-sdpca`, default: `False`)](#15---should-document-private-class-attributes-shortform--sdpca-default-false)
- [16. `--treat-property-methods-as-class-attributes` (shortform: `-tpmaca`, default: `True`)](#16---treat-property-methods-as-class-attributes-shortform--tpmaca-default-true)
- [17. `--baseline`](#17---baseline)
- [18. `--generate-baseline` (default: `False`)](#18---generate-baseline-default-false)
- [19. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)](#19---show-filenames-in-every-violation-message-shortform--sfn-default-false)
- [20. `--config` (default: `pyproject.toml`)](#20---config-default-pyprojecttoml)

<!--TOC-->

Expand Down Expand Up @@ -187,7 +189,20 @@ Please read
[this page](https://jsh9.github.io/pydoclint/checking_class_attributes.html)
for more instructions.

## 15. `--baseline`
## 15. `--should-document-private-class-attributes` (shortform: `-sdpca`, default: `False`)

If True, private class attributes (those that start with leading `_`) should be
documented. If False, they should not be documented.

## 16. `--treat-property-methods-as-class-attributes` (shortform: `-tpmaca`, default: `True`)

If True, treat `@property` methods as class properties. This means that they
need to be documented in the "Attributes" section of the class docstring, and
there cannot be any docstring under the @property methods. This option is only
effective when --check-class-attributes is True. We recommend setting both this
option and --check-class-attributes to True.

## 17. `--baseline`

Baseline allows you to remember the current project state and then show only
new violations, ignoring old ones. This can be very useful when you'd like to
Expand All @@ -207,12 +222,12 @@ project. If `--generate-baseline` is not passed (default value is `False`),
_pydoclint_ will read your baseline file, and ignore all violations specified
in that file.

## 16. `--generate-baseline` (default: `False`)
## 18. `--generate-baseline` (default: `False`)

Required to use with `--baseline` option. If `True`, generate the baseline file
that contains all current violations.

## 17. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)
## 19. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)

If False, in the terminal the violation messages are grouped by file names:

Expand Down Expand Up @@ -246,7 +261,7 @@ This can be convenient if you would like to click on each violation message and
go to the corresponding line in your IDE. (Note: not all terminal app offers
this functionality.)

## 18. `--config` (default: `pyproject.toml`)
## 20. `--config` (default: `pyproject.toml`)

The full path of the .toml config file that contains the config options. Note
that the command line options take precedence over the .toml file. Look at this
Expand Down
25 changes: 25 additions & 0 deletions pydoclint/flake8_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,21 @@ def add_options(cls, parser): # noqa: D102
' class attributes should not appear in the docstring.'
),
)
parser.add_option(
'-tpmaca',
'--treat-property-methods-as-class-attributes',
action='store',
default='True',
parse_from_config=True,
help=(
'If True, treat @property methods as class properties. This means'
' that they need to be documented in the "Attributes" section of'
' the class docstring, and there cannot be any docstring under'
' the @property methods. This option is only effective when'
' --check-class-attributes is True. We recommend setting both'
' this option and --check-class-attributes to True.'
),
)

@classmethod
def parse_options(cls, options): # noqa: D102
Expand Down Expand Up @@ -208,6 +223,9 @@ def parse_options(cls, options): # noqa: D102
cls.should_document_private_class_attributes = (
options.should_document_private_class_attributes
)
cls.treat_property_methods_as_class_attributes = (
options.treat_property_methods_as_class_attributes
)
cls.style = options.style

def run(self) -> Generator[Tuple[int, int, str, Any], None, None]:
Expand Down Expand Up @@ -281,6 +299,10 @@ def run(self) -> Generator[Tuple[int, int, str, Any], None, None]:
'--should-document-private-class-attributes',
self.should_document_private_class_attributes,
)
treatPropertyMethodsAsClassAttributes = self._bool(
'--treat-property-methods-as-class-attributes',
self.treat_property_methods_as_class_attributes,
)

if self.style not in {'numpy', 'google', 'sphinx'}:
raise ValueError(
Expand All @@ -307,6 +329,9 @@ def run(self) -> Generator[Tuple[int, int, str, Any], None, None]:
shouldDocumentPrivateClassAttributes=(
shouldDocumentPrivateClassAttributes
),
treatPropertyMethodsAsClassAttributes=(
treatPropertyMethodsAsClassAttributes
),
style=self.style,
)
v.visit(self._tree)
Expand Down
27 changes: 27 additions & 0 deletions pydoclint/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,21 @@ def validateStyleValue(
' class attributes should not appear in the docstring.'
),
)
@click.option(
'-tpmaca',
'--treat-property-methods-as-class-attributes',
type=bool,
show_default=True,
default=True,
help=(
'If True, treat @property methods as class properties. This means'
' that they need to be documented in the "Attributes" section of'
' the class docstring, and there cannot be any docstring under'
' the @property methods. This option is only effective when'
' --check-class-attributes is True. We recommend setting both'
' this option and --check-class-attributes to True.'
),
)
@click.option(
'--baseline',
type=click.Path(
Expand Down Expand Up @@ -304,6 +319,7 @@ def main( # noqa: C901
ignore_underscore_args: bool,
check_class_attributes: bool,
should_document_private_class_attributes: bool,
treat_property_methods_as_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 @@ -392,6 +408,9 @@ def main( # noqa: C901
shouldDocumentPrivateClassAttributes=(
should_document_private_class_attributes
),
treatPropertyMethodsAsClassAttributes=(
treat_property_methods_as_class_attributes
),
requireReturnSectionWhenReturningNothing=(
require_return_section_when_returning_nothing
),
Expand Down Expand Up @@ -508,6 +527,7 @@ def _checkPaths(
ignoreUnderscoreArgs: bool = True,
checkClassAttributes: bool = True,
shouldDocumentPrivateClassAttributes: bool = False,
treatPropertyMethodsAsClassAttributes: bool = True,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
quiet: bool = False,
Expand Down Expand Up @@ -557,6 +577,9 @@ def _checkPaths(
shouldDocumentPrivateClassAttributes=(
shouldDocumentPrivateClassAttributes
),
treatPropertyMethodsAsClassAttributes=(
treatPropertyMethodsAsClassAttributes
),
requireReturnSectionWhenReturningNothing=(
requireReturnSectionWhenReturningNothing
),
Expand All @@ -583,6 +606,7 @@ def _checkFile(
ignoreUnderscoreArgs: bool = True,
checkClassAttributes: bool = True,
shouldDocumentPrivateClassAttributes: bool = False,
treatPropertyMethodsAsClassAttributes: bool = True,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
) -> List[Violation]:
Expand All @@ -605,6 +629,9 @@ def _checkFile(
shouldDocumentPrivateClassAttributes=(
shouldDocumentPrivateClassAttributes
),
treatPropertyMethodsAsClassAttributes=(
treatPropertyMethodsAsClassAttributes
),
requireReturnSectionWhenReturningNothing=(
requireReturnSectionWhenReturningNothing
),
Expand Down
34 changes: 29 additions & 5 deletions pydoclint/utils/visitor_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from pydoclint.utils.annotation import unparseAnnotation
from pydoclint.utils.arg import Arg, ArgList
from pydoclint.utils.astTypes import FuncOrAsyncFuncDef
from pydoclint.utils.doc import Doc
from pydoclint.utils.generic import (
appendArgsToCheckToV105,
Expand All @@ -15,6 +16,7 @@
from pydoclint.utils.internal_error import InternalError
from pydoclint.utils.return_anno import ReturnAnnotation
from pydoclint.utils.return_arg import ReturnArg
from pydoclint.utils.special_methods import checkIsPropertyMethod
from pydoclint.utils.violation import Violation
from pydoclint.utils.yield_arg import YieldArg

Expand All @@ -37,6 +39,7 @@ def checkClassAttributesAgainstClassDocstring(
argTypeHintsInDocstring: bool,
skipCheckingShortDocstrings: bool,
shouldDocumentPrivateClassAttributes: bool,
treatPropertyMethodsAsClassAttributes: bool,
) -> None:
"""Check class attribute list against the attribute list in docstring"""
classAttributes = _collectClassAttributes(
Expand All @@ -45,7 +48,10 @@ def checkClassAttributesAgainstClassDocstring(
shouldDocumentPrivateClassAttributes
),
)
actualArgs: ArgList = _convertClassAttributesIntoArgList(classAttributes)
actualArgs: ArgList = _convertClassAttributesIntoArgList(
classAttrs=classAttributes,
treatPropertyMethodsAsClassAttrs=treatPropertyMethodsAsClassAttributes,
)

classDocstring: str = getDocstring(node)

Expand Down Expand Up @@ -122,12 +128,15 @@ def _collectClassAttributes(
*,
node: ast.ClassDef,
shouldDocumentPrivateClassAttributes: bool,
) -> List[Union[ast.Assign, ast.AnnAssign]]:
) -> List[Union[ast.Assign, ast.AnnAssign, FuncOrAsyncFuncDef]]:
if 'body' not in node.__dict__ or len(node.body) == 0:
return []

attributes: List[Union[ast.Assign, ast.AnnAssign]] = []
for item in node.body:
# Notes:
# - ast.Assign are something like "attr1 = 1.5"
# - ast.AnnAssign are something like "attr2: float = 1.5"
if isinstance(item, (ast.Assign, ast.AnnAssign)):
classAttrName: str = _getClassAttrName(item)
if shouldDocumentPrivateClassAttributes:
Expand All @@ -136,6 +145,11 @@ def _collectClassAttributes(
if not classAttrName.startswith('_'):
attributes.append(item)

if isinstance(
item, (ast.AsyncFunctionDef, ast.FunctionDef)
) and checkIsPropertyMethod(item):
attributes.append(item)

return attributes


Expand All @@ -150,20 +164,30 @@ def _getClassAttrName(attrItem: Union[ast.Assign, ast.AnnAssign]) -> str:


def _convertClassAttributesIntoArgList(
classAttributes: List[Union[ast.Assign, ast.AnnAssign]],
*,
classAttrs: List[Union[ast.Assign, ast.AnnAssign, FuncOrAsyncFuncDef]],
treatPropertyMethodsAsClassAttrs: bool,
) -> ArgList:
atl: List[Arg] = []
for attr in classAttributes:
for attr in classAttrs:
if isinstance(attr, ast.AnnAssign):
atl.append(Arg.fromAstAnnAssign(attr))
elif isinstance(attr, ast.Assign):
if isinstance(attr.targets[0], ast.Tuple):
atl.extend(ArgList.fromAstAssignWithTupleTarget(attr).infoList)
else:
atl.append(Arg.fromAstAssignWithNonTupleTarget(attr))
elif isinstance(attr, (ast.AsyncFunctionDef, ast.FunctionDef)):
if treatPropertyMethodsAsClassAttrs:
atl.append(
Arg(
name=attr.name,
typeHint=unparseAnnotation(attr.returns),
)
)
else:
raise InternalError(
f'Unkonwn type of class attribute: {type(attr)}'
f'Unknown type of class attribute: {type(attr)}'
)

return ArgList(infoList=atl)
Expand Down
7 changes: 7 additions & 0 deletions pydoclint/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __init__(
ignoreUnderscoreArgs: bool = True,
checkClassAttributes: bool = True,
shouldDocumentPrivateClassAttributes: bool = False,
treatPropertyMethodsAsClassAttributes: bool = True,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
) -> None:
Expand All @@ -77,6 +78,9 @@ def __init__(
self.shouldDocumentPrivateClassAttributes: bool = (
shouldDocumentPrivateClassAttributes
)
self.treatPropertyMethodsAsClassAttributes: bool = (
treatPropertyMethodsAsClassAttributes
)
self.requireReturnSectionWhenReturningNothing: bool = (
requireReturnSectionWhenReturningNothing
)
Expand Down Expand Up @@ -105,6 +109,9 @@ def visit_ClassDef(self, node: ast.ClassDef): # noqa: D102
shouldDocumentPrivateClassAttributes=(
self.shouldDocumentPrivateClassAttributes
),
treatPropertyMethodsAsClassAttributes=(
self.treatPropertyMethodsAsClassAttributes
),
)

self.generic_visit(node)
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = pydoclint
version = 0.5.3
version = 0.5.4
description = A Python docstring linter that checks arguments, returns, yields, and raises sections
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down
29 changes: 29 additions & 0 deletions tests/data/edge_cases/12_property_methods_as_class_attr/google.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class House:
"""
A house
Attributes:
price (float): House price
Args:
price_0 (float): House price
"""

def __init__(self, price_0: float) -> None:
self._price = price_0

@property
def price(self) -> float:
"""The house price"""
return self._price

@price.setter
def price(self, new_price):
if new_price > 0 and isinstance(new_price, float):
self._price = new_price
else:
print('Please enter a valid price')

@price.deleter
def price(self):
del self._price
Loading

0 comments on commit 0b750d5

Please sign in to comment.