Skip to content

Commit d9e7b65

Browse files
committed
Refactor flake8 plugin checks and improve test cases
1 parent 3f3f65f commit d9e7b65

5 files changed

Lines changed: 46 additions & 37 deletions

File tree

src/community_of_python_flake8_plugin/checks/disabled/module_import_many_names.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
"""Check is disabled because it doesn't work when flake8 invoked directly, not as python module (.venv/bin/flake8 doesn't work, python -m flake8 works)."""
1+
"""Check is disabled because it doesn't work when flake8 invoked directly, not as python module.
2+
3+
(.venv/bin/flake8 doesn't work, python -m flake8 works).
4+
5+
6+
"""
27

38
from __future__ import annotations
49
import ast
@@ -32,12 +37,12 @@ def check_module_path_exists(module_name: str) -> bool:
3237
return importlib_util.find_spec(module_name) is not None
3338

3439
parent, _, _child = module_name.rpartition(".")
35-
parent_spec = importlib_util.find_spec(parent) # does not execute parent
40+
parent_spec: typing.Final = importlib_util.find_spec(parent) # does not execute parent
3641
if parent_spec is None:
3742
return False
3843

3944
# Parent must be a package (have places to search for submodules)
40-
locations = parent_spec.submodule_search_locations
45+
locations: typing.Final = parent_spec.submodule_search_locations
4146
if locations is None:
4247
return False
4348

src/community_of_python_flake8_plugin/checks/function_verb.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ def check_is_property(function_node: ast.AST) -> bool:
2626
return any(check_is_property_decorator(decorator) for decorator in function_node.decorator_list) # noqa: COP011
2727

2828

29-
def check_is_property_decorator(decorator: ast.expr) -> bool:
30-
# Handle direct name references like @property or @cached_property
29+
def check_is_property_decorator(decorator: ast.expr) -> bool: # noqa: PLR0911
3130
if isinstance(decorator, ast.Name):
3231
return decorator.id in {"property", "cached_property"}
3332

@@ -42,9 +41,13 @@ def check_is_property_decorator(decorator: ast.expr) -> bool:
4241
if isinstance(decorator.func, ast.Name):
4342
return decorator.func.id in {"property", "cached_property"}
4443
if isinstance(decorator.func, ast.Attribute):
44+
if (
45+
decorator.func.attr in {"property", "setter", "cached_property"}
46+
and isinstance(decorator.func.value, ast.Name)
47+
and decorator.func.value.id == "functools"
48+
):
49+
return decorator.func.attr == "cached_property"
4550
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"
4851
return decorator.func.attr in {"property", "setter"}
4952

5053
return False

