Skip to content

Commit 20a4029

Browse files
authored
New violation code D504 (#220)
1 parent 193dc55 commit 20a4029

15 files changed

Lines changed: 160 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Change Log
22

3+
## [unpublished]
4+
5+
- Changed
6+
- Added `DOC504` and a config option
7+
`--should-declare-assert-error-if-assert-statement-exists`. If this option
8+
is True and a function has an `assert` statement, an `AssertError`
9+
declaration is required in the docstring. Otherwise `DOC504` is raised.
10+
(This changes the behavior introduced in v0.6.1.)
11+
312
## [0.6.2] - 2025-02-17
413

514
- Fixed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ _pydoclint_ currently has 7 categories of style violation codes:
202202
- `DOC2xx`: Violations about return argument(s)
203203
- `DOC3xx`: Violations about class docstring and class constructor
204204
- `DOC4xx`: Violations about "yield" statements
205-
- `DOC5xx`: Violations about "raise" statements
205+
- `DOC5xx`: Violations about "raise" and "assert" statements
206206
- `DOC6xx`: Violations about class attributes
207207

208208
For detailed explanations of each violation code, please read this page:

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ _pydoclint_ currently has 7 categories of style violation codes:
202202
- `DOC2xx`: Violations about return argument(s)
203203
- `DOC3xx`: Violations about class docstring and class constructor
204204
- `DOC4xx`: Violations about "yield" statements
205-
- `DOC5xx`: Violations about "raise" statements
205+
- `DOC5xx`: Violations about "raise" and "assert" statements
206206
- `DOC6xx`: Violations about class attributes
207207

208208
For detailed explanations of each violation code, please read this page:

docs/violation_codes.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- [2. `DOC2xx`: Violations about return argument(s)](#2-doc2xx-violations-about-return-arguments)
1111
- [3. `DOC3xx`: Violations about class docstring and class constructor](#3-doc3xx-violations-about-class-docstring-and-class-constructor)
1212
- [4. `DOC4xx`: Violations about "yield" statements](#4-doc4xx-violations-about-yield-statements)
13-
- [5. `DOC5xx`: Violations about "raise" statements](#5-doc5xx-violations-about-raise-statements)
13+
- [5. `DOC5xx`: Violations about "raise" and "assert" statements](#5-doc5xx-violations-about-raise-and-assert-statements)
1414
- [6. `DOC6xx`: Violations about class attributes](#6-doc6xx-violations-about-class-attributes)
1515

1616
<!--TOC-->
@@ -83,13 +83,14 @@ have a return section.
8383
| `DOC403` | Function/method has a "Yields" section in the docstring, but there are no "yield" statements, or the return annotation is not a Generator/Iterator/Iterable |
8484
| `DOC404` | The types in the docstring's Yields section and the return annotation in the signature are not consistent |
8585

86-
## 5. `DOC5xx`: Violations about "raise" statements
86+
## 5. `DOC5xx`: Violations about "raise" and "assert" statements
8787

88-
| Code | Explanation |
89-
| -------- | --------------------------------------------------------------------------------------------------------- |
90-
| `DOC501` | Function/method has raise/assert statements, but the docstring does not have a "Raises" section |
91-
| `DOC502` | Function/method has a "Raises" section in the docstring, but there are not "raise" statements in the body |
92-
| `DOC503` | Exceptions in the "Raises" section in the docstring do not match those in the function body |
88+
| Code | Explanation |
89+
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
90+
| `DOC501` | Function/method has raise statements, but the docstring does not have a "Raises" section |
91+
| `DOC502` | Function/method has a "Raises" section in the docstring, but there are not "raise" statements in the body |
92+
| `DOC503` | Exceptions in the "Raises" section in the docstring do not match those in the function body |
93+
| `DOC504` | Function/method has assert statements, but the docstring does not have a "Raises" section. (Assert statements could raise "AssertError".) |
9394

9495
## 6. `DOC6xx`: Violations about class attributes
9596

pydoclint/flake8_entry.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,18 @@ def add_options(cls, parser: Any) -> None: # noqa: D102
225225
' appear in the docstring.'
226226
),
227227
)
228+
parser.add_option(
229+
'-sdae',
230+
'--should-declare-assert-error-if-assert-statement-exists',
231+
action='store',
232+
default='False',
233+
help=(
234+
'If True, we need to declare AssertError in the "Raises"'
235+
' section of the docstring if there are any "assert"'
236+
' statements in the function body. This is because an "assert"'
237+
' statement could raise an AssertError.'
238+
),
239+
)
228240
parser.add_option(
229241
'-csm',
230242
'--check-style-mismatch',

pydoclint/main.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,19 @@ def validateStyleValue(
264264
' appear in the docstring.'
265265
),
266266
)
267+
@click.option(
268+
'-sdae',
269+
'--should-declare-assert-error-if-assert-statement-exists',
270+
type=bool,
271+
show_default=True,
272+
default=False,
273+
help=(
274+
'If True, we need to declare AssertError in the "Raises"'
275+
' section of the docstring if there are any "assert"'
276+
' statements in the function body. This is because an "assert"'
277+
' statement could raise an AssertError.'
278+
),
279+
)
267280
@click.option(
268281
'-csm',
269282
'--check-style-mismatch',
@@ -379,6 +392,7 @@ def main( # noqa: C901
379392
require_yield_section_when_yielding_nothing: bool,
380393
only_attrs_with_classvar_are_treated_as_class_attrs: bool,
381394
should_document_star_arguments: bool,
395+
should_declare_assert_error_if_assert_statement_exists: bool,
382396
check_style_mismatch: bool,
383397
generate_baseline: bool,
384398
auto_regenerate_baseline: bool,
@@ -491,6 +505,9 @@ def main( # noqa: C901
491505
require_yield_section_when_yielding_nothing
492506
),
493507
shouldDocumentStarArguments=should_document_star_arguments,
508+
shouldDeclareAssertErrorIfAssertStatementExists=(
509+
should_declare_assert_error_if_assert_statement_exists
510+
),
494511
checkStyleMismatch=check_style_mismatch,
495512
)
496513

@@ -628,6 +645,7 @@ def _checkPaths(
628645
requireReturnSectionWhenReturningNothing: bool = False,
629646
requireYieldSectionWhenYieldingNothing: bool = False,
630647
shouldDocumentStarArguments: bool = True,
648+
shouldDeclareAssertErrorIfAssertStatementExists: bool = False,
631649
checkStyleMismatch: bool = False,
632650
quiet: bool = False,
633651
exclude: str = '',
@@ -689,6 +707,9 @@ def _checkPaths(
689707
requireYieldSectionWhenYieldingNothing
690708
),
691709
shouldDocumentStarArguments=shouldDocumentStarArguments,
710+
shouldDeclareAssertErrorIfAssertStatementExists=(
711+
shouldDeclareAssertErrorIfAssertStatementExists
712+
),
692713
checkStyleMismatch=checkStyleMismatch,
693714
)
694715
allViolations[filename.as_posix()] = violationsInThisFile
@@ -715,6 +736,7 @@ def _checkFile(
715736
requireReturnSectionWhenReturningNothing: bool = False,
716737
requireYieldSectionWhenYieldingNothing: bool = False,
717738
shouldDocumentStarArguments: bool = True,
739+
shouldDeclareAssertErrorIfAssertStatementExists: bool = False,
718740
checkStyleMismatch: bool = False,
719741
) -> list[Violation]:
720742
if not filename.is_file(): # sometimes folder names can end with `.py`
@@ -771,6 +793,9 @@ def _checkFile(
771793
requireYieldSectionWhenYieldingNothing
772794
),
773795
shouldDocumentStarArguments=shouldDocumentStarArguments,
796+
shouldDeclareAssertErrorIfAssertStatementExists=(
797+
shouldDeclareAssertErrorIfAssertStatementExists
798+
),
774799
checkStyleMismatch=checkStyleMismatch,
775800
)
776801
visitor.visit(tree)

pydoclint/utils/return_yield_raise.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,24 @@ def isThisNodeABareReturnStmt(node_: ast.AST) -> bool:
9898
return _hasExpectedStatements(node, isThisNodeABareReturnStmt)
9999

100100

101-
def hasRaiseOrAssertStatements(node: FuncOrAsyncFuncDef) -> bool:
102-
"""Check whether the function node has any raise or assert statements"""
101+
def hasRaiseStatements(node: FuncOrAsyncFuncDef) -> bool:
102+
"""Check whether the function node has any raise statements"""
103103

104104
def isThisNodeARaiseStmt(node_: ast.AST) -> bool:
105-
return isinstance(node_, (ast.Raise, ast.Assert))
105+
return isinstance(node_, ast.Raise)
106106

107107
return _hasExpectedStatements(node, isThisNodeARaiseStmt)
108108

109109

110+
def hasAssertStatements(node: FuncOrAsyncFuncDef) -> bool:
111+
"""Check whether the function node has any assert statements"""
112+
113+
def isThisNodeAnAssertStmt(node_: ast.AST) -> bool:
114+
return isinstance(node_, ast.Assert)
115+
116+
return _hasExpectedStatements(node, isThisNodeAnAssertStmt)
117+
118+
110119
def getRaisedExceptions(node: FuncOrAsyncFuncDef) -> list[str]:
111120
"""Get the raised exceptions in a function node as a sorted list"""
112121
return sorted(set(_getRaisedExceptions(node)))

pydoclint/utils/violation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@
5656
' https://jsh9.github.io/pydoclint/notes_generator_vs_iterator.html'
5757
),
5858

59-
501: 'has raise/assert statements, but the docstring does not have a "Raises" section',
59+
501: 'has raise statements, but the docstring does not have a "Raises" section',
6060
502: 'has a "Raises" section in the docstring, but there are not "raise" statements in the body',
6161
503: 'exceptions in the "Raises" section in the docstring do not match those in the function body.',
62+
504: 'has assert statements, but the docstring does not have a "Raises" section. (Assert statements could raise "AssertError".)',
6263

6364
601: 'Class docstring contains fewer class attributes than actual class attributes.',
6465
602: 'Class docstring contains more class attributes than in actual class attributes.',

pydoclint/visitor.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@
2525
from pydoclint.utils.return_arg import ReturnArg
2626
from pydoclint.utils.return_yield_raise import (
2727
getRaisedExceptions,
28+
hasAssertStatements,
2829
hasBareReturnStatements,
2930
hasGeneratorAsReturnAnnotation,
3031
hasIteratorOrIterableAsReturnAnnotation,
31-
hasRaiseOrAssertStatements,
32+
hasRaiseStatements,
3233
hasReturnAnnotation,
3334
hasReturnStatements,
3435
hasYieldStatements,
@@ -76,6 +77,7 @@ def __init__(
7677
requireReturnSectionWhenReturningNothing: bool = False,
7778
requireYieldSectionWhenYieldingNothing: bool = False,
7879
shouldDocumentStarArguments: bool = True,
80+
shouldDeclareAssertErrorIfAssertStatementExists: bool = False,
7981
checkStyleMismatch: bool = False,
8082
) -> None:
8183
self.style: str = style
@@ -105,6 +107,9 @@ def __init__(
105107
requireYieldSectionWhenYieldingNothing
106108
)
107109
self.shouldDocumentStarArguments: bool = shouldDocumentStarArguments
110+
self.shouldDeclareAssertErrorIfAssertStatementExists: bool = (
111+
shouldDeclareAssertErrorIfAssertStatementExists
112+
)
108113
self.checkStyleMismatch: bool = checkStyleMismatch
109114

110115
self.parent: ast.AST = ast.Pass() # keep track of parent node
@@ -875,7 +880,7 @@ def my_function(num: int) -> Generator[int, None, str]:
875880

876881
return violations
877882

878-
def checkRaises(
883+
def checkRaises( # noqa: C901
879884
self,
880885
node: FuncOrAsyncFuncDef,
881886
parent: ast.AST,
@@ -890,19 +895,37 @@ def checkRaises(
890895
v501 = Violation(code=501, line=lineNum, msgPrefix=msgPrefix)
891896
v502 = Violation(code=502, line=lineNum, msgPrefix=msgPrefix)
892897
v503 = Violation(code=503, line=lineNum, msgPrefix=msgPrefix)
898+
v504 = Violation(code=504, line=lineNum, msgPrefix=msgPrefix)
893899

894900
docstringHasRaisesSection: bool = doc.hasRaisesSection
895-
hasRaiseOrAssertStmt: bool = hasRaiseOrAssertStatements(node)
901+
hasRaiseStmt: bool = hasRaiseStatements(node)
902+
hasAssertStmt: bool = hasAssertStatements(node)
896903

897-
if hasRaiseOrAssertStmt and not docstringHasRaisesSection:
904+
if hasRaiseStmt and not docstringHasRaisesSection:
898905
violations.append(v501)
899906

900-
if not hasRaiseOrAssertStmt and docstringHasRaisesSection:
901-
if not self.isAbstractMethod:
907+
if self.shouldDeclareAssertErrorIfAssertStatementExists:
908+
if (
909+
not hasAssertStmt
910+
and not hasRaiseStmt
911+
and docstringHasRaisesSection
912+
and not self.isAbstractMethod
913+
):
914+
violations.append(v502)
915+
else:
916+
if (
917+
not hasRaiseStmt
918+
and docstringHasRaisesSection
919+
and not self.isAbstractMethod
920+
):
902921
violations.append(v502)
903922

923+
if self.shouldDeclareAssertErrorIfAssertStatementExists:
924+
if hasAssertStmt and not docstringHasRaisesSection:
925+
violations.append(v504)
926+
904927
# check that the raise statements match those in body.
905-
if hasRaiseOrAssertStmt:
928+
if hasRaiseStmt:
906929
docRaises: list[str] = []
907930

908931
for raises in doc.parsed.raises:

tests/data/google/raises/cases.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ def func16(self) -> None:
229229

230230
def func17(self) -> None:
231231
"""
232+
Assuming --should-declare-assert-error-if-assert-statement-exists True.
233+
232234
It should pass.
233235
234236
Raises:
@@ -238,6 +240,8 @@ def func17(self) -> None:
238240

239241
def func18(self) -> None:
240242
"""
243+
Assuming --should-declare-assert-error-if-assert-statement-exists True.
244+
241245
It should pass.
242246
243247
Raises:
@@ -247,6 +251,8 @@ def func18(self) -> None:
247251

248252
def func19(self) -> None:
249253
"""
254+
Assuming --should-declare-assert-error-if-assert-statement-exists True.
255+
250256
Should fail, expects `AssertionError`.
251257
252258
Returns:
@@ -256,6 +262,8 @@ def func19(self) -> None:
256262

257263
def func20(self) -> None:
258264
"""
265+
When --should-declare-assert-error-if-assert-statement-exists is True:
266+
259267
Should fail, expects `AssertionError`.
260268
261269
Raises:

0 commit comments

Comments
 (0)