Skip to content

Commit

Permalink
Add new config option to treat ClassVar attrs as class attrs (#188)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsh9 authored Dec 16, 2024
1 parent dc5a53b commit da14ebd
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 10 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- Use "modern" type annotation, such as `list` and `str | None`
- Added
- Added static type checking using `mypy`
- A new config option,
`--only-attrs-with-ClassVar-are-treated-as-class-attrs`

## [0.5.11] - 2024-12-14

Expand Down
23 changes: 15 additions & 8 deletions docs/config_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ page:
- [14. `--check-class-attributes` (shortform: `-cca`, default: `True`)](#14---check-class-attributes-shortform--cca-default-true)
- [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: `False`)](#16---treat-property-methods-as-class-attributes-shortform--tpmaca-default-false)
- [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)
- [17. `--only-attrs-with-ClassVar-are-treated-as-class-attrs` (shortform: `-oawcv`, default: `False)](#17---only-attrs-with-classvar-are-treated-as-class-attrs-shortform--oawcv-default-false)
- [18. `--baseline`](#18---baseline)
- [19. `--generate-baseline` (default: `False`)](#19---generate-baseline-default-false)
- [20. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)](#20---show-filenames-in-every-violation-message-shortform--sfn-default-false)
- [21. `--config` (default: `pyproject.toml`)](#21---config-default-pyprojecttoml)

<!--TOC-->

Expand Down Expand Up @@ -202,7 +203,13 @@ 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.

## 17. `--baseline`
## 17. `--only-attrs-with-ClassVar-are-treated-as-class-attrs` (shortform: `-oawcv`, default: `False)

If True, only the attributes whose type annotations are wrapped within
`ClassVar` (where `ClassVar` is imported from `typing`) are treated as class
attributes, and all other attributes are treated as instance attributes.

## 18. `--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 @@ -222,12 +229,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.

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

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

## 19. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)
## 20. `--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 @@ -261,7 +268,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.)

## 20. `--config` (default: `pyproject.toml`)
## 21. `--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
16 changes: 16 additions & 0 deletions pydoclint/flake8_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,19 @@ def add_options(cls, parser: Any) -> None: # noqa: D102
' this option and --check-class-attributes to True.'
),
)
parser.add_option(
'-oawcv',
'--only-attrs-with-ClassVar-are-treated-as-class-attrs',
action='store',
default='False',
parse_from_config=True,
help=(
'If True, only the attributes whose type annotations are wrapped'
' within `ClassVar` (where `ClassVar` is imported from `typing`)'
' are treated as class attributes, and all other attributes are'
' treated as instance attributes.'
),
)

@classmethod
def parse_options(cls, options: Any) -> None: # noqa: D102
Expand Down Expand Up @@ -229,6 +242,9 @@ def parse_options(cls, options: Any) -> None: # noqa: D102
cls.treat_property_methods_as_class_attributes = (
options.treat_property_methods_as_class_attributes
)
cls.only_attrs_with_ClassVar_are_treated_as_class_attrs = (
options.only_attrs_with_ClassVar_are_treated_as_class_attrs
)
cls.style = options.style

def run(self) -> Generator[tuple[int, int, str, Any], None, None]:
Expand Down
25 changes: 25 additions & 0 deletions pydoclint/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,19 @@ def validateStyleValue(
' this option and --check-class-attributes to True.'
),
)
@click.option(
'-oawcv',
'--only-attrs-with-ClassVar-are-treated-as-class-attrs',
type=bool,
show_default=True,
default=False,
help=(
'If True, only the attributes whose type annotations are wrapped'
' within `ClassVar` (where `ClassVar` is imported from `typing`)'
' are treated as class attributes, and all other attributes are'
' treated as instance attributes.'
),
)
@click.option(
'--baseline',
type=click.Path(
Expand Down Expand Up @@ -325,6 +338,7 @@ def main( # noqa: C901
require_return_section_when_returning_none: bool,
require_return_section_when_returning_nothing: bool,
require_yield_section_when_yielding_nothing: bool,
only_attrs_with_classvar_are_treated_as_class_attrs: bool,
generate_baseline: bool,
baseline: str,
show_filenames_in_every_violation_message: bool,
Expand Down Expand Up @@ -414,6 +428,9 @@ def main( # noqa: C901
treatPropertyMethodsAsClassAttributes=(
treat_property_methods_as_class_attributes
),
onlyAttrsWithClassVarAreTreatedAsClassAttrs=(
only_attrs_with_classvar_are_treated_as_class_attrs
),
requireReturnSectionWhenReturningNothing=(
require_return_section_when_returning_nothing
),
Expand Down Expand Up @@ -531,6 +548,7 @@ def _checkPaths(
checkClassAttributes: bool = True,
shouldDocumentPrivateClassAttributes: bool = False,
treatPropertyMethodsAsClassAttributes: bool = False,
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool = False,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
quiet: bool = False,
Expand Down Expand Up @@ -583,6 +601,9 @@ def _checkPaths(
treatPropertyMethodsAsClassAttributes=(
treatPropertyMethodsAsClassAttributes
),
onlyAttrsWithClassVarAreTreatedAsClassAttrs=(
onlyAttrsWithClassVarAreTreatedAsClassAttrs
),
requireReturnSectionWhenReturningNothing=(
requireReturnSectionWhenReturningNothing
),
Expand Down Expand Up @@ -610,6 +631,7 @@ def _checkFile(
checkClassAttributes: bool = True,
shouldDocumentPrivateClassAttributes: bool = False,
treatPropertyMethodsAsClassAttributes: bool = False,
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool = False,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
) -> list[Violation]:
Expand Down Expand Up @@ -638,6 +660,9 @@ def _checkFile(
treatPropertyMethodsAsClassAttributes=(
treatPropertyMethodsAsClassAttributes
),
onlyAttrsWithClassVarAreTreatedAsClassAttrs=(
onlyAttrsWithClassVarAreTreatedAsClassAttrs
),
requireReturnSectionWhenReturningNothing=(
requireReturnSectionWhenReturningNothing
),
Expand Down
24 changes: 23 additions & 1 deletion pydoclint/utils/visitor_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def checkClassAttributesAgainstClassDocstring(
skipCheckingShortDocstrings: bool,
shouldDocumentPrivateClassAttributes: bool,
treatPropertyMethodsAsClassAttributes: bool,
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool,
) -> None:
"""Check class attribute list against the attribute list in docstring"""
actualArgs: ArgList = extractClassAttributesFromNode(
Expand All @@ -48,6 +49,9 @@ def checkClassAttributesAgainstClassDocstring(
shouldDocumentPrivateClassAttributes
),
treatPropertyMethodsAsClassAttrs=treatPropertyMethodsAsClassAttributes,
onlyAttrsWithClassVarAreTreatedAsClassAttrs=(
onlyAttrsWithClassVarAreTreatedAsClassAttrs
),
)

classDocstring: str = getDocstring(node)
Expand Down Expand Up @@ -126,6 +130,7 @@ def extractClassAttributesFromNode(
node: ast.ClassDef,
shouldDocumentPrivateClassAttributes: bool,
treatPropertyMethodsAsClassAttrs: bool,
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool,
) -> ArgList:
"""
Extract class attributes from an AST node.
Expand All @@ -140,6 +145,11 @@ def extractClassAttributesFromNode(
treatPropertyMethodsAsClassAttrs : bool
Whether we'd like to treat property methods as class attributes.
If ``True``, property methods will be included in the return value.
onlyAttrsWithClassVarAreTreatedAsClassAttrs : bool
If ``True``, only the attributes whose type annotations are wrapped
within ``ClassVar`` (where ``ClassVar`` is imported from ``typing``)
are treated as class attributes, and all other attributes are
treated as instance attributes.
Returns
-------
Expand All @@ -149,7 +159,7 @@ def extractClassAttributesFromNode(
Raises
------
EdgeCaseError
When the length of item.targets is 0
When the length of ``item.targets`` is 0
"""
if 'body' not in node.__dict__ or len(node.body) == 0:
return ArgList([])
Expand Down Expand Up @@ -178,6 +188,18 @@ def extractClassAttributesFromNode(
if not shouldDocumentPrivateClassAttributes:
atl = [_ for _ in atl if not _.name.startswith('_')]

if onlyAttrsWithClassVarAreTreatedAsClassAttrs:
atl = [
Arg(
name=_.name,
typeHint=_.typeHint[9:-1], # remove "ClassVar[" and "]"
)
for _ in atl
if (
_.typeHint.startswith('ClassVar[') and _.typeHint.endswith(']')
)
]

return ArgList(infoList=atl)


Expand Down
9 changes: 8 additions & 1 deletion pydoclint/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def __init__(
ignoreUnderscoreArgs: bool = True,
checkClassAttributes: bool = True,
shouldDocumentPrivateClassAttributes: bool = False,
treatPropertyMethodsAsClassAttributes: bool = True,
treatPropertyMethodsAsClassAttributes: bool = False,
onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool = False,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
) -> None:
Expand All @@ -84,6 +85,9 @@ def __init__(
self.treatPropertyMethodsAsClassAttributes: bool = (
treatPropertyMethodsAsClassAttributes
)
self.onlyAttrsWithClassVarAreTreatedAsClassAttrs: bool = (
onlyAttrsWithClassVarAreTreatedAsClassAttrs
)
self.requireReturnSectionWhenReturningNothing: bool = (
requireReturnSectionWhenReturningNothing
)
Expand Down Expand Up @@ -115,6 +119,9 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: D102
treatPropertyMethodsAsClassAttributes=(
self.treatPropertyMethodsAsClassAttributes
),
onlyAttrsWithClassVarAreTreatedAsClassAttrs=(
self.onlyAttrsWithClassVarAreTreatedAsClassAttrs
),
)

self.generic_visit(node)
Expand Down
53 changes: 53 additions & 0 deletions tests/data/edge_cases/17_ClassVar/cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# This edge case comes from:
# https://github.com/jsh9/pydoclint/issues/140#issuecomment-2426031940

from dataclasses import dataclass
from typing import ClassVar

from attrs import define, field
from pydantic import BaseModel, Field


@define
class AttrsClass:
"""
My class.
Attributes:
a (bool): my class attribute
c (float): This is y
"""

a: ClassVar[bool] = True # class attribute
b: int # instance attribute
c: float = 1.0 # instance attribute
d: str = field(default='abc') # instance attribute


@dataclass
class DataClass:
"""
My class.
Attributes:
e (ClassVar[bool]): my class attribute
"""

e: ClassVar[bool] = True # class attribute
f: int # instance attribute
g: float = 1.0 # instance attribute
h: str = field(default='abc') # instance attribute


class PydanticClass(BaseModel):
"""
My class.
Attributes:
i (bool): my class attribute
"""

i: ClassVar[bool] = True # class attribute
j: int # instance attribute
k: float = 1.0 # instance attribute
l: str = Field(default='abc') # instance attribute
68 changes: 68 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,74 @@ def testNonAscii() -> None:
('16_assign_to_attr/cases.py', {'style': 'sphinx'}, []),
('16_assign_to_attr/cases.py', {'style': 'google'}, []),
('16_assign_to_attr/cases.py', {'style': 'numpy'}, []),
(
'17_ClassVar/cases.py',
{
'style': 'google',
'checkClassAttributes': True,
'onlyAttrsWithClassVarAreTreatedAsClassAttrs': False,
},
[
'DOC601: Class `AttrsClass`: 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 `AttrsClass`: 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: [b: int, d: '
'str]. (Please read '
'https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to '
'correctly document class attributes.)',
'DOC601: Class `DataClass`: 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 `DataClass`: 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: [f: int, g: '
'float, h: str]. (Please read '
'https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to '
'correctly document class attributes.)',
'DOC601: Class `PydanticClass`: 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 `PydanticClass`: 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: [j: int, k: '
'float, l: str]. (Please read '
'https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to '
'correctly document class attributes.)',
],
),
(
'17_ClassVar/cases.py',
{
'style': 'google',
'checkClassAttributes': True,
'onlyAttrsWithClassVarAreTreatedAsClassAttrs': True,
},
[
'DOC602: Class `AttrsClass`: Class docstring contains more class attributes '
'than in actual class attributes. (Please read '
'https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to '
'correctly document class attributes.)',
'DOC603: Class `AttrsClass`: 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 ). '
'Arguments in the docstring but not in the actual class attributes: [c: '
'float]. (Please read '
'https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to '
'correctly document class attributes.)',
'DOC605: Class `DataClass`: Attribute names match, but type hints in these '
'attributes do not match: e (Please read '
'https://jsh9.github.io/pydoclint/checking_class_attributes.html on how to '
'correctly document class attributes.)',
],
),
],
)
def testEdgeCases(
Expand Down
Loading

0 comments on commit da14ebd

Please sign in to comment.