src/community_of_python_flake8_plugin/checks/name_length.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,14 @@ def validate_name_length(self, identifier: str, ast_node: ast.stmt, parent_class
9292

9393
if len(identifier) < MIN_NAME_LENGTH:
9494
# Determine if this is an attribute (inside a class) or variable (at module level)
95-
is_attribute = parent_class is not None
9695
self.violations.append(
9796
Violation(
9897
line_number=ast_node.lineno,
9998
column_number=ast_node.col_offset,
10099
violation_code=(
101-
ViolationCodes.ATTRIBUTE_NAME_LENGTH if is_attribute else ViolationCodes.VARIABLE_NAME_LENGTH
100+
ViolationCodes.ATTRIBUTE_NAME_LENGTH
101+
if parent_class is not None
102+
else ViolationCodes.VARIABLE_NAME_LENGTH
102103
),
103104
)
104105
)

src/community_of_python_flake8_plugin/checks/temp_var.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,21 @@
77
from community_of_python_flake8_plugin.violations import Violation
88

99

10-
def collect_variable_usage_and_stores(function_node: ast.AST) -> tuple[dict[str, list[ast.Name]], set[str]]:
11-
variable_usage: typing.Final[defaultdict[str, list[ast.Name]]] = defaultdict(list)
12-
assigned_variable_names: typing.Final[set[str]] = set()
10+
def is_tuple_unpacking(assign_node: ast.Assign) -> bool:
11+
return bool(assign_node.targets and isinstance(assign_node.targets[0], ast.Tuple))
12+
1313

14-
def extract_names(expression: ast.expr) -> typing.Iterable[str]:
15-
if isinstance(expression, ast.Name):
16-
yield expression.id
17-
elif isinstance(expression, ast.Tuple):
18-
for elt in expression.elts:
19-
yield from extract_names(elt)
14+
def extract_names(expression: ast.expr) -> typing.Iterable[str]:
15+
if isinstance(expression, ast.Name):
16+
yield expression.id
17+
elif isinstance(expression, ast.Tuple):
18+
for elt in expression.elts:
19+
yield from extract_names(elt)
2020

21-
def is_tuple_unpacking(assign_node: ast.Assign) -> bool:
22-
"""Check if this is a tuple unpacking assignment."""
23-
if not assign_node.targets:
24-
return False
2521

26-
target = assign_node.targets[0]
27-
return isinstance(target, ast.Tuple)
22+
def collect_variable_usage_and_stores(function_node: ast.AST) -> tuple[dict[str, list[ast.Name]], set[str]]:
23+
variable_usage: typing.Final[defaultdict[str, list[ast.Name]]] = defaultdict(list)
24+
assigned_variable_names: typing.Final[set[str]] = set()
2825

2926
@typing.final
3027
class UsageCollector(ast.NodeVisitor):
@@ -34,9 +31,12 @@ def visit_Name(self, name_node: ast.Name) -> None:
3431

3532
def visit_Assign(self, assign_node: ast.Assign) -> None:
3633
# Skip collecting variables from tuple unpacking assignments
37-
if not is_tuple_unpacking(assign_node):
38-
for target in assign_node.targets:
39-
assigned_variable_names.update(extract_names(target))
34+
if is_tuple_unpacking(assign_node):
35+
self.generic_visit(assign_node)
36+
return
37+
38+
for target in assign_node.targets:
39+
assigned_variable_names.update(extract_names(target))
4040
self.generic_visit(assign_node)
4141

4242
def visit_AugAssign(self, aug_assign_node: ast.AugAssign) -> None:

tests/test_plugin.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ def test_type_annotation_validations(input_source: str, expected_output: list[st
184184
),
185185
# No violation: ModelFactory methods should be exempt from COP009
186186
(
187-
"from polyfactory.factories.pydantic_factory import ModelFactory\nclass MyFactory(ModelFactory):\n def calculator(self): pass",
187+
"from polyfactory.factories.pydantic_factory import ModelFactory\n"
188+
"class MyFactory(ModelFactory):\n def calculator(self): pass",
188189
["COP012"],
189190
),
190191
# No violation: ModelFactory generic methods should be exempt from COP009
@@ -205,21 +206,20 @@ def test_type_annotation_validations(input_source: str, expected_output: list[st
205206
"class MyFactory(ModelFactory[some_module.SomeClass]):\n @classmethod\n def create(cls): pass",
206207
["COP012"],
207208
),
208-
# No violation: cached_property imported directly should exempt function from COP009 (but triggers COP002 for import style)
209+
# No violation: cached_property imported directly should exempt function from COP009
210+
# (but triggers COP002 for import style)
209211
(
210-
"from functools import cached_property\nclass ExampleClass:\n @cached_property\n def calculator(self): pass",
212+
"from functools import cached_property\n"
213+
"class ExampleClass:\n @cached_property\n def calculator(self): pass",
211214
["COP002", "COP012"],
212215
),
213-
# No violation: cached_property() imported directly should exempt function from COP009 (but triggers COP002)
216+
# No violation: cached_property() imported directly should exempt function from COP009
217+
# (but triggers COP002)
214218
(
215-
"from functools import cached_property\nclass ExampleClass:\n @cached_property()\n def calculator(self): pass",
219+
"from functools import cached_property\n"
220+
"class ExampleClass:\n @cached_property()\n def calculator(self): pass",
216221
["COP002", "COP012"],
217222
),
218-
# No violation: pytest fixture with name parameter should be exempt from COP009
219-
(
220-
"import pytest\n@pytest.fixture(name='events')\ndef fixture_events() -> list[dict]:\n return []",
221-
[],
222-
),
223223
# COP009: faker.Faker annotation doesn't exempt function naming rules
224224
(
225225
"import faker\ndef some_func(arg: faker.Faker): pass",

0 commit comments

Comments
 (0)