Skip to content

Commit 8aa4206

Browse files
committed
Enhance property decorator detection and add clear verb prefix
1 parent 9cf2001 commit 8aa4206

4 files changed

Lines changed: 53 additions & 5 deletions

File tree

src/community_of_python_flake8_plugin/checks/function_verb.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,26 @@ def check_is_property(function_node: ast.AST) -> bool:
2727

2828

2929
def check_is_property_decorator(decorator: ast.expr) -> bool:
30+
# Handle direct name references like @property or @cached_property
3031
if isinstance(decorator, ast.Name):
31-
return decorator.id == "property"
32+
return decorator.id in {"property", "cached_property"}
33+
34+
# Handle attribute references like @functools.cached_property
3235
if isinstance(decorator, ast.Attribute) and decorator.attr in {"property", "setter", "cached_property"}:
3336
if isinstance(decorator.value, ast.Name) and decorator.value.id == "functools":
3437
return decorator.attr == "cached_property"
3538
return decorator.attr in {"property", "setter"}
39+
40+
# Handle decorator calls like @property() or @functools.cached_property()
41+
if isinstance(decorator, ast.Call):
42+
if isinstance(decorator.func, ast.Name):
43+
return decorator.func.id in {"property", "cached_property"}
44+
if isinstance(decorator.func, ast.Attribute):
45+
if decorator.func.attr in {"property", "setter", "cached_property"}:
46+
if isinstance(decorator.func.value, ast.Name) and decorator.func.value.id == "functools":
47+
return decorator.func.attr == "cached_property"
48+
return decorator.func.attr in {"property", "setter"}
49+
3650
return False
3751

3852

src/community_of_python_flake8_plugin/checks/name_length.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,12 @@ def validate_name_length(self, identifier: str, ast_node: ast.stmt, parent_class
9393
if len(identifier) < MIN_NAME_LENGTH:
9494
# Determine if this is an attribute (inside a class) or variable (at module level)
9595
is_attribute = parent_class is not None
96-
9796
self.violations.append(
9897
Violation(
9998
line_number=ast_node.lineno,
10099
column_number=ast_node.col_offset,
101100
violation_code=(
102-
ViolationCodes.ATTRIBUTE_NAME_LENGTH
103-
if is_attribute
104-
else ViolationCodes.VARIABLE_NAME_LENGTH
101+
ViolationCodes.ATTRIBUTE_NAME_LENGTH if is_attribute else ViolationCodes.VARIABLE_NAME_LENGTH
105102
),
106103
)
107104
)

src/community_of_python_flake8_plugin/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"measure",
108108
"ensure",
109109
"submit",
110+
"clear",
110111
}
111112

112113
SCALAR_ANNOTATIONS: typing.Final = {"int", "str", "float", "bool", "bytes", "complex"}

tests/test_plugin.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def test_import_stdlib_validations(input_source: str, expected_output: list[str]
5050
[item[2].split(" ")[0] for item in CommunityOfPythonFlake8Plugin(ast.parse(input_source)).run()] # noqa: COP011
5151
) == sorted(expected_output)
5252

53+
5354
@pytest.mark.skip("disabled")
5455
@pytest.mark.parametrize(
5556
("input_source", "expected_output"),
@@ -161,6 +162,41 @@ def test_type_annotation_validations(input_source: str, expected_output: list[st
161162
"import pytest\n@pytest.fixture(name='events')\ndef fixture_events() -> list[dict]:\n return []",
162163
[],
163164
),
165+
# No violation: property decorator should exempt function from COP009 (but not COP007 for name length)
166+
(
167+
"class ExampleClass:\n @property\n def calculator(): pass",
168+
["COP012"],
169+
),
170+
# No violation: property() decorator call should exempt function from COP009 (but not COP007)
171+
(
172+
"class ExampleClass:\n @property()\n def calculator(): pass",
173+
["COP012"],
174+
),
175+
# No violation: cached_property decorator should exempt function from COP009 (but not COP007)
176+
(
177+
"import functools\nclass ExampleClass:\n @functools.cached_property\n def calculator(): pass",
178+
["COP012"],
179+
),
180+
# No violation: cached_property() decorator call should exempt function from COP009 (but not COP007)
181+
(
182+
"import functools\nclass ExampleClass:\n @functools.cached_property()\n def calculator(): pass",
183+
["COP012"],
184+
),
185+
# No violation: ModelFactory methods should be exempt from COP009
186+
(
187+
"from polyfactory.factories.pydantic_factory import ModelFactory\nclass MyFactory(ModelFactory):\n def calculator(self): pass",
188+
["COP012"],
189+
),
190+
# No violation: cached_property imported directly should exempt function from COP009 (but triggers COP002 for import style)
191+
(
192+
"from functools import cached_property\nclass ExampleClass:\n @cached_property\n def calculator(self): pass",
193+
["COP002", "COP012"],
194+
),
195+
# No violation: cached_property() imported directly should exempt function from COP009 (but triggers COP002)
196+
(
197+
"from functools import cached_property\nclass ExampleClass:\n @cached_property()\n def calculator(self): pass",
198+
["COP002", "COP012"],
199+
),
164200
# No violation: pytest fixture with name parameter should be exempt from COP009
165201
(
166202
"import pytest\n@pytest.fixture(name='events')\ndef fixture_events() -> list[dict]:\n return []",

0 commit comments

Comments
 (0)