Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check class attributes against docstring #130

Merged
merged 6 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Change Log

## [0.5.0] - 2024-06-22

- Added

- Added checks for class attributes
- This functionality checks class attributes against the "Attributes"
section of the docstring
- There is a new config option, `--check-class-attributes` (or `-cca`),
which defaults to `True`. Therefore, this breaks backward compatibility.
- To maintain backward compatibility, set `--check-class-attributes` to
`False`
- Options like `--check-arg-order`, `--arg-type-hints-in-signature`, and
`--arg-type-hints-in-docstring` are still effective in checking class
attributes

- Full diff
- https://github.com/jsh9/pydoclint/compare/0.4.2...0.5.0

## [0.4.2] - 2024-05-29

- Changed
Expand Down
105 changes: 105 additions & 0 deletions docs/checking_class_attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Checking class attributes

Class attributes are similar to function arguments. They look like this:

```python
class MyPet:
name: str
age_in_months: int
weight_in_kg: float
is_very_cute_or_not: bool = True
```

And we'd like to also document them in docstrings. However, none of the
mainstream docstring styles (Google, numpy, or Sphinx) offers explicit
guidelines on documenting class attributes. Therefore, in _pydoclint_, we
designed a new (but not totally surprising) docstring section: "attributes"
under which we can document the class attributes.

Here is an example that demonstrates the expected style:

```python
class MyPet:
"""
A class to hold information of my pet.

Attributes
----------
name : str
Name of my pet
age_in_months : int
Age of my pet (unit: months)
weight_in_kg : float
Weight of my pet (unit: kg)
is_very_cute_or_not : bool
Is my pet very cute? Or just cute?

Parameters
----------
airtag_id : int
The ID of the AirTag that I put on my pet
"""
name: str
age_in_months: int
weight_in_kg: float
is_very_cute_or_not: bool = True

def __init__(self, airtag_id: int) -> None:
self.airtag_id = airtag_id
```

From this example, we can see a few things:

1. The class attributes should be put in a different docstring section than the
argument passed into the class constructor (`__init__()`)
2. Both the class attributes and the input arguments to `__init__()` are in the
same docstring. (This is assuming the _pydoclint_ option
`--allow-init-docstring` is `False`)

If `--allow-init-docstring` is set to `True`, we can write two separate
docstrings like this:

```python
class MyPet:
"""
A class to hold information of my pet.

Attributes
----------
name : str
Name of my pet
age_in_months : int
Age of my pet (unit: months)
weight_in_kg : float
Weight of my pet (unit: kg)
is_very_cute_or_not : bool
Is my pet very cute? Or just cute?
"""
name: str
age_in_months: int
weight_in_kg: float
is_very_cute_or_not: bool = True

def __init__(self, airtag_id: int) -> None:
"""
Initialize a class object.

Parameters
----------
airtag_id : int
The ID of the AirTag that I put on my pet
"""
self.airtag_id = airtag_id
```

#### Special note for Sphinx style docstrings

If you use the Sphinx style, you can annotate class attributes like this:

```python
:attr my_attr: My attribute
:type my_attr: float
```

