Skip to content

Commit

Permalink
Use "attribute" docstring section in Sphinx style (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsh9 authored Jun 24, 2024
1 parent 63bed6b commit f43a961
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 54 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# Change Log

## [unpublished]
## [0.5.1] - 2024-06-24

- Fixed

- Fixed a bug in unparsing annotations when checking class attributes
- Fixed a bug in checking class attributes where there are no attributes in
class def or in docstring

- Changed
- Used a dedicated "attribute" section for Sphinx-style docstrings

## [0.5.0] - 2024-06-22

- Added
Expand Down
95 changes: 82 additions & 13 deletions docs/checking_class_attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,27 @@ class MyPet:
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.
Oftentimes we'd like to also document them in docstrings and have _pydoclint_
check them. It is controlled by the `--check-class-attributes` option (see
https://jsh9.github.io/pydoclint/config_options.html)

Here is an example that demonstrates the expected style:
However, none of the mainstream docstring styles (Google, numpy, or Sphinx)
offers explicit guidelines on documenting class attributes. Therefore,
_pydoclint_ adopts the following stance (i.e., how to write docstrings that
pass _pydoclint_'s check)

- Document the class attributes under the "Attribute" section, and document the
input arguments to `__init__()` under the "Parameters" (or "Args") section
- Separate the "Attribute" and "Parameters" sections in your docstring
- You can use a single docstring (under the class name) or two docstrings (one
under the class name and the other under `__init__()`)
- If you use two docstrings, please keep the "Attributes" section in the
docstring under the class name

Here are some examples showing how to document class attributes in different
styles:

## 1. Numpy style

```python
class MyPet:
Expand Down Expand Up @@ -92,14 +106,69 @@ class MyPet:
self.airtag_id = airtag_id
```

#### Special note for Sphinx style docstrings

If you use the Sphinx style, you can annotate class attributes like this:
## 2. Google style

```python
:attr my_attr: My attribute
:type my_attr: float
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?
Args:
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
```

However, there is no guarantee that this `:attr` tag is recognized by current
doc rendering programs.
You can also use two separate docstrings (one for the class and one for
`__init__()`, similar to the Numpy style.)

## 3. Sphinx style

```python
class MyPet:
"""
A class to hold information of my pet.
.. attribute :: name
:type: str
Name of my pet
.. attribute :: age_in_months
:type: int
Age of my pet (unit: months)
.. attribute :: weight_in_keg
:type: float
Weight of my pet (unit: kg)
.. attribute :: is_very_cute_or_not
:type: bool
Is my pet very cute? Or just cute?
:param airtag_id: The ID of the AirTag that I put on my pet
:type airtag_id: int
"""
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
```
35 changes: 29 additions & 6 deletions pydoclint/utils/visitor_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
from pydoclint.utils.violation import Violation
from pydoclint.utils.yield_arg import YieldArg

SPHINX_MSG_POSTFIX: str = (
' (Please read'
' https://jsh9.github.io/pydoclint/checking_class_attributes.html'
' on how to correctly document class attributes.)'
)


def checkClassAttributesAgainstClassDocstring(
*,
Expand All @@ -42,7 +48,7 @@ def checkClassAttributesAgainstClassDocstring(
violations.append(
Violation(
code=1,
line=node.lineno,
line=lineNum,
msgPrefix=f'Class `{node.name}`:',
msgPostfix=str(excp).replace('\n', ' '),
)
Expand All @@ -55,10 +61,16 @@ def checkClassAttributesAgainstClassDocstring(
actualArgs=actualArgs,
violations=violations,
violationForDocArgsLengthShorter=Violation(
code=601, line=lineNum, msgPrefix=msgPrefix
code=601,
line=lineNum,
msgPrefix=msgPrefix,
msgPostfix=SPHINX_MSG_POSTFIX,
),
violationForDocArgsLengthLonger=Violation(
code=602, line=lineNum, msgPrefix=msgPrefix
code=602,
line=lineNum,
msgPrefix=msgPrefix,
msgPostfix=SPHINX_MSG_POSTFIX,
),
)

Expand All @@ -68,10 +80,16 @@ def checkClassAttributesAgainstClassDocstring(
violations=violations,
actualArgsAreClassAttributes=True,
violationForOrderMismatch=Violation(
code=604, line=lineNum, msgPrefix=msgPrefix
code=604,
line=lineNum,
msgPrefix=msgPrefix,
msgPostfix=SPHINX_MSG_POSTFIX,
),
violationForTypeHintMismatch=Violation(
code=605, line=lineNum, msgPrefix=msgPrefix
code=605,
line=lineNum,
msgPrefix=msgPrefix,
msgPostfix=SPHINX_MSG_POSTFIX,
),
shouldCheckArgOrder=shouldCheckArgOrder,
argTypeHintsInSignature=argTypeHintsInSignature,
Expand Down Expand Up @@ -220,12 +238,17 @@ def checkNameOrderAndTypeHintsOfDocArgsAgainstActualArgs(
+ f' {sorted(argsInDocNotInFunc)}.'
)

msgPostfixTemp: str = ' '.join(msgPostfixParts)

if actualArgsAreClassAttributes:
msgPostfixTemp += SPHINX_MSG_POSTFIX

violations.append(
Violation(
code=603 if actualArgsAreClassAttributes else 103,
line=lineNum,
msgPrefix=msgPrefix,
msgPostfix=' '.join(msgPostfixParts),
msgPostfix=msgPostfixTemp,
)
)

Expand Down
4 changes: 2 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = pydoclint
version = 0.5.0
version = 0.5.1
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 All @@ -16,7 +16,7 @@ classifiers =
packages = find:
install_requires =
click>=8.1.0
docstring_parser_fork>=0.0.7
docstring_parser_fork>=0.0.8
tomli>=2.0.1; python_version<'3.11'
python_requires = >=3.8

Expand Down
7 changes: 5 additions & 2 deletions tests/data/sphinx/allow_init_docstring/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,11 @@ class E:
"""
A class that does something
:attr attr1:
:attr attr2: Arg 2
.. attribute :: attr1
.. attribute :: attr2
Arg 2
"""

def __init__(self, arg1: int, arg2: float) -> None:
Expand Down
40 changes: 28 additions & 12 deletions tests/data/sphinx/class_attributes/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ class MyClass1:
"""
A class that holds some things.
:attr name: The name
:type name: str | bool | None
:attr indices: The indices
:type indices: pd.DataFrame
.. attribute :: name
:type: str | bool | None
The name
.. attribute :: indices
:type: pd.DataFrame
The indices
:param arg1: The information
:type arg1: float
"""
Expand Down Expand Up @@ -38,12 +44,20 @@ class MyClass2:
In this class, the class attributes and the instance attribute (self.arg1)
are mixed together as attributes.
:attr name: The name
:type name: str
:attr indices: The indices
:type indices: int
:attr arg1: The information
:type arg1: float
.. attribute :: name
:type: str | bool | None
The name
.. attribute :: indices
:type: int
The indices
.. attribute :: arg1
:type: float
The information
"""

name: str
Expand Down Expand Up @@ -107,8 +121,10 @@ class MyClass4:
"""
This is a class
:attr name: My name
:type name: str
.. attribute :: name
:type: str
My name
"""

def __int__(self):
Expand Down
12 changes: 8 additions & 4 deletions tests/data/sphinx/class_attributes/init_docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ class MyClass1:
"""
A class that holds some things.
.. attribute :: name
:type: str
:attr name: The name
:type name: str
:attr indices: The indices
:type indices: int
The name
.. attribute :: indices
:type: int
The indices
"""

name: str
Expand Down
Loading

0 comments on commit f43a961

Please sign in to comment.