Skip to content

Commit 2310f32

Browse files
authored
Support property (#5)
* add support for property * add documentation * update message for missing attribute to include property
1 parent 57767b0 commit 2310f32

File tree

7 files changed

+245
-9
lines changed

7 files changed

+245
-9
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## [Unreleased]
44

5+
## [v1.0.3] - 2023-01-05
6+
7+
### Added
8+
9+
- Support for class properties
10+
511
## [v1.0.2] - 2023-01-04
612

713
### Added
@@ -78,3 +84,4 @@
7884
[v1.0.0]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.0.0
7985
[v1.0.1]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.0.1
8086
[v1.0.2]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.0.2
87+
[v1.0.3]: https://github.com/jdkandersson/flake8-docstrings-complete/releases/v1.0.3

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,6 +1315,16 @@ class FooClass:
13151315
def __init__(self):
13161316
self.bar = "bar"
13171317

1318+
class FooClass:
1319+
"""Perform foo action.
1320+
1321+
Attrs:
1322+
"""
1323+
1324+
@property
1325+
def bar(self):
1326+
return "bar"
1327+
13181328
class FooClass:
13191329
"""Perform foo action."""
13201330

@@ -1361,6 +1371,17 @@ class FooClass:
13611371
def __init__(self):
13621372
self.bar = "bar"
13631373

1374+
class FooClass:
1375+
"""Perform foo action.
1376+
1377+
Attrs:
1378+
bar: The value to perform the foo action on.
1379+
"""
1380+
1381+
@property
1382+
def bar(self):
1383+
return "bar"
1384+
13641385
class FooClass:
13651386
"""Perform foo action.
13661387

flake8_docstrings_complete/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def _is_fixture_decorator(self, node: ast.expr) -> bool:
275275
node: The node to check.
276276
277277
Returns:
278-
Whether the node is a decorator fixture.
278+
Whether the node is a fixture decorator.
279279
"""
280280
# Handle variable
281281
fixture_name: str | None = None
@@ -298,17 +298,26 @@ def _is_fixture_decorator(self, node: ast.expr) -> bool:
298298
def _skip_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
299299
"""Check whether to skip a function.
300300
301+
A function is skipped if it is a test function in a test file, if it is a fixture in a test
302+
or fixture file or if it is a property.
303+
301304
Args:
302305
node: The function to check
303306
304307
Returns:
305308
Whether to skip the function.
306309
"""
310+
# Check for properties
311+
if any(attrs.is_property_decorator(decorator) for decorator in node.decorator_list):
312+
return True
313+
314+
# Check for test functions
307315
if self._file_type == types_.FileType.TEST and re.match(
308316
self._test_function_pattern, node.name
309317
):
310318
return True
311319

320+
# Check for fixtures
312321
if self._file_type in {types_.FileType.TEST, types_.FileType.FIXTURE}:
313322
return any(self._is_fixture_decorator(decorator) for decorator in node.decorator_list)
314323

flake8_docstrings_complete/attrs.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
)
2727
ATTR_NOT_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}063"
2828
ATTR_NOT_IN_DOCSTR_MSG = (
29-
f'{ATTR_NOT_IN_DOCSTR_CODE} "%s" attribute should be described in the docstring'
29+
f'{ATTR_NOT_IN_DOCSTR_CODE} "%s" attribute/ property should be described in the docstring'
3030
f"{MORE_INFO_BASE}{ATTR_NOT_IN_DOCSTR_CODE.lower()}"
3131
)
3232
ATTR_IN_DOCSTR_CODE = f"{ERROR_CODE_PREFIX}064"
@@ -39,6 +39,25 @@
3939
PRIVATE_ATTR_PREFIX = "_"
4040

4141