However, there is no guarantee that this `:attr` tag is recognized by current
doc rendering programs.
26 changes: 18 additions & 8 deletions docs/config_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ page:
- [9. `--require-return-section-when-returning-nothing` (shortform: `-rrs`, default: `False`)](#9---require-return-section-when-returning-nothing-shortform--rrs-default-false)
- [10. `--check-return-types` (shortform: `-crt`, default: `True`)](#10---check-return-types-shortform--crt-default-true)
- [11. `--check-yield-types` (shortform: `-cyt`, default: `True`)](#11---check-yield-types-shortform--cyt-default-true)
- [12. `--baseline`](#12---baseline)
- [13. `--generate-baseline` (default: `False`)](#13---generate-baseline-default-false)
- [14. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)](#14---show-filenames-in-every-violation-message-shortform--sfn-default-false)
- [15. `--config` (default: `pyproject.toml`)](#15---config-default-pyprojecttoml)
- [12. `--check-class-attributes` (shortform: `-cca`, default: `True`)](#12---check-class-attributes-shortform--cca-default-true)
- [13. `--baseline`](#13---baseline)
- [14. `--generate-baseline` (default: `False`)](#14---generate-baseline-default-false)
- [15. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)](#15---show-filenames-in-every-violation-message-shortform--sfn-default-false)
- [16. `--config` (default: `pyproject.toml`)](#16---config-default-pyprojecttoml)

<!--TOC-->

Expand Down Expand Up @@ -165,7 +166,16 @@ annotation in the function signature are consistent
If True, check that the type(s) in the docstring "yields" section and the
return annotation in the function signature are consistent.

## 12. `--baseline`
## 12. `--check-class-attributes` (shortform: `-cca`, default: `True`)

If True, check the class attributes (defined under the class definition)
against the "Attributes" section of the class's docstring.

Please read
[this page](https://jsh9.github.io/pydoclint/checking_class_attributes.html)
for more instructions.

## 13. `--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 @@ -185,12 +195,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.

## 13. `--generate-baseline` (default: `False`)
## 14. `--generate-baseline` (default: `False`)

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

## 14. `--show-filenames-in-every-violation-message` (shortform: `-sfn`, default: `False`)
## 15. `--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 @@ -224,7 +234,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.)

## 15. `--config` (default: `pyproject.toml`)
## 16. `--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
17 changes: 17 additions & 0 deletions pydoclint/flake8_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ def add_options(cls, parser): # noqa: D102
' signature do not need to appear in the docstring.'
),
)
parser.add_option(
'-cca',
'--check-class-attributes',
action='store',
default='True',
parse_from_config=True,
help=(
'If True, class attributes (the ones defined right beneath'
' "class MyClass:") are checked against the docstring.'
),
)

@classmethod
def parse_options(cls, options): # noqa: D102
Expand All @@ -181,6 +192,7 @@ def parse_options(cls, options): # noqa: D102
cls.check_return_types = options.check_return_types
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.style = options.style

def run(self) -> Generator[Tuple[int, int, str, Any], None, None]:
Expand Down Expand Up @@ -246,6 +258,10 @@ def run(self) -> Generator[Tuple[int, int, str, Any], None, None]:
'--ignore-underscore-args',
self.ignore_underscore_args,
)
checkClassAttributes = self._bool(
'--check-class-attributes',
self.check_class_attributes,
)

if self.style not in {'numpy', 'google', 'sphinx'}:
raise ValueError(
Expand All @@ -268,6 +284,7 @@ def run(self) -> Generator[Tuple[int, int, str, Any], None, None]:
checkReturnTypes=checkReturnTypes,
checkYieldTypes=checkYieldTypes,
ignoreUnderscoreArgs=ignoreUnderscoreArgs,
checkClassAttributes=checkClassAttributes,
style=self.style,
)
v.visit(self._tree)
Expand Down
17 changes: 17 additions & 0 deletions pydoclint/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,17 @@ def validateStyleValue(
' signature do not need to appear in the docstring.'
),
)
@click.option(
'-cca',
'--check-class-attributes',
type=bool,
show_default=True,
default=True,
help=(
'If True, class attributes (the ones defined right beneath'
' "class MyClass:") are checked against the docstring.'
),
)
@click.option(
'--baseline',
type=click.Path(
Expand Down Expand Up @@ -279,6 +290,7 @@ def main( # noqa: C901
check_return_types: bool,
check_yield_types: bool,
ignore_underscore_args: bool,
check_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 @@ -363,6 +375,7 @@ def main( # noqa: C901
checkReturnTypes=check_return_types,
checkYieldTypes=check_yield_types,
ignoreUnderscoreArgs=ignore_underscore_args,
checkClassAttributes=check_class_attributes,
requireReturnSectionWhenReturningNothing=(
require_return_section_when_returning_nothing
),
Expand Down Expand Up @@ -477,6 +490,7 @@ def _checkPaths(
checkReturnTypes: bool = True,
checkYieldTypes: bool = True,
ignoreUnderscoreArgs: bool = True,
checkClassAttributes: bool = True,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
quiet: bool = False,
Expand Down Expand Up @@ -522,6 +536,7 @@ def _checkPaths(
checkReturnTypes=checkReturnTypes,
checkYieldTypes=checkYieldTypes,
ignoreUnderscoreArgs=ignoreUnderscoreArgs,
checkClassAttributes=checkClassAttributes,
requireReturnSectionWhenReturningNothing=(
requireReturnSectionWhenReturningNothing
),
Expand All @@ -546,6 +561,7 @@ def _checkFile(
checkReturnTypes: bool = True,
checkYieldTypes: bool = True,
ignoreUnderscoreArgs: bool = True,
checkClassAttributes: bool = True,
requireReturnSectionWhenReturningNothing: bool = False,
requireYieldSectionWhenYieldingNothing: bool = False,
) -> List[Violation]:
Expand All @@ -564,6 +580,7 @@ def _checkFile(
checkReturnTypes=checkReturnTypes,
checkYieldTypes=checkYieldTypes,
ignoreUnderscoreArgs=ignoreUnderscoreArgs,
checkClassAttributes=checkClassAttributes,
requireReturnSectionWhenReturningNothing=(
requireReturnSectionWhenReturningNothing
),
Expand Down
Loading
Loading