Skip to content

Commit 22b9b37

Browse files
authored
Fix false positive DOC405 & DOC201 for bare returns (#205)
1 parent 1f2ea57 commit 22b9b37

File tree

6 files changed

+175
-1
lines changed

6 files changed

+175
-1
lines changed

CHANGELOG.md

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

3+
## [Unpublished]
4+
5+
- Fixed
6+
- False positive DOC405 and DOC201 when we have bare return statements
7+
together with `yield` statements
8+
39
## [0.5.18] - 2025-01-12
410

511
- Fixed

pydoclint/utils/return_yield_raise.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ def isThisNodeAReturnStmt(node_: ast.AST) -> bool:
8686
return _hasExpectedStatements(node, isThisNodeAReturnStmt)
8787

8888

89+
def hasBareReturnStatements(node: FuncOrAsyncFuncDef) -> bool:
90+
"""
91+
Check whether the function node has bare return statements (i.e.,
92+
just a "return" without anything behind it)
93+
"""
94+
95+
def isThisNodeABareReturnStmt(node_: ast.AST) -> bool:
96+
return isinstance(node_, ast.Return) and node_.value is None
97+
98+
return _hasExpectedStatements(node, isThisNodeABareReturnStmt)
99+
100+
89101
def hasRaiseStatements(node: FuncOrAsyncFuncDef) -> bool:
90102
"""Check whether the function node has any raise statements"""
91103

pydoclint/visitor.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from pydoclint.utils.return_arg import ReturnArg
2020
from pydoclint.utils.return_yield_raise import (
2121
getRaisedExceptions,
22+
hasBareReturnStatements,
2223
hasGeneratorAsReturnAnnotation,
2324
hasIteratorOrIterableAsReturnAnnotation,
2425
hasRaiseStatements,
@@ -736,6 +737,11 @@ def my_function(num: int) -> Generator[int, None, str]:
736737
onlyHasYieldStmt: bool = hasYieldStmt and not hasReturnStmt
737738
hasReturnAnno: bool = hasReturnAnnotation(node)
738739

740+
if hasReturnStmt:
741+
hasBareReturnStmt: bool = hasBareReturnStatements(node)
742+
else:
743+
hasBareReturnStmt = False # to save some time
744+
739745
returnAnno = ReturnAnnotation(unparseName(node.returns))
740746
returnSec: list[ReturnArg] = doc.returnSection
741747

@@ -752,6 +758,11 @@ def my_function(num: int) -> Generator[int, None, str]:
752758
hasReturnAnno and not hasGenAsRetAnno
753759
))
754760

