community-of-python-flake8-plugin 0.2.0__tar.gz → 0.3.0__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 (23) hide show
  1. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/PKG-INFO +1 -1
  2. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/pyproject.toml +1 -1
  3. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/dataclass_config.py +17 -13
  4. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/disabled/module_import_many_names.py +10 -9
  5. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/final_class.py +5 -5
  6. community_of_python_flake8_plugin-0.3.0/src/community_of_python_flake8_plugin/checks/for_loop_one_prefix.py +121 -0
  7. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/function_verb.py +1 -1
  8. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/mapping_proxy.py +52 -0
  9. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/name_length.py +113 -0
  10. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/temp_var.py +2 -2
  11. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/violation_codes.py +5 -0
  12. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/README.md +0 -0
  13. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/__init__.py +0 -0
  14. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/__init__.py +0 -0
  15. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/async_get_prefix.py +0 -0
  16. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/disabled/__init__.py +0 -0
  17. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/module_import_stdlib.py +0 -0
  18. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/checks/scalar_annotation.py +0 -0
  19. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/constants.py +0 -0
  20. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/plugin.py +0 -0
  21. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/py.typed +0 -0
  22. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/src/community_of_python_flake8_plugin/utils.py +0 -0
  23. {community_of_python_flake8_plugin-0.2.0 → community_of_python_flake8_plugin-0.3.0}/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.2.0
3
+ Version: 0.3.0
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.2.0"
3
+ version = "0.3.0"
4
4
  description = "Community of Python flake8 plugin"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Lev Vereshchagin", email = "mail@vrslev.com" }]
@@ -24,7 +24,11 @@ def has_required_dataclass_params(decorator: ast.expr) -> bool:
24
24
  if not isinstance(decorator, ast.Call):
25
25
  return False
26
26
 
27
- keywords: typing.Final = {kw.arg: kw.value for kw in decorator.keywords if isinstance(kw.value, ast.Constant)}
27
+ keywords: typing.Final = {
28
+ one_keyword_item.arg: one_keyword_item.value
29
+ for one_keyword_item in decorator.keywords
30
+ if isinstance(one_keyword_item.value, ast.Constant)
31
+ }
28
32
  kw_only_param: typing.Final = keywords.get("kw_only")
29
33
  slots_param: typing.Final = keywords.get("slots")
30
34
  frozen_param: typing.Final = keywords.get("frozen")
@@ -43,20 +47,20 @@ def has_required_dataclass_params(decorator: ast.expr) -> bool:
43
47
 
44
48
  def is_pydantic_model(class_node: ast.ClassDef) -> bool:
45
49
  """Check if class inherits from Pydantic BaseModel or RootModel."""
46
- for base_class in class_node.bases:
47
- if isinstance(base_class, ast.Name) and base_class.id in {"BaseModel", "RootModel"}:
50
+ for one_base_class in class_node.bases:
51
+ if isinstance(one_base_class, ast.Name) and one_base_class.id in {"BaseModel", "RootModel"}:
48
52
  return True
49
- if isinstance(base_class, ast.Attribute) and base_class.attr in {"BaseModel", "RootModel"}:
53
+ if isinstance(one_base_class, ast.Attribute) and one_base_class.attr in {"BaseModel", "RootModel"}:
50
54
  return True
51
55
  return False
52
56
 
53
57
 
54
58
  def is_model_factory(class_node: ast.ClassDef) -> bool:
55
59
  """Check if class inherits from ModelFactory."""
56
- for base_class in class_node.bases:
57
- if isinstance(base_class, ast.Name) and base_class.id in {"ModelFactory", "SQLAlchemyFactory"}:
60
+ for one_base_class in class_node.bases:
61
+ if isinstance(one_base_class, ast.Name) and one_base_class.id in {"ModelFactory", "SQLAlchemyFactory"}:
58
62
  return True
59
- if isinstance(base_class, ast.Attribute) and base_class.attr in {"ModelFactory", "SQLAlchemyFactory"}:
63
+ if isinstance(one_base_class, ast.Attribute) and one_base_class.attr in {"ModelFactory", "SQLAlchemyFactory"}:
60
64
  return True
