community-of-python-flake8-plugin 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/PKG-INFO +1 -1
  2. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/pyproject.toml +1 -1
  3. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/final_class.py +14 -2
  4. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/function_verb.py +7 -6
  5. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/mapping_proxy.py +5 -5
  6. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/name_length.py +45 -36
  7. community_of_python_flake8_plugin-0.3.2/src/community_of_python_flake8_plugin/checks/temp_var.py +149 -0
  8. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/constants.py +2 -0
  9. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/plugin.py +10 -10
  10. community_of_python_flake8_plugin-0.3.2/src/community_of_python_flake8_plugin/utils.py +46 -0
  11. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/violation_codes.py +3 -1
  12. community_of_python_flake8_plugin-0.3.0/src/community_of_python_flake8_plugin/checks/temp_var.py +0 -89
  13. community_of_python_flake8_plugin-0.3.0/src/community_of_python_flake8_plugin/utils.py +0 -35
  14. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/README.md +0 -0
  15. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/__init__.py +0 -0
  16. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/__init__.py +0 -0
  17. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/async_get_prefix.py +0 -0
  18. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/dataclass_config.py +0 -0
  19. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/disabled/__init__.py +0 -0
  20. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/disabled/module_import_many_names.py +0 -0
  21. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/for_loop_one_prefix.py +0 -0
  22. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/module_import_stdlib.py +0 -0
  23. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/checks/scalar_annotation.py +0 -0
  24. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/py.typed +0 -0
  25. {community_of_python_flake8_plugin-0.3.0 → community_of_python_flake8_plugin-0.3.2}/src/community_of_python_flake8_plugin/violations.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: community-of-python-flake8-plugin
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Community of Python flake8 plugin
5
5
  Author: Lev Vereshchagin
6
6
  Author-email: Lev Vereshchagin <mail@vrslev.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "community-of-python-flake8-plugin"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Community of Python flake8 plugin"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Lev Vereshchagin", email = "mail@vrslev.com" }]
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
  import ast
3
3
  import typing
4
4
 
5
+ from community_of_python_flake8_plugin.utils import check_inherits_from_bases
5
6
  from community_of_python_flake8_plugin.violation_codes import ViolationCodes
6
7
  from community_of_python_flake8_plugin.violations import Violation
7
8
 
@@ -25,9 +26,20 @@ def is_protocol_class(class_node: ast.ClassDef) -> bool:
25
26
  # Check for attributed Protocol reference: class MyClass(typing.Protocol):
26
27
  if isinstance(one_base, ast.Attribute) and one_base.attr == "Protocol":
27
28
  return True
29
+ # Check for subscripted Protocol reference: class MyClass(Protocol[SomeType]):
30
+ if isinstance(one_base, ast.Subscript):
31
+ if isinstance(one_base.value, ast.Name) and one_base.value.id == "Protocol":
32
+ return True
33
+ if isinstance(one_base.value, ast.Attribute) and one_base.value.attr == "Protocol":
34
+ return True
28
35
  return False
29
36
 
30
37
 
38
+ def is_model_factory_class(class_node: ast.ClassDef) -> bool:
39
+ """Check if the class inherits from ModelFactory or SQLAlchemyFactory."""
40
+ return check_inherits_from_bases(class_node, {"ModelFactory", "SQLAlchemyFactory"})
41
+
42
+
31
43
  @typing.final
32
44
  class FinalClassCheck(ast.NodeVisitor):
33
45
  def __init__(self, syntax_tree: ast.AST) -> None: # noqa: ARG002
@@ -38,8 +50,8 @@ class FinalClassCheck(ast.NodeVisitor):
38
50
  self.generic_visit(ast_node)
39
51
 
40
52
  def _check_final_decorator(self, ast_node: ast.ClassDef) -> None:
41
- # Skip Protocol classes and test classes
42
- if is_protocol_class(ast_node) or ast_node.name.startswith("Test"):
53
+ # Skip Protocol classes, test classes, and ModelFactory classes
54
+ if is_protocol_class(ast_node) or ast_node.name.startswith("Test") or is_model_factory_class(ast_node):
43
55
  return
44
56
 
45
57
  if not contains_final_decorator(ast_node):