761+
# If the return statement in the function body is a bare
762+
# return, we don't throw DOC201 or DOC405. See more at:
763+
# https://github.com/jsh9/pydoclint/issues/126#issuecomment-2136497913
764+
and not hasBareReturnStmt
765+
755766
# fmt: on
756767
):
757768
retTypeInGenerator = extractReturnTypeFromGenerator(
@@ -813,7 +824,9 @@ def my_function(num: int) -> Generator[int, None, str]:
813824
else:
814825
violations.append(v405)
815826
else:
816-
if not hasGenAsRetAnno or not hasIterAsRetAnno:
827+
if (
828+
not hasGenAsRetAnno or not hasIterAsRetAnno
829+
) and not hasBareReturnStmt:
817830
violations.append(v405)
818831

819832
return violations
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# From: https://github.com/jsh9/pydoclint/issues/126
2+
3+
from contextlib import contextmanager
4+
5+
6+
@contextmanager
7+
def my_func_1(db: Optional[int]) -> Iterator[int]:
8+
"""Test a function.
9+
10+
Args:
11+
db: the database
12+
13+
Yields:
14+
Some stuff.
15+
"""
16+
if db is not None:
17+
yield db
18+
return
19+
20+
db = ...
21+
yield db
22+
23+
24+
def my_func_2(arg1: int) -> None:
25+
"""
26+
Test a function.
27+
28+
Args:
29+
arg1: some argument
30+
31+
Returns:
32+
The return value
33+
"""
34+
pass

tests/test_main.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1600,6 +1600,61 @@ def testNonAscii() -> None:
16001600
{'style': 'numpy'},
16011601
[],
16021602
),
1603+
(
1604+
'23_bare_return_stmt_with_yield/google.py',
1605+
{
1606+
'style': 'google',
1607+
'argTypeHintsInDocstring': False,
1608+
'checkYieldTypes': False,
1609+
'checkReturnTypes': True,
1610+
},
1611+
[
1612+
'DOC203: Function `my_func_2` return type(s) in docstring not consistent with '
1613+
"the return annotation. Return annotation types: ['None']; docstring return "
1614+
"section types: ['']"
1615+
],
1616+
),
1617+
(
1618+
'23_bare_return_stmt_with_yield/google.py',
1619+
{
1620+
'style': 'google',
1621+
'argTypeHintsInDocstring': False,
1622+
'checkYieldTypes': False,
1623+
'checkReturnTypes': False,
1624+
},
1625+
[],
1626+
),
1627+
(
1628+
'23_bare_return_stmt_with_yield/google.py',
1629+
{
1630+
'style': 'google',
1631+
'argTypeHintsInDocstring': False,
1632+
'checkYieldTypes': True,
1633+
'checkReturnTypes': True,
1634+
},
1635+
[
1636+
'DOC404: Function `my_func_1` yield type(s) in docstring not consistent with '
1637+
'the return annotation. The yield type (the 0th arg in '
1638+
'Generator[...]/Iterator[...]): int; docstring "yields" section types:',
1639+
'DOC203: Function `my_func_2` return type(s) in docstring not consistent with '
1640+
"the return annotation. Return annotation types: ['None']; docstring return "
1641+
"section types: ['']",
1642+
],
1643+
),
1644+
(
1645+
'23_bare_return_stmt_with_yield/google.py',
1646+
{
1647+
'style': 'google',
1648+
'argTypeHintsInDocstring': False,
1649+
'checkYieldTypes': True,
1650+
'checkReturnTypes': False,
1651+
},
1652+
[
1653+
'DOC404: Function `my_func_1` yield type(s) in docstring not consistent with '
1654+
'the return annotation. The yield type (the 0th arg in '
1655+
'Generator[...]/Iterator[...]): int; docstring "yields" section types:',
1656+
],
1657+
),
16031658
],
16041659
)
16051660
def testEdgeCases(
@@ -1627,6 +1682,8 @@ def testPlayground() -> None:
16271682
filename=DATA_DIR / 'playground.py',
16281683
style='google',
16291684
skipCheckingRaises=True,
1685+
argTypeHintsInDocstring=False,
1686+
checkYieldTypes=False,
16301687
)
16311688
expected = []
16321689
assert list(map(str, violations)) == expected

tests/utils/test_returns_yields_raise.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pydoclint.utils.generic import getFunctionId
88
from pydoclint.utils.return_yield_raise import (
99
getRaisedExceptions,
10+
hasBareReturnStatements,
1011
hasGeneratorAsReturnAnnotation,
1112
hasRaiseStatements,
1213
hasReturnAnnotation,
@@ -83,6 +84,33 @@ def classmethod1_child1():
8384
"""
8485

8586

87+
src8 = """
88+
def func8():
89+
return
90+
"""
91+
92+
93+
src9 = """
94+
def func9():
95+
# In tested function, so it doesn't
96+
# count as having a return statement
97+
def func9_child1():
98+
return
99+
"""
100+
101+
102+
src10 = """
103+
def func10():
104+
# When mixed, we still consider it
105+
# as having a bare return statement
106+
if 1 > 2:
107+
return 501
108+
109+
if 2 > 6:
110+
return
111+
"""
112+
113+
86114
@pytest.mark.parametrize(
87115
'src, expected',
88116
[
@@ -92,6 +120,9 @@ def classmethod1_child1():
92120
(src4, False),
93121
(src5, True),
94122
(src6, True),
123+
(src8, True),
124+
(src9, False),
125+
(src10, True),
95126
],
96127
)
97128
def testHasReturnStatements(src: str, expected: bool) -> None:
@@ -101,6 +132,27 @@ def testHasReturnStatements(src: str, expected: bool) -> None:
101132
assert hasReturnStatements(tree.body[0]) == expected
102133

103134

135+
@pytest.mark.parametrize(
136+
'src, expected',
137+
[
138+
(src1, False),
139+
(src2, False),
140+
(src3, False),
141+
(src4, False),
142+
(src5, False),
143+
(src6, False),
144+
(src8, True),
145+
(src9, False),
146+
(src10, True),
147+
],
148+
)
149+
def testHasBareReturnStatements(src: str, expected: bool) -> None:
150+
tree = ast.parse(src)
151+
assert len(tree.body) == 1 # sanity check
152+
assert isinstance(tree.body[0], (ast.FunctionDef, ast.AsyncFunctionDef))
153+
assert hasBareReturnStatements(tree.body[0]) == expected
154+
155+
104156
def testHasReturnStatements_inClass() -> None:
105157
tree = ast.parse(src7)
106158
assert len(tree.body) == 1 # sanity check

0 commit comments

Comments
 (0)