61
65
  return False
62
66
 
@@ -78,9 +82,9 @@ class DataclassConfigCheck(ast.NodeVisitor):
78
82
  return
79
83
 
80
84
  # Check for dataclass decorator
81
- for decorator in ast_node.decorator_list:
82
- if is_dataclass_decorator(decorator):
83
- if not has_required_dataclass_params(decorator):
85
+ for one_decorator in ast_node.decorator_list:
86
+ if is_dataclass_decorator(one_decorator):
87
+ if not has_required_dataclass_params(one_decorator):
84
88
  self.violations.append(
85
89
  Violation(
86
90
  line_number=ast_node.lineno,
@@ -94,9 +98,9 @@ class DataclassConfigCheck(ast.NodeVisitor):
94
98
 
95
99
  def _inherits_from_exception(self, ast_node: ast.ClassDef) -> bool:
96
100
  """Check if class inherits from Exception or its subclasses."""
97
- for base in ast_node.bases:
98
- if isinstance(base, ast.Name) and ("Error" in base.id or "Exception" in base.id):
101
+ for one_base in ast_node.bases:
102
+ if isinstance(one_base, ast.Name) and ("Error" in one_base.id or "Exception" in one_base.id):
99
103
  return True
100
- if isinstance(base, ast.Attribute) and ("Error" in base.attr or "Exception" in base.attr):
104
+ if isinstance(one_base, ast.Attribute) and ("Error" in one_base.attr or "Exception" in one_base.attr):
101
105
  return True
102
106
  return False
@@ -17,15 +17,16 @@ from community_of_python_flake8_plugin.violations import Violation
17
17
 
18
18
 
19
19
  def check_module_has_all_declaration(module_node: ast.Module) -> bool:
20
- for statement in module_node.body:
21
- if isinstance(statement, ast.Assign) and any(
22
- isinstance(target, ast.Name) and target.id == "__all__" for target in statement.targets
20
+ for one_statement in module_node.body:
21
+ if isinstance(one_statement, ast.Assign) and any(
22
+ isinstance(one_target_element, ast.Name) and one_target_element.id == "__all__"
23
+ for one_target_element in one_statement.targets
23
24
  ):
24
25
  return True
25
26
  if (
26
- isinstance(statement, ast.AnnAssign)
27
- and isinstance(statement.target, ast.Name)
28
- and statement.target.id == "__all__"
27
+ isinstance(one_statement, ast.AnnAssign)
28
+ and isinstance(one_statement.target, ast.Name)
29
+ and one_statement.target.id == "__all__"
29
30
  ):
30
31
  return True
31
32
  return False
@@ -76,9 +77,9 @@ class ModuleImportManyNamesCheck(ast.NodeVisitor):
76
77
  return
77
78
 
78
79
  if not any(
79
- check_module_path_exists(f"{module_name}.{alias.name}")
80
- for alias in ast_node.names
81
- if isinstance(alias, ast.alias) and module_name is not None
80
+ check_module_path_exists(f"{module_name}.{one_alias_element.name}")
81
+ for one_alias_element in ast_node.names
82
+ if isinstance(one_alias_element, ast.alias) and module_name is not None
82
83
  ):
83
84
  self.violations.append(
84
85
  Violation(
@@ -7,8 +7,8 @@ from community_of_python_flake8_plugin.violations import Violation
7
7
 
8
8
 
9
9
  def contains_final_decorator(class_node: ast.ClassDef) -> bool:
10
- for decorator in class_node.decorator_list:
11
- target_name = decorator.func if isinstance(decorator, ast.Call) else decorator
10
+ for one_decorator in class_node.decorator_list:
11
+ target_name = one_decorator.func if isinstance(one_decorator, ast.Call) else one_decorator
12
12
  if isinstance(target_name, ast.Name) and target_name.id == "final":
13
13
  return True
14
14
  if isinstance(target_name, ast.Attribute) and target_name.attr == "final":
@@ -18,12 +18,12 @@ def contains_final_decorator(class_node: ast.ClassDef) -> bool:
18
18
 
19
19
  def is_protocol_class(class_node: ast.ClassDef) -> bool:
20
20
  """Check if the class directly inherits from typing.Protocol."""
21
- for base in class_node.bases:
21
+ for one_base in class_node.bases:
22
22
  # Check for direct Protocol reference: class MyClass(Protocol):
23
- if isinstance(base, ast.Name) and base.id == "Protocol":
23
+ if isinstance(one_base, ast.Name) and one_base.id == "Protocol":
24
24
  return True
25
25
  # Check for attributed Protocol reference: class MyClass(typing.Protocol):
26
- if isinstance(base, ast.Attribute) and base.attr == "Protocol":
26
+ if isinstance(one_base, ast.Attribute) and one_base.attr == "Protocol":
27
27
  return True
28
28
  return False
29
29
 
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import typing
4
+
5
+ from community_of_python_flake8_plugin.violation_codes import ViolationCodes
6
+ from community_of_python_flake8_plugin.violations import Violation
7
+
8
+
9
+ def _is_ignored_target(target_node: ast.expr) -> bool:
10
+ """Check if target should be ignored (e.g., underscore variables)."""
11
+ # Ignore underscore variables
12
+ return bool(isinstance(target_node, ast.Name) and target_node.id == "_")
13
+
14
+
15
+ @typing.final
16
+ class COP015ForLoopOnePrefixCheck(ast.NodeVisitor):
17
+ def __init__(self, syntax_tree: ast.AST) -> None:
18
+ self.violations: list[Violation] = []
19
+ self.syntax_tree: typing.Final[ast.AST] = syntax_tree
20
+
21
+ def visit_ListComp(self, ast_node: ast.ListComp) -> None:
22
+ # Validate targets in generators (the 'v' in 'for v in lst')
23
+ for one_comprehension in ast_node.generators:
24
+ if not self._is_partial_unpacking(ast_node.elt, one_comprehension.target):
25
+ self._validate_comprehension_target(one_comprehension.target)
26
+ self.generic_visit(ast_node)
27
+
28
+ def visit_SetComp(self, ast_node: ast.SetComp) -> None:
29
+ # Validate targets in generators (the 'v' in 'for v in lst')
30
+ for one_comprehension in ast_node.generators:
31
+ if not self._is_partial_unpacking(ast_node.elt, one_comprehension.target):
32
+ self._validate_comprehension_target(one_comprehension.target)
33
+ self.generic_visit(ast_node)
34
+
35
+ def visit_DictComp(self, ast_node: ast.DictComp) -> None:
36
+ # Validate targets in generators (the 'v' in 'for v in lst')
37
+ # For dict comprehensions, we consider both key and value
38
+ # key and value are both used
39
+ for one_comprehension in ast_node.generators:
40
+ if not self._is_partial_unpacking_expr_count(2, one_comprehension.target):
41
+ self._validate_comprehension_target(one_comprehension.target)
42
+ self.generic_visit(ast_node)
43
+
44
+ def visit_For(self, ast_node: ast.For) -> None:
45
+ # Validate target variables in regular for-loops
46
+ # Apply same unpacking logic as comprehensions
47
+ # For-loops don't have an expression that references vars
48
+ if not self._is_partial_unpacking_expr_count(1, ast_node.target):
49
+ self._validate_comprehension_target(ast_node.target)
50
+ self.generic_visit(ast_node)
51
+
52
+ def visit_GeneratorExp(self, ast_node: ast.GeneratorExp) -> None:
53
+ # Validate targets in generators (the 'v' in 'for v in lst')
54
+ for one_comprehension in ast_node.generators:
55
+ if not self._is_partial_unpacking(ast_node.elt, one_comprehension.target):
56
+ self._validate_comprehension_target(one_comprehension.target)
57
+ self.generic_visit(ast_node)
58
+
59
+ def _is_partial_unpacking(self, expression: ast.expr, target_node: ast.expr) -> bool:
60
+ """Check if this is partial unpacking (referencing fewer vars than unpacked)."""
61
+ return self._is_partial_unpacking_expr_count(self._count_referenced_vars(expression), target_node)
62
+
63
+ def _is_partial_unpacking_expr_count(self, expression_count: int, target_node: ast.expr) -> bool:
64
+ """Check if this is partial unpacking given expression variable count."""
65
+ target_count: typing.Final = self._count_unpacked_vars(target_node)
66
+ return target_count > expression_count and target_count > 1
67
+
68
+ def _count_referenced_vars(self, expression: ast.expr) -> int:
69
+ """Count how many variables are referenced in the expression."""
70
+ if isinstance(expression, ast.Name):
71
+ return 1
72
+ if isinstance(expression, ast.Tuple):
73
+ return len([one_element for one_element in expression.elts if isinstance(one_element, ast.Name)])
74
+ # For simplicity, assume other expressions reference 1 variable
75
+ # This covers cases like function calls, attributes, etc.
76
+ return 1
77
+
78
+ def _count_unpacked_vars(self, target_node: ast.expr) -> int:
79
+ """Count how many variables are unpacked in the target."""
80
+ if isinstance(target_node, ast.Name):
81
+ return 1
82
+ if isinstance(target_node, ast.Tuple):
83
+ return len([one_element for one_element in target_node.elts if isinstance(one_element, ast.Name)])
84
+ return 0
85
+
86
+ def _validate_comprehension_target(self, target_node: ast.expr) -> None:
87
+ """Validate that comprehension target follows the one_ prefix rule."""
88
+ # Skip ignored targets (underscore, unpacking)
89
+ if _is_ignored_target(target_node):
90
+ return
91
+
92
+ # For simple names, validate the prefix
93
+ if isinstance(target_node, ast.Name):
94
+ if not self._has_valid_one_prefix(target_node.id):
95
+ self.violations.append(
96
+ Violation(
97
+ line_number=target_node.lineno,
98
+ column_number=target_node.col_offset,
99
+ violation_code=ViolationCodes.FOR_LOOP_VARIABLE_PREFIX,
100
+ )
101
+ )
102
+ # For tuples (unpacking), validate each element
103
+ elif isinstance(target_node, ast.Tuple):
104
+ for one_element in target_node.elts:
105
+ if isinstance(one_element, ast.Name) and not self._has_valid_one_prefix(one_element.id):
106
+ self.violations.append(
107
+ Violation(
108
+ line_number=one_element.lineno,
109
+ column_number=one_element.col_offset,
110
+ violation_code=ViolationCodes.FOR_LOOP_VARIABLE_PREFIX,
111
+ )
112
+ )
113
+
114
+ def _has_valid_one_prefix(self, identifier: str) -> bool:
115
+ """Check if identifier has valid one_ prefix."""
116
+ # Allow underscore variables
117
+ if identifier == "_":
118
+ return True
119
+
120
+ # Check for one_ prefix
121
+ return identifier.startswith("one_")
@@ -17,7 +17,7 @@ def check_is_ignored_name(identifier: str) -> bool:
17
17
 
18
18
 
19
19
  def check_is_verb_name(identifier: str) -> bool:
20
- return any(identifier == verb or identifier.startswith(f"{verb}_") for verb in VERB_PREFIXES)
20
+ return any(identifier == verb_name or identifier.startswith(f"{verb_name}_") for verb_name in VERB_PREFIXES)
21
21
 
22
22
 
23
23
  def check_is_property(function_node: ast.AST) -> bool:
@@ -17,6 +17,54 @@ def is_mapping_proxy_type(annotation: ast.expr | None) -> bool:
17
17
  return False
18
18
 
19
19
 
20
+ def _get_base_name(annotation_value: ast.expr) -> str:
21
+ """Extract the base name from an annotation value."""
22
+ if isinstance(annotation_value, ast.Name):
23
+ return annotation_value.id
24
+ if isinstance(annotation_value, ast.Attribute):
25
+ return annotation_value.attr
26
+ return ""
27
+
28
+
29
+ def is_dict_type_annotation(annotation: ast.expr | None) -> bool:
30
+ """Check if annotation represents a dict type that should trigger COP013.
31
+
32
+ Returns True for:
33
+ - dict
34
+ - Final[dict]
35
+ - dict[key, value]
36
+ - Final[dict[key, value]]
37
+
38
+ Returns False for TypedDict and other non-dict annotations.
39
+ """
40
+ is_dict_annotation = False
41
+
42
+ if annotation is not None:
43
+ # Handle simple name annotations like 'dict'
44
+ if isinstance(annotation, ast.Name):
45
+ is_dict_annotation = annotation.id == "dict"
46
+ # Handle attribute annotations like 'typing.Final'
47
+ elif isinstance(annotation, ast.Attribute):
48
+ is_dict_annotation = annotation.attr == "dict"
49
+ # Handle subscript annotations like 'dict[str, int]' or 'Final[dict]'
50
+ elif isinstance(annotation, ast.Subscript):
51
+ base_name: typing.Final = _get_base_name(annotation.value)
52
+ if base_name:
53
+ # Check for Final[...] annotations
54
+ if base_name == "Final":
55
+ # Extract the inner type from Final[inner_type]
56
+ inner_type = annotation.slice
57
+ # Handle Python 3.8 vs 3.9+ differences
58
+ if hasattr(inner_type, "value"): # Python 3.8
59
+ inner_type = inner_type.value
60
+ is_dict_annotation = is_dict_type_annotation(inner_type)
61
+ # Check for dict[...] annotations
62
+ elif base_name == "dict":
63
+ is_dict_annotation = True
64
+
65
+ return is_dict_annotation
66
+
67
+
20
68
  @typing.final
21
69
  class MappingProxyCheck(ast.NodeVisitor):
22
70
  def __init__(self, syntax_tree: ast.AST) -> None: # noqa: ARG002
@@ -33,6 +81,10 @@ class MappingProxyCheck(ast.NodeVisitor):
33
81
  if isinstance(ast_node, ast.AnnAssign) and is_mapping_proxy_type(ast_node.annotation):
34
82
  return
35
83
 
84
+ # Skip annotated assignments that are not dict-like types
85
+ if isinstance(ast_node, ast.AnnAssign) and not is_dict_type_annotation(ast_node.annotation):
86
+ return
87
+
36
88
  # Check for dictionary literals assigned to module-level variables
37
89
  assigned_value: ast.expr | None
38
90
  assignment_targets: list[ast.expr]
@@ -78,6 +78,119 @@ class COP004NameLengthCheck(ast.NodeVisitor):
78
78
  self.validate_class_name_length(ast_node)
79
79
  self.generic_visit(ast_node)
80
80
 
81
+ def visit_ListComp(self, ast_node: ast.ListComp) -> None:
82
+ for comprehension in ast_node.generators:
83
+ self._validate_comprehension_target(comprehension.target)
84
+ self.generic_visit(ast_node)
85
+
86
+ def visit_SetComp(self, ast_node: ast.SetComp) -> None:
87
+ for comprehension in ast_node.generators:
88
+ self._validate_comprehension_target(comprehension.target)
89
+ self.generic_visit(ast_node)
90
+
91
+ def visit_DictComp(self, ast_node: ast.DictComp) -> None:
92
+ for comprehension in ast_node.generators:
93
+ self._validate_comprehension_target(comprehension.target)
94
+ self.generic_visit(ast_node)
95
+
96
+ def visit_Lambda(self, ast_node: ast.Lambda) -> None:
97
+ self._validate_function_args(ast_node.args)
98
+ self.generic_visit(ast_node)
99
+
100
+ 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)
104
+ self.generic_visit(ast_node)
105
+
106
+ def visit_ExceptHandler(self, ast_node: ast.ExceptHandler) -> None:
107
+ if ast_node.name is not None:
108
+ self._validate_except_target(ast_node)
109
+ self.generic_visit(ast_node)
110
+
111
+ def visit_GeneratorExp(self, ast_node: ast.GeneratorExp) -> None:
112
+ for comprehension in ast_node.generators:
113
+ self._validate_comprehension_target(comprehension.target)
114
+ self.generic_visit(ast_node)
115
+
116
+ def _validate_function_args(self, arguments_node: ast.arguments) -> None:
117
+ # 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
+
125
+ if arguments_node.vararg is not None:
126
+ self._validate_argument_name_length(arguments_node.vararg)
127
+ if arguments_node.kwarg is not None:
128
+ self._validate_argument_name_length(arguments_node.kwarg)
129
+
130
+ def _validate_argument_name_length(self, argument: ast.arg) -> None:
131
+ if argument.arg in {"self", "cls"}:
132
+ return
133
+ if check_is_ignored_name(argument.arg):
134
+ return
135
+ if check_is_whitelisted_annotation(argument.annotation):
136
+ return
137
+
138
+ if len(argument.arg) < MIN_NAME_LENGTH:
139
+ self.violations.append(
140
+ Violation(
141
+ line_number=argument.lineno,
142
+ column_number=argument.col_offset,
143
+ violation_code=ViolationCodes.ARGUMENT_NAME_LENGTH,
144
+ )
145
+ )
146
+
147
+ def _validate_comprehension_target(self, comprehension_target: ast.expr) -> None:
148
+ if isinstance(comprehension_target, ast.Name):
149
+ # For comprehension targets, we'll treat them as variables
150
+ if not check_is_ignored_name(comprehension_target.id) and len(comprehension_target.id) < MIN_NAME_LENGTH:
151
+ self.violations.append(
152
+ Violation(
153
+ line_number=comprehension_target.lineno,
154
+ column_number=comprehension_target.col_offset,
155
+ violation_code=ViolationCodes.VARIABLE_NAME_LENGTH,
156
+ )
157
+ )
158
+ elif isinstance(comprehension_target, ast.Tuple):
159
+ # 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)
162
+
163
+ def _validate_with_target(self, target_node: ast.expr) -> None:
164
+ if isinstance(target_node, ast.Name):
165
+ # For with targets, we'll treat them as variables
166
+ if not check_is_ignored_name(target_node.id) and len(target_node.id) < MIN_NAME_LENGTH:
167
+ self.violations.append(
168
+ Violation(
169
+ line_number=target_node.lineno,
170
+ column_number=target_node.col_offset,
171
+ violation_code=ViolationCodes.VARIABLE_NAME_LENGTH,
172
+ )
173
+ )
174
+ elif isinstance(target_node, ast.Tuple):
175
+ # 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)
178
+
179
+ def _validate_except_target(self, ast_node: ast.ExceptHandler) -> None:
180
+ # For except targets, we'll treat them as variables
181
+ if (
182
+ ast_node.name is not None
183
+ and not check_is_ignored_name(ast_node.name)
184
+ and len(ast_node.name) < MIN_NAME_LENGTH
185
+ ):
186
+ self.violations.append(
187
+ Violation(
188
+ line_number=ast_node.lineno,
189
+ column_number=0, # ast.ExceptHandler doesn't have col_offset
190
+ violation_code=ViolationCodes.VARIABLE_NAME_LENGTH,
191
+ )
192
+ )
193
+
81
194
  def validate_name_length(self, identifier: str, ast_node: ast.stmt, parent_class: ast.ClassDef | None) -> None:
82
195
  if check_is_ignored_name(identifier):
83
196
  return
@@ -73,8 +73,8 @@ class TempVarCheck(ast.NodeVisitor):
73
73
  continue
74
74
 
75
75
  if (
76
- len([usage for usage in usages if isinstance(usage.ctx, ast.Store)]) == 1
77
- and len([usage for usage in usages if isinstance(usage.ctx, ast.Load)]) == 1
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
78
  and variable_name in usage_and_stores[1]
79
79
  and not found_temporary_variable
80
80
  ):
@@ -47,3 +47,8 @@ class ViolationCodes:
47
47
  DATACLASS_CONFIG = ViolationCodeItem(
48
48
  code="COP014", description="Use dataclasses with kw_only=True, slots=True, frozen=True"
49
49
  )
50
+
51
+ # For-loop variable prefix violations
52
+ FOR_LOOP_VARIABLE_PREFIX = ViolationCodeItem(
53
+ code="COP015", description="For-loop variables must be prefixed with 'one_'"
54
+ )