Skip to content

Commit 75e485b

Browse files
authored
Fix false positive DOC203 in property methods (#115)
1 parent 55c0fde commit 75e485b

File tree

9 files changed

+151
-83
lines changed

9 files changed

+151
-83
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Change Log
22

3+
## [0.3.9] - 2024-01-16
4+
5+
- Fixed
6+
7+
- False positive violation `DOC203` when there is no docstring return section
8+
for methods with `@property` decorator
9+
10+
- Full diff
11+
- https://github.com/jsh9/pydoclint/compare/0.3.8...0.3.9
12+
313
## [0.3.8] - 2023-10-20
414

515
- Fixed

pydoclint/utils/generic.py

-24
Original file line numberDiff line numberDiff line change
@@ -92,19 +92,6 @@ def detectMethodType(node: ast.FunctionDef) -> MethodType:
9292
return MethodType.INSTANCE_METHOD
9393

9494

95-
def checkIsAbstractMethod(node: ast.FunctionDef) -> bool:
96-
"""Check whether `node` is an abstract method"""
97-
if len(node.decorator_list) == 0:
98-
return False
99-
100-
for decorator in node.decorator_list:
101-
if isinstance(decorator, ast.Name):
102-
if decorator.id == 'abstractmethod':
103-
return True
104-
105-
return False
106-
107-
10895
def getDocstring(node: ClassOrFunctionDef) -> str:
10996
"""Get docstring from a class definition or a function definition"""
11097
docstring_: Optional[str] = ast.get_docstring(node)
@@ -153,17 +140,6 @@ def getNodeName(node: ast.AST) -> str:
153140
return node.name if 'name' in node.__dict__ else ''
154141

155142

156-
def isPropertyMethod(node: FuncOrAsyncFuncDef) -> bool:
157-
"""Check whether a function has `@property` as its last decorator"""
158-
return (
159-
isinstance(node.decorator_list, list)
160-
and len(node.decorator_list) > 0
161-
and isinstance(node.decorator_list[-1], ast.Name)
162-
and hasattr(node.decorator_list[-1], 'id')
163-
and node.decorator_list[-1].id == 'property'
164-
)
165-
166-
167143
def stringStartsWith(string: str, substrings: Tuple[str, ...]) -> bool:
168144
"""Check whether the string starts with any of the substrings"""
169145
for substring in substrings:

pydoclint/utils/special_methods.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import ast
2+
3+
from pydoclint.utils.astTypes import FuncOrAsyncFuncDef
4+
5+
6+
def checkIsAbstractMethod(node: FuncOrAsyncFuncDef) -> bool:
7+
"""Check whether `node` is an abstract method"""
8+
return checkMethodContainsSpecifiedDecorator(node, 'abstractmethod')
9+
10+
11+
def checkIsPropertyMethod(node: FuncOrAsyncFuncDef) -> bool:
12+
"""Check whether `node` is a method with @property decorator"""
13+
return checkMethodContainsSpecifiedDecorator(node, 'property')
14+
15+
16+
def checkMethodContainsSpecifiedDecorator(
17+
node: FuncOrAsyncFuncDef,
18+
decorator: str,
19+
) -> bool:
20+
"""Check whether a method is decorated by the specified decorator"""
21+
return (
22+
isinstance(node.decorator_list, list)
23+
and len(node.decorator_list) > 0
24+
and any(
25+
( # noqa: PAR001
26+
isinstance(_, ast.Name)
27+
and hasattr(node.decorator_list[-1], 'id')
28+
and _.id == decorator
29+
)
30+
for _ in node.decorator_list
31+
)
32+
)

pydoclint/visitor.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66
from pydoclint.utils.astTypes import FuncOrAsyncFuncDef
77
from pydoclint.utils.doc import Doc
88
from pydoclint.utils.generic import (
9-
checkIsAbstractMethod,
109
collectFuncArgs,
1110
detectMethodType,
1211
generateMsgPrefix,
1312
getDocstring,
14-
isPropertyMethod,
1513
)
1614
from pydoclint.utils.internal_error import InternalError
1715
from pydoclint.utils.method_type import MethodType
@@ -27,6 +25,10 @@
2725
isReturnAnnotationNone,
2826
isReturnAnnotationNoReturn,
2927
)
28+
from pydoclint.utils.special_methods import (
29+
checkIsAbstractMethod,
30+
checkIsPropertyMethod,
31+
)
3032
from pydoclint.utils.violation import Violation
3133
from pydoclint.utils.visitor_helper import (
3234
checkReturnTypesForViolations,
@@ -485,11 +487,12 @@ def checkReturns( # noqa: C901
485487
hasGenAsRetAnno: bool = hasGeneratorAsReturnAnnotation(node)
486488
onlyHasYieldStmt: bool = hasYieldStmt and not hasReturnStmt
487489
hasIterAsRetAnno: bool = hasIteratorOrIterableAsReturnAnnotation(node)
490+
isPropertyMethod: bool = checkIsPropertyMethod(node)
488491

489492
docstringHasReturnSection: bool = doc.hasReturnsSection
490493

491494
violations: List[Violation] = []
492-
if not docstringHasReturnSection and not isPropertyMethod(node):
495+
if not docstringHasReturnSection and not isPropertyMethod:
493496
if (
494497
# fmt: off
495498
not (onlyHasYieldStmt and hasIterAsRetAnno)
@@ -541,6 +544,12 @@ def checkReturns( # noqa: C901
541544
# to check for DOC203 violations.
542545
return violations
543546

547+
if returnSec == [] and isPropertyMethod:
548+
# No need to check return type for methods with "@property"
549+
# decorator. This is because it's OK for @property methods
550+
# to have no return section in the docstring.
551+
return violations
552+
544553
checkReturnTypesForViolations(
545554
style=self.style,
546555
returnAnnotation=returnAnno,

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = pydoclint
3-
version = 0.3.8
3+
version = 0.3.9
44
description = A Python docstring linter that checks arguments, returns, yields, and raises sections
55
long_description = file: README.md
66
long_description_content_type = text/markdown

tests/data/common/property_method.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class MyClass:
2+
data: float = 2.1
3+
4+
@property
5+
def something(self) -> float:
6+
"""
7+
Some property.
8+
9+
It's OK to have no return section in this method, because this
10+
is a "property method" and is intended to be used as an attribute.
11+
"""
12+
return self.data

tests/test_main.py

+10
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,16 @@ def testAbstractMethod(style: str, checkReturnTypes: bool) -> None:
900900
assert list(map(str, violations)) == expected
901901

902902

903+
@pytest.mark.parametrize('style', ['google', 'numpy', 'sphinx'])
904+
def testNoReturnSectionInPropertyMethod(style: str) -> None:
905+
violations = _checkFile(
906+
filename=DATA_DIR / 'common/property_method.py',
907+
style=style,
908+
skipCheckingShortDocstrings=False,
909+
)
910+
assert len(violations) == 0
911+
912+
903913
@pytest.mark.parametrize(
904914
'style, argTypeHintsInDocstring, argTypeHintsInSignature',
905915
itertools.product(

tests/utils/test_generic.py

+2-55
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import ast
2-
from typing import List, Optional
2+
from typing import List
33

44
import pytest
55

6-
from pydoclint.utils.generic import (
7-
collectFuncArgs,
8-
isPropertyMethod,
9-
stripQuotes,
10-
)
6+
from pydoclint.utils.generic import collectFuncArgs, stripQuotes
117

128
src1 = """
139
def func1(
@@ -76,55 +72,6 @@ def testCollectFuncArgs(src: str, expected: List[str]) -> None:
7672
assert [_.arg for _ in out] == expected
7773

7874

79-
srcProperty1 = """
80-
class A:
81-
def method1(self):
82-
pass
83-
"""
84-
85-
srcProperty2 = """
86-
class A:
87-
@property
88-
def method1(self):
89-
pass
90-
"""
91-
92-
srcProperty3 = """
93-
# pydoclint only does static code analysis in order to achieve fast speed.
94-
# If users rename built-in decorator names (such as `property`), pydoclint
95-
# will not recognize it.
96-
97-
hello_world = property
98-
99-
class A:
100-
@hello_world
101-
def method1(self):
102-
pass
103-
"""
104-
105-
106-
@pytest.mark.parametrize(
107-
'src, expected',
108-
[
109-
(srcProperty1, False),
110-
(srcProperty2, True),
111-
(srcProperty3, False),
112-
],
113-
)
114-
def testIsPropertyMethod(src: str, expected: bool) -> None:
115-
def getMethod1(tree_: ast.AST) -> Optional[ast.FunctionDef]:
116-
for node_ in ast.walk(tree_):
117-
if isinstance(node_, ast.FunctionDef) and node_.name == 'method1':
118-
return node_
119-
120-
return None
121-
122-
tree = ast.parse(src)
123-
node = getMethod1(tree)
124-
result = isPropertyMethod(node)
125-
assert result == expected
126-
127-
12875
@pytest.mark.parametrize(
12976
'string, expected',
13077
[

tests/utils/test_special_methods.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import ast
2+
from typing import Optional
3+
4+
import pytest
5+
6+
from pydoclint.utils.special_methods import (
7+
checkMethodContainsSpecifiedDecorator,
8+
)
9+
10+
src1 = """
11+
class A:
12+
def method1(self):
13+
pass
14+
"""
15+
16+
src2 = """
17+
class A:
18+
@property
19+
def method1(self):
20+
pass
21+
"""
22+
23+
src3 = """
24+
class A:
25+
@hello
26+
@world
27+
@property
28+
@morning
29+
def method1(self):
30+
pass
31+
"""
32+
33+
src4 = """
34+
# pydoclint only does static code analysis in order to achieve fast speed.
35+
# If users rename built-in decorator names (such as `property`), pydoclint
36+
# will not recognize it.
37+
38+
hello_world = property
39+
40+
class A:
41+
@hello_world
42+
def method1(self):
43+
pass
44+
"""
45+
46+
47+
@pytest.mark.parametrize(
48+
'src, decorator, expected',
49+
[
50+
(src1, 'something', False),
51+
(src2, 'property', True),
52+
(src3, 'property', True),
53+
(src4, 'hello_world', True),
54+
(src4, 'property', False),
55+
],
56+
)
57+
def testCheckMethodContainsSpecifiedDecorator(
58+
src: str,
59+
decorator: str,
60+
expected: bool,
61+
) -> None:
62+
def getMethod1(tree_: ast.AST) -> Optional[ast.FunctionDef]:
63+
for node_ in ast.walk(tree_):
64+
if isinstance(node_, ast.FunctionDef) and node_.name == 'method1':
65+
return node_
66+
67+
return None
68+
69+
tree = ast.parse(src)
70+
node = getMethod1(tree)
71+
result = checkMethodContainsSpecifiedDecorator(node, decorator=decorator)
72+
assert result == expected

0 commit comments

Comments
 (0)