@@ -11,19 +11,20 @@ from community_of_python_flake8_plugin.violations import Violation
11
11
  def check_is_ignored_name(identifier: str) -> bool:
12
12
  if identifier == "main":
13
13
  return True
14
- if identifier.startswith("__") and identifier.endswith("__"):
15
- return True
16
- return bool(identifier.startswith("_"))
14
+ return bool(identifier.startswith("__") and identifier.endswith("__"))
17
15
 
18
16
 
19
17
  def check_is_verb_name(identifier: str) -> bool:
20
- return any(identifier == verb_name or identifier.startswith(f"{verb_name}_") for verb_name in VERB_PREFIXES)
18
+ return any(
19
+ identifier == one_verb_name or identifier.startswith((f"{one_verb_name}_", f"_{one_verb_name}"))
20
+ for one_verb_name in VERB_PREFIXES
21
+ )
21
22
 
22
23
 
23
24
  def check_is_property(function_node: ast.AST) -> bool:
24
25
  if not isinstance(function_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
25
26
  return False
26
- return any(check_is_property_decorator(decorator) for decorator in function_node.decorator_list) # noqa: COP011
27
+ return any(check_is_property_decorator(one_decorator) for one_decorator in function_node.decorator_list) # noqa: COP011
27
28
 
28
29
 
29
30
  def check_is_property_decorator(decorator: ast.expr) -> bool: # noqa: PLR0911
@@ -56,7 +57,7 @@ def check_is_property_decorator(decorator: ast.expr) -> bool: # noqa: PLR0911
56
57
  def check_is_pytest_fixture(function_node: ast.AST) -> bool:
57
58
  if not isinstance(function_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
58
59
  return False
59
- return any(check_is_fixture_decorator(decorator) for decorator in function_node.decorator_list) # noqa: COP011
60
+ return any(check_is_fixture_decorator(one_decorator) for one_decorator in function_node.decorator_list) # noqa: COP011
60
61
 
61
62
 
62
63
  def check_is_fixture_decorator(decorator: ast.expr) -> bool:
@@ -71,9 +71,9 @@ class MappingProxyCheck(ast.NodeVisitor):
71
71
  self.violations: list[Violation] = []
72
72
 
73
73
  def visit_Module(self, ast_node: ast.Module) -> None:
74
- for statement in ast_node.body:
75
- if isinstance(statement, (ast.Assign, ast.AnnAssign)):
76
- self._check_mapping_assignment(statement)
74
+ for one_statement in ast_node.body:
75
+ if isinstance(one_statement, (ast.Assign, ast.AnnAssign)):
76
+ self._check_mapping_assignment(one_statement)
77
77
  self.generic_visit(ast_node)
78
78
 
79
79
  def _check_mapping_assignment(self, ast_node: ast.Assign | ast.AnnAssign) -> None:
@@ -99,8 +99,8 @@ class MappingProxyCheck(ast.NodeVisitor):
99
99
  # Only check module-level assignments (no parent function/class)
100
100
  if assigned_value is not None and isinstance(assigned_value, ast.Dict) and assignment_targets:
101
101
  # Check if this is a module-level assignment
102
- for target in assignment_targets: # noqa: COP011
103
- if isinstance(target, ast.Name):
102
+ for one_target in assignment_targets: # noqa: COP011
103
+ if isinstance(one_target, ast.Name):
104
104
  self.violations.append(
105
105
  Violation(
106
106
  line_number=ast_node.lineno,
@@ -3,7 +3,11 @@ import ast
3
3
  import typing
4
4
 
5
5
  from community_of_python_flake8_plugin.constants import FINAL_CLASS_EXCLUDED_BASES, MIN_NAME_LENGTH
6
- from community_of_python_flake8_plugin.utils import check_inherits_from_bases, find_parent_class_definition
6
+ from community_of_python_flake8_plugin.utils import (
7
+ check_inherits_from_bases,
8
+ find_parent_class_definition,
9
+ find_parent_function_definition,
10
+ )
7
11
  from community_of_python_flake8_plugin.violation_codes import ViolationCodes
8
12
  from community_of_python_flake8_plugin.violations import Violation
9
13
 
@@ -33,7 +37,7 @@ def check_is_whitelisted_annotation(annotation: ast.expr | None) -> bool:
33
37
  def check_is_pytest_fixture(ast_node: ast.AST) -> bool:
34
38
  if not isinstance(ast_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
35
39
  return False
36
- return any(check_is_fixture_decorator(decorator) for decorator in ast_node.decorator_list) # noqa: COP011
40
+ return any(check_is_fixture_decorator(one_decorator) for one_decorator in ast_node.decorator_list) # noqa: COP011
37
41
 
38
42
 
39
43
  def check_is_fixture_decorator(decorator: ast.expr) -> bool:
@@ -58,9 +62,11 @@ class COP004NameLengthCheck(ast.NodeVisitor):
58
62
  self.generic_visit(ast_node)
59
63
 
60
64
  def visit_Assign(self, ast_node: ast.Assign) -> None:
61
- for target in ast_node.targets:
62
- if isinstance(target, ast.Name):
63
- self.validate_name_length(target.id, ast_node, find_parent_class_definition(self.syntax_tree, ast_node))
65
+ for one_target in ast_node.targets:
66
+ if isinstance(one_target, ast.Name):
67
+ self.validate_name_length(
68
+ one_target.id, ast_node, find_parent_class_definition(self.syntax_tree, ast_node)
69
+ )
64
70
  self.generic_visit(ast_node)
65
71
 
66
72
  def visit_FunctionDef(self, ast_node: ast.FunctionDef) -> None:
@@ -79,18 +85,18 @@ class COP004NameLengthCheck(ast.NodeVisitor):
79
85
  self.generic_visit(ast_node)
80
86
 
81
87
  def visit_ListComp(self, ast_node: ast.ListComp) -> None:
82
- for comprehension in ast_node.generators:
83
- self._validate_comprehension_target(comprehension.target)
88
+ for one_comprehension in ast_node.generators:
89
+ self._validate_comprehension_target(one_comprehension.target)
84
90
  self.generic_visit(ast_node)
85
91
 
86
92
  def visit_SetComp(self, ast_node: ast.SetComp) -> None:
87
- for comprehension in ast_node.generators:
88
- self._validate_comprehension_target(comprehension.target)
93
+ for one_comprehension in ast_node.generators:
94
+ self._validate_comprehension_target(one_comprehension.target)
89
95
  self.generic_visit(ast_node)
90
96
 
91
97
  def visit_DictComp(self, ast_node: ast.DictComp) -> None:
92
- for comprehension in ast_node.generators:
93
- self._validate_comprehension_target(comprehension.target)
98
+ for one_comprehension in ast_node.generators:
99
+ self._validate_comprehension_target(one_comprehension.target)
94
100
  self.generic_visit(ast_node)
95
101
 
96
102
  def visit_Lambda(self, ast_node: ast.Lambda) -> None:
@@ -98,9 +104,9 @@ class COP004NameLengthCheck(ast.NodeVisitor):
98
104
  self.generic_visit(ast_node)
99
105
 
100
106
  def visit_With(self, ast_node: ast.With) -> None:
101
- for item in ast_node.items:
102
- if item.optional_vars is not None:
103
- self._validate_with_target(item.optional_vars)
107
+ for one_item in ast_node.items:
108
+ if one_item.optional_vars is not None:
109
+ self._validate_with_target(one_item.optional_vars)
104
110
  self.generic_visit(ast_node)
105
111
 
106
112
  def visit_ExceptHandler(self, ast_node: ast.ExceptHandler) -> None:
@@ -109,18 +115,18 @@ class COP004NameLengthCheck(ast.NodeVisitor):
109
115
  self.generic_visit(ast_node)
110
116
 
111
117
  def visit_GeneratorExp(self, ast_node: ast.GeneratorExp) -> None:
112
- for comprehension in ast_node.generators:
113
- self._validate_comprehension_target(comprehension.target)
118
+ for one_comprehension in ast_node.generators:
119
+ self._validate_comprehension_target(one_comprehension.target)
114
120
  self.generic_visit(ast_node)
115
121
 
116
122
  def _validate_function_args(self, arguments_node: ast.arguments) -> None:
117
123
  # Process all argument types
118
- for argument in arguments_node.posonlyargs:
119
- self._validate_argument_name_length(argument)
120
- for argument in arguments_node.args:
121
- self._validate_argument_name_length(argument)
122
- for argument in arguments_node.kwonlyargs:
123
- self._validate_argument_name_length(argument)
124
+ for one_argument in arguments_node.posonlyargs:
125
+ self._validate_argument_name_length(one_argument)
126
+ for one_argument in arguments_node.args:
127
+ self._validate_argument_name_length(one_argument)
128
+ for one_argument in arguments_node.kwonlyargs:
129
+ self._validate_argument_name_length(one_argument)
124
130
 
125
131
  if arguments_node.vararg is not None:
126
132
  self._validate_argument_name_length(arguments_node.vararg)
@@ -157,8 +163,8 @@ class COP004NameLengthCheck(ast.NodeVisitor):
157
163
  )
158
164
  elif isinstance(comprehension_target, ast.Tuple):
159
165
  # Handle tuple unpacking in comprehensions like [(a, b) for a, b in pairs]
160
- for elt in comprehension_target.elts:
161
- self._validate_comprehension_target(elt)
166
+ for one_elt in comprehension_target.elts:
167
+ self._validate_comprehension_target(one_elt)
162
168
 
163
169
  def _validate_with_target(self, target_node: ast.expr) -> None:
164
170
  if isinstance(target_node, ast.Name):
@@ -173,8 +179,8 @@ class COP004NameLengthCheck(ast.NodeVisitor):
173
179
  )
174
180
  elif isinstance(target_node, ast.Tuple):
175
181
  # Handle tuple unpacking in with statements like with open(f1) as f1, open(f2) as f2:
176
- for elt in target_node.elts:
177
- self._validate_with_target(elt)
182
+ for one_elt in target_node.elts:
183
+ self._validate_with_target(one_elt)
178
184
 
179
185
  def _validate_except_target(self, ast_node: ast.ExceptHandler) -> None:
180
186
  # For except targets, we'll treat them as variables
@@ -204,15 +210,18 @@ class COP004NameLengthCheck(ast.NodeVisitor):
204
210
  return
205
211
 
206
212
  if len(identifier) < MIN_NAME_LENGTH:
207
- # Determine if this is an attribute (inside a class) or variable (at module level)
213
+ # Determine if this is an attribute (inside a class but not in a method) or variable
214
+ parent_function: typing.Final = find_parent_function_definition(self.syntax_tree, ast_node)
215
+
216
+ # It's an attribute only if it's in a class but NOT in a function/method
217
+ is_attribute: typing.Final = parent_class is not None and parent_function is None
218
+
208
219
  self.violations.append(
209
220
  Violation(
210
221
  line_number=ast_node.lineno,
211
222
  column_number=ast_node.col_offset,
212
223
  violation_code=(
213
- ViolationCodes.ATTRIBUTE_NAME_LENGTH
214
- if parent_class is not None
215
- else ViolationCodes.VARIABLE_NAME_LENGTH
224
+ ViolationCodes.ATTRIBUTE_NAME_LENGTH if is_attribute else ViolationCodes.VARIABLE_NAME_LENGTH
216
225
  ),
217
226
  )
218
227
  )
@@ -240,12 +249,12 @@ class COP004NameLengthCheck(ast.NodeVisitor):
240
249
 
241
250
  def validate_function_args(self, ast_node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
242
251
  # Process all argument types
243
- for argument in ast_node.args.posonlyargs:
244
- self.validate_argument_name_length(argument)
245
- for argument in ast_node.args.args:
246
- self.validate_argument_name_length(argument)
247
- for argument in ast_node.args.kwonlyargs:
248
- self.validate_argument_name_length(argument)
252
+ for one_argument in ast_node.args.posonlyargs:
253
+ self.validate_argument_name_length(one_argument)
254
+ for one_argument in ast_node.args.args:
255
+ self.validate_argument_name_length(one_argument)
256
+ for one_argument in ast_node.args.kwonlyargs:
257
+ self.validate_argument_name_length(one_argument)
249
258
 
250
259
  if ast_node.args.vararg is not None:
251
260
  self.validate_argument_name_length(ast_node.args.vararg)
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import typing
4
+ from collections import defaultdict
5
+
6
+ from community_of_python_flake8_plugin.violation_codes import ViolationCodes
7
+ from community_of_python_flake8_plugin.violations import Violation
8
+
9
+
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
+
13
+
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 one_elt in expression.elts:
19
+ yield from extract_names(one_elt)
20
+
21
+
22
+ def is_single_line_assignment(assign_node: ast.Assign | ast.AnnAssign) -> bool:
23
+ """Check if assignment value fits on a single line."""
24
+ # For simple values, they're always single line
25
+ if isinstance(assign_node.value, (ast.Constant, ast.Name, ast.Attribute)):
26
+ return True
27
+
28
+ # For complex expressions, check if they span multiple lines
29
+ # We'll consider it multi-line if the end line is different from start line
30
+ if hasattr(assign_node, "lineno") and assign_node.value is not None and hasattr(assign_node.value, "end_lineno"):
31
+ return assign_node.lineno == assign_node.value.end_lineno
32
+
33
+ return True
34
+
35
+
36
+ def collect_variable_usage_and_stores_with_nodes(
37
+ function_node: ast.AST,
38
+ ) -> tuple[dict[str, list[ast.Name]], set[str], dict[str, ast.Assign | ast.AnnAssign]]:
39
+ variable_usage: typing.Final[defaultdict[str, list[ast.Name]]] = defaultdict(list)
40
+ assigned_variable_names: typing.Final[set[str]] = set()
41
+ variable_assignments: typing.Final[dict[str, ast.Assign | ast.AnnAssign]] = {}
42
+
43
+ @typing.final
44
+ class UsageCollector(ast.NodeVisitor):
45
+ def visit_Name(self, name_node: ast.Name) -> None:
46
+ variable_usage[name_node.id].append(name_node)
47
+ self.generic_visit(name_node)
48
+
49
+ def visit_Assign(self, assign_node: ast.Assign) -> None:
50
+ # Skip collecting variables from tuple unpacking assignments
51
+ if is_tuple_unpacking(assign_node):
52
+ self.generic_visit(assign_node)
53
+ return
54
+
55
+ for one_target in assign_node.targets:
56
+ one_names = list(extract_names(one_target))
57
+ assigned_variable_names.update(one_names)
58
+ # Store the assignment node for each variable
59
+ for one_name in one_names:
60
+ variable_assignments[one_name] = assign_node
61
+ self.generic_visit(assign_node)
62
+
63
+ def visit_AugAssign(self, aug_assign_node: ast.AugAssign) -> None:
64
+ assigned_variable_names.update(list(extract_names(aug_assign_node.target)))
65
+ self.generic_visit(aug_assign_node)
66
+
67
+ def visit_AnnAssign(self, ann_assign_node: ast.AnnAssign) -> None:
68
+ extracted_names: typing.Final = list(extract_names(ann_assign_node.target)) # noqa: COP011
69
+ assigned_variable_names.update(extracted_names)
70
+ # Store the assignment node for each variable
71
+ for one_name in extracted_names:
72
+ variable_assignments[one_name] = ann_assign_node
73
+ self.generic_visit(ann_assign_node)
74
+
75
+ UsageCollector().visit(function_node)
76
+ return dict(variable_usage), assigned_variable_names, variable_assignments
77
+
78
+
79
+ def is_used_in_next_line(assign_node: ast.Assign | ast.AnnAssign, usage_nodes: list[ast.Name]) -> bool:
80
+ """Check if variable is used in the immediate next line."""
81
+ if not usage_nodes:
82
+ return False
83
+
84
+ # Find the load usage (actual use of the variable)
85
+ load_usages: typing.Final = [one_node for one_node in usage_nodes if isinstance(one_node.ctx, ast.Load)]
86
+ if not load_usages:
87
+ return False
88
+
89
+ first_load: typing.Final = load_usages[0]
90
+
91
+ # Check if the usage is on the line immediately following the assignment
92
+ return first_load.lineno == assign_node.lineno + 1
93
+
94
+
95
+ @typing.final
96
+ class TempVarCheck(ast.NodeVisitor):
97
+ def __init__(self, syntax_tree: ast.AST) -> None: # noqa: ARG002
98
+ self.violations: list[Violation] = []
99
+
100
+ def visit_FunctionDef(self, ast_node: ast.FunctionDef) -> None:
101
+ self._check_temporary_variables(ast_node)
102
+ self.generic_visit(ast_node)
103
+
104
+ def visit_AsyncFunctionDef(self, ast_node: ast.AsyncFunctionDef) -> None:
105
+ self._check_temporary_variables(ast_node)
106
+ self.generic_visit(ast_node)
107
+
108
+ def _check_temporary_variables(self, ast_node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
109
+ usage_and_stores: typing.Final = collect_variable_usage_and_stores_with_nodes(ast_node)
110
+ variable_usages: typing.Final = usage_and_stores[0]
111
+ assigned_variable_names: typing.Final = usage_and_stores[1]
112
+ variable_assignments: typing.Final = usage_and_stores[2]
113
+
114
+ for variable_name, usages in variable_usages.items():
115
+ if variable_name.startswith("_") or variable_name in {"self", "cls"}:
116
+ continue
117
+
118
+ store_count = len(
119
+ [one_usage_element for one_usage_element in usages if isinstance(one_usage_element.ctx, ast.Store)]
120
+ )
121
+ load_count = len(
122
+ [one_usage_element for one_usage_element in usages if isinstance(one_usage_element.ctx, ast.Load)]
123
+ )
124
+
125
+ # Check if variable is assigned once and used once
126
+ if store_count == 1 and load_count == 1 and variable_name in assigned_variable_names:
127
+ # Get the assignment node
128
+ assign_node = variable_assignments.get(variable_name)
129
+ if not assign_node:
130
+ continue
131
+
132
+ # Check if it's a single-line assignment
133
+ if not is_single_line_assignment(assign_node):
134
+ continue
135
+
136
+ # Check if it's used in the next line
137
+ if is_used_in_next_line(assign_node, usages):
138
+ # Only flag the variable that is assigned and then immediately used
139
+ # Find the store usage (assignment)
140
+ store_usages = [one_node for one_node in usages if isinstance(one_node.ctx, ast.Store)]
141
+ if store_usages:
142
+ first_store = store_usages[0]
143
+ self.violations.append(
144
+ Violation(
145
+ line_number=first_store.lineno,
146
+ column_number=first_store.col_offset,
147
+ violation_code=ViolationCodes.TEMP_VAR,
148
+ )
149
+ )
@@ -109,6 +109,8 @@ VERB_PREFIXES: typing.Final = {
109
109
  "submit",
110
110
  "clear",
111
111
  "undo",
112
+ "cache",
113
+ "fill",
112
114
  }
113
115
 
114
116
  SCALAR_ANNOTATIONS: typing.Final = {"int", "str", "float", "bool", "bytes", "complex"}
@@ -31,23 +31,23 @@ class CommunityOfPythonFlake8Plugin:
31
31
  self.ast_syntax_tree: typing.Final[ast.AST] = tree
32
32
 
33
33
  def run(self) -> Iterable[tuple[int, int, str, type[object]]]: # noqa: COP007
34
- for check_instance in self._collect_checks():
35
- for violation in check_instance.violations:
34
+ for one_check_instance in self._collect_checks():
35
+ for one_violation in one_check_instance.violations:
36
36
  yield (
37
- violation.line_number,
38
- violation.column_number,
39
- f"{violation.violation_code.code} {violation.violation_code.description}",
37
+ one_violation.line_number,
38
+ one_violation.column_number,
39
+ f"{one_violation.violation_code.code} {one_violation.violation_code.description}",
40
40
  type(self),
41
41
  )
42
42
 
43
43
  def _collect_checks(self) -> list[PluginCheckProtocol]:
44
44
  checks_collection: typing.Final = []
45
- for _, module_name, _ in pkgutil.iter_modules(checks_module.__path__):
46
- imported_module = importlib.import_module(f"{checks_module.__name__}.{module_name}")
45
+ for _, one_module_name, _ in pkgutil.iter_modules(checks_module.__path__):
46
+ imported_module = importlib.import_module(f"{checks_module.__name__}.{one_module_name}")
47
47
 
48
- for attribute_name in dir(imported_module):
49
- attribute = getattr(imported_module, attribute_name)
50
- if isinstance(attribute, type) and attribute_name.endswith("Check") and hasattr(attribute, "visit"):
48
+ for one_attribute_name in dir(imported_module):
49
+ attribute = getattr(imported_module, one_attribute_name)
50
+ if isinstance(attribute, type) and one_attribute_name.endswith("Check") and hasattr(attribute, "visit"):
51
51
  check_instance = attribute(self.ast_syntax_tree)
52
52
  check_instance.visit(self.ast_syntax_tree)
53
53
  checks_collection.append(check_instance)
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+ import ast
3
+
4
+
5
+ def find_parent_class_definition(syntax_tree: ast.AST, target_node: ast.AST) -> ast.ClassDef | None:
6
+ for one_potential_parent in ast.walk(syntax_tree):
7
+ if isinstance(one_potential_parent, ast.ClassDef):
8
+ for one_child_node in ast.walk(one_potential_parent):
9
+ if one_child_node is target_node:
10
+ return one_potential_parent
11
+ return None
12
+
13
+
14
+ def find_parent_function_definition(
15
+ syntax_tree: ast.AST, target_node: ast.AST
16
+ ) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
17
+ for one_potential_parent in ast.walk(syntax_tree):
18
+ if isinstance(one_potential_parent, (ast.FunctionDef, ast.AsyncFunctionDef)):
19
+ for one_child_node in ast.walk(one_potential_parent):
20
+ if one_child_node is target_node:
21
+ return one_potential_parent
22
+ return None
23
+
24
+
25
+ def check_inherits_from_bases(class_definition: ast.ClassDef, base_classes: set[str]) -> bool:
26
+ for one_base_class in class_definition.bases:
27
+ if isinstance(one_base_class, ast.Name) and one_base_class.id in base_classes:
28
+ return True
29
+ if isinstance(one_base_class, ast.Attribute) and one_base_class.attr in base_classes:
30
+ return True
31
+ # Handle generic types like ModelFactory[SomeType]
32
+ if isinstance(one_base_class, ast.Subscript):
33
+ if isinstance(one_base_class.value, ast.Name) and one_base_class.value.id in base_classes:
34
+ return True
35
+ if isinstance(one_base_class.value, ast.Attribute) and one_base_class.value.attr in base_classes:
36
+ return True
37
+ return False
38
+
39
+
40
+ def find_parent_node(syntax_tree: ast.AST, target_node: ast.AST, node_types: tuple[type, ...]) -> ast.AST | None:
41
+ for one_potential_parent in ast.walk(syntax_tree):
42
+ if isinstance(one_potential_parent, node_types):
43
+ for one_child_node in ast.walk(one_potential_parent):
44
+ if one_child_node is target_node:
45
+ return one_potential_parent
46
+ return None
@@ -35,7 +35,9 @@ class ViolationCodes:
35
35
  ASYNC_GET_PREFIX = ViolationCodeItem(code="COP010", description="Avoid get_ prefix in async function names")
36
36
 
37
37
  # Variable usage violations
38
- TEMP_VAR = ViolationCodeItem(code="COP011", description="Inline variables that are used only once")
38
+ TEMP_VAR = ViolationCodeItem(
39
+ code="COP011", description="Inline those temporary variables that are used only once and close to assignment"
40
+ )
39
41
 
40
42
  # Class related violations
41
43
  FINAL_CLASS = ViolationCodeItem(code="COP012", description="Classes must be marked final with @typing.final")
@@ -1,89 +0,0 @@
1
- from __future__ import annotations
2
- import ast
3
- import typing
4
- from collections import defaultdict
5
-
6
- from community_of_python_flake8_plugin.violation_codes import ViolationCodes
7
- from community_of_python_flake8_plugin.violations import Violation
8
-
9
-
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
-
13
-
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)
20
-
21
-
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()
25
-
26
- @typing.final
27
- class UsageCollector(ast.NodeVisitor):
28
- def visit_Name(self, name_node: ast.Name) -> None:
29
- variable_usage[name_node.id].append(name_node)
30
- self.generic_visit(name_node)
31
-
32
- def visit_Assign(self, assign_node: ast.Assign) -> None:
33
- # Skip collecting variables from tuple unpacking assignments
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))
40
- self.generic_visit(assign_node)
41
-
42
- def visit_AugAssign(self, aug_assign_node: ast.AugAssign) -> None:
43
- assigned_variable_names.update(extract_names(aug_assign_node.target))
44
- self.generic_visit(aug_assign_node)
45
-
46
- def visit_AnnAssign(self, ann_assign_node: ast.AnnAssign) -> None:
47
- assigned_variable_names.update(extract_names(ann_assign_node.target))
48
- self.generic_visit(ann_assign_node)
49
-
50
- UsageCollector().visit(function_node)
51
- return dict(variable_usage), assigned_variable_names
52
-
53
-
54
- @typing.final
55
- class TempVarCheck(ast.NodeVisitor):
56
- def __init__(self, syntax_tree: ast.AST) -> None: # noqa: ARG002
57
- self.violations: list[Violation] = []
58
-
59
- def visit_FunctionDef(self, ast_node: ast.FunctionDef) -> None:
60
- self._check_temporary_variables(ast_node)
61
- self.generic_visit(ast_node)
62
-
63
- def visit_AsyncFunctionDef(self, ast_node: ast.AsyncFunctionDef) -> None:
64
- self._check_temporary_variables(ast_node)
65
- self.generic_visit(ast_node)
66
-
67
- def _check_temporary_variables(self, ast_node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
68
- usage_and_stores: typing.Final = collect_variable_usage_and_stores(ast_node)
69
- found_temporary_variable = False
70
-
71
- for variable_name, usages in usage_and_stores[0].items():
72
- if variable_name.startswith("_") or variable_name in {"self", "cls"}:
73
- continue
74
-
75
- if (
76
- len([usage_element for usage_element in usages if isinstance(usage_element.ctx, ast.Store)]) == 1
77
- and len([usage_element for usage_element in usages if isinstance(usage_element.ctx, ast.Load)]) == 1
78
- and variable_name in usage_and_stores[1]
79
- and not found_temporary_variable
80
- ):
81
- first_usage = usages[0]
82
- self.violations.append(
83
- Violation(
84
- line_number=first_usage.lineno,
85
- column_number=first_usage.col_offset,
86
- violation_code=ViolationCodes.TEMP_VAR,
87
- )
88
- )
89
- found_temporary_variable = True
@@ -1,35 +0,0 @@
1
- from __future__ import annotations
2
- import ast
3
-
4
-
5
- def find_parent_class_definition(syntax_tree: ast.AST, target_node: ast.AST) -> ast.ClassDef | None:
6
- for potential_parent in ast.walk(syntax_tree):
7
- if isinstance(potential_parent, ast.ClassDef):
8
- for child_node in ast.walk(potential_parent):
9
- if child_node is target_node:
10
- return potential_parent
11
- return None
12
-
13
-
14
- def check_inherits_from_bases(class_definition: ast.ClassDef, base_classes: set[str]) -> bool:
15
- for base_class in class_definition.bases:
16
- if isinstance(base_class, ast.Name) and base_class.id in base_classes:
17
- return True
18
- if isinstance(base_class, ast.Attribute) and base_class.attr in base_classes:
19
- return True
20
- # Handle generic types like ModelFactory[SomeType]
21
- if isinstance(base_class, ast.Subscript):
22
- if isinstance(base_class.value, ast.Name) and base_class.value.id in base_classes:
23
- return True
24
- if isinstance(base_class.value, ast.Attribute) and base_class.value.attr in base_classes:
25
- return True
26
- return False
27
-
28
-
29
- def find_parent_node(syntax_tree: ast.AST, target_node: ast.AST, node_types: tuple[type, ...]) -> ast.AST | None:
30
- for potential_parent in ast.walk(syntax_tree):
31
- if isinstance(potential_parent, node_types):
32
- for child_node in ast.walk(potential_parent):
33
- if child_node is target_node:
34
- return potential_parent
35
- return None