42+
def is_property_decorator(node: ast.expr) -> bool:
43+
"""Determine whether an expression is a property decorator.
44+
45+
Args:
46+
node: The node to check.
47+
48+
Returns:
49+
Whether the node is a property decorator.
50+
"""
51+
if isinstance(node, ast.Name):
52+
return node.id == "property"
53+
54+
# Handle call
55+
if isinstance(node, ast.Call):
56+
return is_property_decorator(node=node.func)
57+
58+
return False
59+
60+
4261
def _get_class_target_name(target: ast.expr) -> ast.Name | None:
4362
"""Get the name of the target for an assignment on the class.
4463
@@ -61,7 +80,7 @@ def _get_class_target_name(target: ast.expr) -> ast.Name | None:
6180

6281

6382
def _iter_class_attrs(
64-
nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign],
83+
nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign | types_.Node],
6584
) -> Iterator[types_.Node]:
6685
"""Get the node of the variable being assigned at the class level if the target is a Name.
6786
@@ -72,7 +91,9 @@ def _iter_class_attrs(
7291
All the nodes of name targets of the assignment expressions.
7392
"""
7493
for node in nodes:
75-
if isinstance(node, ast.Assign):
94+
if isinstance(node, types_.Node):
95+
yield node
96+
elif isinstance(node, ast.Assign):
7697
target_names = filter(
7798
None, (_get_class_target_name(target) for target in node.targets)
7899
)
@@ -136,7 +157,7 @@ def _iter_method_attrs(
136157
def check(
137158
docstr_info: docstring.Docstring,
138159
docstr_node: ast.Constant,
139-
class_assign_nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign],
160+
class_assign_nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign | types_.Node],
140161
method_assign_nodes: Iterable[ast.Assign | ast.AnnAssign | ast.AugAssign],
141162
) -> Iterator[types_.Problem]:
142163
"""Check that all class attributes are described in the docstring.
@@ -211,7 +232,7 @@ class VisitorWithinClass(ast.NodeVisitor):
211232
method_assign_nodes: All the return nodes encountered within the class methods.
212233
"""
213234

214-
class_assign_nodes: list[ast.Assign | ast.AnnAssign | ast.AugAssign]
235+
class_assign_nodes: list[ast.Assign | ast.AnnAssign | ast.AugAssign | types_.Node]
215236
method_assign_nodes: list[ast.Assign | ast.AnnAssign | ast.AugAssign]
216237
_visited_once: bool
217238
_visited_top_level: bool
@@ -237,6 +258,18 @@ def visit_assign(self, node: ast.Assign | ast.AnnAssign | ast.AugAssign) -> None
237258
# Ensure recursion continues
238259
self.generic_visit(node)
239260

261+
def visit_any_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
262+
"""Visit a function definition node.
263+
264+
Args:
265+
node: The function definition to check.
266+
"""
267+
if any(is_property_decorator(decorator) for decorator in node.decorator_list):
268+
self.class_assign_nodes.append(
269+
types_.Node(lineno=node.lineno, col_offset=node.col_offset, name=node.name)
270+
)
271+
self.visit_top_level(node=node)
272+
240273
def visit_once(self, node: ast.AST) -> None:
241274
"""Visit the node once and then skip.
242275
@@ -264,6 +297,6 @@ def visit_top_level(self, node: ast.AST) -> None:
264297
visit_AnnAssign = visit_assign # noqa: N815,DCO063
265298
visit_AugAssign = visit_assign # noqa: N815,DCO063
266299
# Ensure that nested functions and classes are not iterated over
267-
visit_FunctionDef = visit_top_level # noqa: N815,DCO063
268-
visit_AsyncFunctionDef = visit_top_level # noqa: N815,DCO063
300+
visit_FunctionDef = visit_any_function # noqa: N815,DCO063
301+
visit_AsyncFunctionDef = visit_any_function # noqa: N815,DCO063
269302
visit_ClassDef = visit_once # noqa: N815,DCO063

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "flake8-docstrings-complete"
3-
version = "1.0.2"
3+
version = "1.0.3"
44
description = "A linter that checks docstrings are complete"
55
authors = ["David Andersson <david@jdkandersson.com>"]
66
license = "Apache 2.0"

tests/unit/test___init__.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,54 @@ def function_1(self):
550550
),
551551
pytest.param(
552552
'''
553+
class Class1:
554+
"""Docstring.
555+
556+
Attrs:
557+
function_1:
558+
"""
559+
@property
560+
def function_1(self):
561+
"""Docstring 1."""
562+
return 1
563+
''',
564+
(),
565+
id="property return value docstring no returns section",
566+
),
567+
pytest.param(
568+
'''
569+
class Class1:
570+
"""Docstring.
571+
572+
Attrs:
573+
function_1:
574+
"""
575+
@property
576+
async def function_1(self):
577+
"""Docstring 1."""
578+
return 1
579+
''',
580+
(),
581+
id="async property return value docstring no returns section",
582+
),
583+
pytest.param(
584+
'''
585+
class Class1:
586+
"""Docstring.
587+
588+
Attrs:
589+
function_1:
590+
"""
591+
@property()
592+
def function_1(self):
593+
"""Docstring 1."""
594+
return 1
595+
''',
596+
(),
597+
id="property call return value docstring no returns section",
598+
),
599+
pytest.param(
600+
'''
553601
def function_1():
554602
"""Docstring 1."""
555603
yield
@@ -647,6 +695,22 @@ def function_1(self):
647695
(),
648696
id="method yield value docstring yields section",
649697
),
698+
pytest.param(
699+
'''
700+
class Class1:
701+
"""Docstring.
702+
703+
Attrs:
704+
function_1:
705+
"""
706+
@property
707+
def function_1(self):
708+
"""Docstring 1."""
709+
yield 1
710+
''',
711+
(),
712+
id="property yield value docstring no yields section",
713+
),
650714
],
651715
)
652716
def test_plugin(code: str, expected_result: tuple[str, ...]):

tests/unit/test___init__attrs.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,70 @@ class Class1:
186186
class Class1:
187187
"""Docstring 1.
188188
189+
Attrs:
190+
"""
191+
@property
192+
def attr_1():
193+
"""Docstring 2."""
194+
return "value 1"
195+
''',
196+
(f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",),
197+
id="class has single property docstring no attr",
198+
),
199+
pytest.param(
200+
'''
201+
class Class1:
202+
"""Docstring 1.
203+
204+
Attrs:
205+
"""
206+
@property
207+
def attr_1(self):
208+
"""Docstring 2."""
209+
self.attr_2 = "value 2"
210+
return "value 1"
211+
''',
212+
(
213+
f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",
214+
f"10:8 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}",
215+
),
216+
id="class has single property with assignment docstring no attr",
217+
),
218+
pytest.param(
219+
'''
220+
class Class1:
221+
"""Docstring 1.
222+
223+
Attrs:
224+
"""
225+
@property
226+
async def attr_1():
227+
"""Docstring 2."""
228+
return "value 1"
229+
''',
230+
(f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",),
231+
id="class has single async property docstring no attr",
232+
),
233+
pytest.param(
234+
'''
235+
class Class1:
236+
"""Docstring 1.
237+
238+
Attrs:
239+
"""
240+
@property()
241+
def attr_1():
242+
"""Docstring 2."""
243+
return "value 1"
244+
''',
245+
(f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",),
246+
id="class has single property call docstring no attr",
247+
),
248+
pytest.param(
249+
'''
250+
class Class1:
251+
"""Docstring 1.
252+
189253
Attrs:
190254
"""
191255
def __init__(self):
@@ -273,6 +337,28 @@ def method_1(self):
273337
class Class1:
274338
"""Docstring 1.
275339
340+
Attrs:
341+
"""
342+
@property
343+
def attr_1():
344+
"""Docstring 2."""
345+
return "value 1"
346+
@property
347+
def attr_2():
348+
"""Docstring 3."""
349+
return "value 3"
350+
''',
351+
(
352+
f"8:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_1'}",
353+
f"12:4 {ATTR_NOT_IN_DOCSTR_MSG % 'attr_2'}",
354+
),
355+
id="class has multiple property docstring no attr",
356+
),
357+
pytest.param(
358+
'''
359+
class Class1:
360+
"""Docstring 1.
361+
276362
Attrs:
277363
"""
278364
def method_1(self):
@@ -541,6 +627,22 @@ class Class1:
541627
class Class1:
542628
"""Docstring 1.
543629
630+
Attrs:
631+
attr_1:
632+
"""
633+
@property
634+
def attr_1():
635+
"""Docstring 2."""
636+
return "value 1"
637+
''',
638+
(),
639+
id="class single property docstring single attr",
640+
),
641+
pytest.param(
642+
'''
643+
class Class1:
644+
"""Docstring 1.
645+
544646
Attrs:
545647
attr_1:
546648
"""

0 commit comments

Comments
 (0)