community-of-python-flake8-plugin 0.5.2__tar.gz → 0.6.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.
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/PKG-INFO +29 -5
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/README.md +26 -3
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/pyproject.toml +9 -2
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/async_get_prefix.py +4 -2
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/dataclass_config.py +4 -2
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/final_class.py +4 -3
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/for_loop_one_prefix.py +4 -3
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/function_keyword_only_args.py +4 -3
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/function_verb.py +4 -3
- community_of_python_flake8_plugin-0.6.0/src/community_of_python_flake8_plugin/checks/immutable_variable.py +130 -0
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/mapping_proxy.py +26 -37
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/module_import_stdlib.py +4 -2
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/name_length.py +4 -3
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/scalar_annotation.py +4 -3
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/temp_var.py +4 -2
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/plugin.py +2 -2
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/violation_codes.py +9 -0
- community_of_python_flake8_plugin-0.6.0/src/cop_extensions/__init__.py +19 -0
- community_of_python_flake8_plugin-0.6.0/src/cop_extensions/py.typed +0 -0
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/__init__.py +0 -0
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/__init__.py +0 -0
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/disabled/__init__.py +0 -0
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/checks/disabled/module_import_many_names.py +0 -0
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/constants.py +0 -0
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/py.typed +0 -0
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/utils.py +0 -0
- {community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/src/community_of_python_flake8_plugin/violations.py +0 -0
{community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/PKG-INFO
RENAMED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: community-of-python-flake8-plugin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Community of Python flake8 plugin
|
|
5
5
|
Author: Lev Vereshchagin
|
|
6
6
|
Author-email: Lev Vereshchagin <mail@vrslev.com>
|
|
7
|
-
Requires-Dist: flake8
|
|
7
|
+
Requires-Dist: flake8 ; extra == 'flake8'
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
|
+
Provides-Extra: flake8
|
|
9
10
|
Description-Content-Type: text/markdown
|
|
10
11
|
|
|
11
12
|
# Community of Python Flake8 Plugin
|
|
@@ -32,19 +33,42 @@ This plugin implements the following code style checks:
|
|
|
32
33
|
- **COP014**: Use dataclasses with `kw_only=True`, `slots=True`, `frozen=True`
|
|
33
34
|
- **COP015**: For-loop variables must be prefixed with `one_`
|
|
34
35
|
- **COP016**: Add `*` or `/` when defining more than two regular arguments
|
|
36
|
+
- **COP017**: Avoid reassigning variables not annotated with `Mutable`
|
|
37
|
+
|
|
38
|
+
### COP017: immutable variables by default
|
|
39
|
+
|
|
40
|
+
Variables are treated as immutable: reassigning a name with `=` in the same scope is a violation. When reassignment is intended, allow it explicitly with the `Mutable` annotation on the first binding:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from cop_extensions import Mutable
|
|
44
|
+
|
|
45
|
+
counter_value: Mutable[int] = 0
|
|
46
|
+
counter_value = compute_next(counter_value) # OK
|
|
47
|
+
|
|
48
|
+
total_value = 0
|
|
49
|
+
total_value = compute_next(total_value) # COP017
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The `cop_extensions` package ships with the plugin distribution and has no runtime dependencies, so importing `Mutable` in production code does not pull in flake8. `Mutable` is matched by name, so any import source works (e.g. an in-house `typing_extensions.Mutable`). Augmented assignments (`+=`), for-loop variables, attribute and subscript targets, and names declared `global`/`nonlocal` are not checked.
|
|
35
53
|
|
|
36
54
|
## Installation
|
|
37
55
|
|
|
38
|
-
Install the plugin using `uv` (recommended):
|
|
56
|
+
Install the plugin using `uv` (recommended). For linting, use the `flake8` extra:
|
|
39
57
|
|
|
40
58
|
```bash
|
|
41
|
-
uv add --dev community-of-python-flake8-plugin
|
|
59
|
+
uv add --dev "community-of-python-flake8-plugin[flake8]"
|
|
42
60
|
```
|
|
43
61
|
|
|
44
62
|
Or install via pip:
|
|
45
63
|
|
|
46
64
|
```bash
|
|
47
|
-
pip install community-of-python-flake8-plugin
|
|
65
|
+
pip install "community-of-python-flake8-plugin[flake8]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
To use the `cop_extensions.Mutable` marker in production code, add the package without the extra — it has no runtime dependencies:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv add community-of-python-flake8-plugin
|
|
48
72
|
```
|
|
49
73
|
|
|
50
74
|
## Usage
|
{community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/README.md
RENAMED
|
@@ -22,19 +22,42 @@ This plugin implements the following code style checks:
|
|
|
22
22
|
- **COP014**: Use dataclasses with `kw_only=True`, `slots=True`, `frozen=True`
|
|
23
23
|
- **COP015**: For-loop variables must be prefixed with `one_`
|
|
24
24
|
- **COP016**: Add `*` or `/` when defining more than two regular arguments
|
|
25
|
+
- **COP017**: Avoid reassigning variables not annotated with `Mutable`
|
|
26
|
+
|
|
27
|
+
### COP017: immutable variables by default
|
|
28
|
+
|
|
29
|
+
Variables are treated as immutable: reassigning a name with `=` in the same scope is a violation. When reassignment is intended, allow it explicitly with the `Mutable` annotation on the first binding:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from cop_extensions import Mutable
|
|
33
|
+
|
|
34
|
+
counter_value: Mutable[int] = 0
|
|
35
|
+
counter_value = compute_next(counter_value) # OK
|
|
36
|
+
|
|
37
|
+
total_value = 0
|
|
38
|
+
total_value = compute_next(total_value) # COP017
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The `cop_extensions` package ships with the plugin distribution and has no runtime dependencies, so importing `Mutable` in production code does not pull in flake8. `Mutable` is matched by name, so any import source works (e.g. an in-house `typing_extensions.Mutable`). Augmented assignments (`+=`), for-loop variables, attribute and subscript targets, and names declared `global`/`nonlocal` are not checked.
|
|
25
42
|
|
|
26
43
|
## Installation
|
|
27
44
|
|
|
28
|
-
Install the plugin using `uv` (recommended):
|
|
45
|
+
Install the plugin using `uv` (recommended). For linting, use the `flake8` extra:
|
|
29
46
|
|
|
30
47
|
```bash
|
|
31
|
-
uv add --dev community-of-python-flake8-plugin
|
|
48
|
+
uv add --dev "community-of-python-flake8-plugin[flake8]"
|
|
32
49
|
```
|
|
33
50
|
|
|
34
51
|
Or install via pip:
|
|
35
52
|
|
|
36
53
|
```bash
|
|
37
|
-
pip install community-of-python-flake8-plugin
|
|
54
|
+
pip install "community-of-python-flake8-plugin[flake8]"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
To use the `cop_extensions.Mutable` marker in production code, add the package without the extra — it has no runtime dependencies:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv add community-of-python-flake8-plugin
|
|
38
61
|
```
|
|
39
62
|
|
|
40
63
|
## Usage
|
{community_of_python_flake8_plugin-0.5.2 → community_of_python_flake8_plugin-0.6.0}/pyproject.toml
RENAMED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "community-of-python-flake8-plugin"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Community of Python flake8 plugin"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Lev Vereshchagin", email = "mail@vrslev.com" }]
|
|
7
7
|
requires-python = ">=3.10"
|
|
8
|
-
dependencies = [
|
|
8
|
+
dependencies = []
|
|
9
|
+
|
|
10
|
+
[project.optional-dependencies]
|
|
11
|
+
flake8 = ["flake8"]
|
|
9
12
|
|
|
10
13
|
[project.entry-points."flake8.extension"]
|
|
11
14
|
COP = "community_of_python_flake8_plugin.plugin:CommunityOfPythonFlake8Plugin"
|
|
@@ -15,6 +18,7 @@ addopts = "--cov"
|
|
|
15
18
|
|
|
16
19
|
[dependency-groups]
|
|
17
20
|
dev = [
|
|
21
|
+
"flake8",
|
|
18
22
|
"mypy",
|
|
19
23
|
"ruff",
|
|
20
24
|
"auto-typing-final",
|
|
@@ -27,6 +31,9 @@ dev = [
|
|
|
27
31
|
requires = ["uv_build"]
|
|
28
32
|
build-backend = "uv_build"
|
|
29
33
|
|
|
34
|
+
[tool.uv.build-backend]
|
|
35
|
+
module-name = ["community_of_python_flake8_plugin", "cop_extensions"]
|
|
36
|
+
|
|
30
37
|
[tool.ruff]
|
|
31
38
|
fix = true
|
|
32
39
|
unsafe-fixes = true
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
6
|
from community_of_python_flake8_plugin.violation_codes import ViolationCodes
|
|
@@ -7,9 +8,10 @@ from community_of_python_flake8_plugin.violations import Violation
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
@typing.final
|
|
11
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
10
12
|
class AsyncGetPrefixCheck(ast.NodeVisitor):
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
syntax_tree: ast.AST
|
|
14
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
13
15
|
|
|
14
16
|
def visit_AsyncFunctionDef(self, ast_node: ast.AsyncFunctionDef) -> None:
|
|
15
17
|
# Always flag async functions with get_ prefix
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
6
|
from community_of_python_flake8_plugin.constants import FINAL_CLASS_EXCLUDED_BASES
|
|
@@ -66,9 +67,10 @@ def is_model_factory(class_node: ast.ClassDef) -> bool:
|
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
@typing.final
|
|
70
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
69
71
|
class DataclassConfigCheck(ast.NodeVisitor):
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
syntax_tree: ast.AST
|
|
73
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
72
74
|
|
|
73
75
|
def visit_ClassDef(self, ast_node: ast.ClassDef) -> None:
|
|
74
76
|
# Skip whitelisted classes and classes that inherit from Exception or other special classes
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
6
|
from community_of_python_flake8_plugin.utils import check_inherits_from_bases
|
|
@@ -55,10 +56,10 @@ def has_local_subclasses(syntax_tree: ast.AST, class_node: ast.ClassDef) -> bool
|
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
@typing.final
|
|
59
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
58
60
|
class FinalClassCheck(ast.NodeVisitor):
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
self.violations: list[Violation] = []
|
|
61
|
+
syntax_tree: ast.AST
|
|
62
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
62
63
|
|
|
63
64
|
def visit_ClassDef(self, ast_node: ast.ClassDef) -> None:
|
|
64
65
|
self._check_final_decorator(ast_node)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
6
|
from community_of_python_flake8_plugin.violation_codes import ViolationCodes
|
|
@@ -13,10 +14,10 @@ def _is_ignored_target(target_node: ast.expr) -> bool:
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
@typing.final
|
|
17
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
16
18
|
class COP015ForLoopOnePrefixCheck(ast.NodeVisitor):
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
self.syntax_tree: typing.Final[ast.AST] = syntax_tree
|
|
19
|
+
syntax_tree: ast.AST
|
|
20
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
20
21
|
|
|
21
22
|
def visit_ListComp(self, ast_node: ast.ListComp) -> None:
|
|
22
23
|
# Validate targets in generators (the 'v' in 'for v in lst')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
6
|
from community_of_python_flake8_plugin.violation_codes import ViolationCodes
|
|
@@ -46,10 +47,10 @@ def check_has_positional_or_keyword_only_separator(arguments_node: ast.arguments
|
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
@typing.final
|
|
50
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
49
51
|
class COP016FunctionKeywordOnlyArgsCheck(ast.NodeVisitor):
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
self.syntax_tree: typing.Final[ast.AST] = syntax_tree
|
|
52
|
+
syntax_tree: ast.AST
|
|
53
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
53
54
|
|
|
54
55
|
def visit_FunctionDef(self, ast_node: ast.FunctionDef) -> None:
|
|
55
56
|
self.validate_function_definition(ast_node)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
6
|
from community_of_python_flake8_plugin.constants import FINAL_CLASS_EXCLUDED_BASES, VERB_PREFIXES
|
|
@@ -79,10 +80,10 @@ def check_is_fixture_decorator(decorator: ast.expr) -> bool:
|
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
@typing.final
|
|
83
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
82
84
|
class FunctionVerbCheck(ast.NodeVisitor):
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
self.syntax_tree: typing.Final[ast.AST] = syntax_tree
|
|
85
|
+
syntax_tree: ast.AST
|
|
86
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
86
87
|
|
|
87
88
|
def visit_FunctionDef(self, ast_node: ast.FunctionDef) -> None:
|
|
88
89
|
self.validate_function_name(ast_node, find_parent_class_definition(self.syntax_tree, ast_node))
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import dataclasses
|
|
4
|
+
import typing
|
|
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
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Iterable, Sequence
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MUTABLE_ANNOTATION_NAME: typing.Final = "Mutable"
|
|
15
|
+
NESTED_SCOPE_NODE_TYPES: typing.Final = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_is_mutable_annotation(annotation_node: ast.AST) -> bool:
|
|
19
|
+
if isinstance(annotation_node, ast.Name):
|
|
20
|
+
return annotation_node.id == MUTABLE_ANNOTATION_NAME
|
|
21
|
+
if isinstance(annotation_node, ast.Attribute):
|
|
22
|
+
return annotation_node.attr == MUTABLE_ANNOTATION_NAME
|
|
23
|
+
if isinstance(annotation_node, ast.Subscript):
|
|
24
|
+
return check_is_mutable_annotation(annotation_node.value)
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def extract_assigned_names(target_node: ast.expr) -> Iterable[str]:
|
|
29
|
+
if isinstance(target_node, ast.Name):
|
|
30
|
+
yield target_node.id
|
|
31
|
+
elif isinstance(target_node, (ast.Tuple, ast.List)):
|
|
32
|
+
for one_element in target_node.elts:
|
|
33
|
+
yield from extract_assigned_names(one_element)
|
|
34
|
+
elif isinstance(target_node, ast.Starred):
|
|
35
|
+
yield from extract_assigned_names(target_node.value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def iter_scope_child_nodes(scope_body: Iterable[ast.stmt]) -> Iterable[ast.AST]:
|
|
39
|
+
for one_statement in scope_body:
|
|
40
|
+
yield from iter_same_scope_nodes(one_statement)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def iter_same_scope_nodes(ast_node: ast.AST) -> Iterable[ast.AST]:
|
|
44
|
+
yield ast_node
|
|
45
|
+
if isinstance(ast_node, NESTED_SCOPE_NODE_TYPES):
|
|
46
|
+
return
|
|
47
|
+
for one_child_node in ast.iter_child_nodes(ast_node):
|
|
48
|
+
yield from iter_same_scope_nodes(one_child_node)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@typing.final
|
|
52
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
53
|
+
class COP017ImmutableVariableCheck(ast.NodeVisitor):
|
|
54
|
+
syntax_tree: ast.AST
|
|
55
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
56
|
+
|
|
57
|
+
def visit_Module(self, ast_node: ast.Module) -> None:
|
|
58
|
+
self.validate_scope(ast_node.body)
|
|
59
|
+
self.generic_visit(ast_node)
|
|
60
|
+
|
|
61
|
+
def visit_FunctionDef(self, ast_node: ast.FunctionDef) -> None:
|
|
62
|
+
self.validate_scope(ast_node.body)
|
|
63
|
+
self.generic_visit(ast_node)
|
|
64
|
+
|
|
65
|
+
def visit_AsyncFunctionDef(self, ast_node: ast.AsyncFunctionDef) -> None:
|
|
66
|
+
self.validate_scope(ast_node.body)
|
|
67
|
+
self.generic_visit(ast_node)
|
|
68
|
+
|
|
69
|
+
def visit_ClassDef(self, ast_node: ast.ClassDef) -> None:
|
|
70
|
+
self.validate_scope(ast_node.body)
|
|
71
|
+
self.generic_visit(ast_node)
|
|
72
|
+
|
|
73
|
+
def validate_scope(self, scope_body: Sequence[ast.stmt]) -> None:
|
|
74
|
+
skipped_names: typing.Final = self.collect_outer_scope_names(scope_body)
|
|
75
|
+
mutable_flag_by_name: typing.Final[dict[str, bool]] = {}
|
|
76
|
+
for one_scope_node in iter_scope_child_nodes(scope_body):
|
|
77
|
+
if isinstance(one_scope_node, ast.Assign):
|
|
78
|
+
self.validate_assignment(
|
|
79
|
+
one_scope_node, skipped_names=skipped_names, mutable_flag_by_name=mutable_flag_by_name
|
|
80
|
+
)
|
|
81
|
+
elif isinstance(one_scope_node, ast.AnnAssign):
|
|
82
|
+
self.validate_annotated_assignment(
|
|
83
|
+
one_scope_node, skipped_names=skipped_names, mutable_flag_by_name=mutable_flag_by_name
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def collect_outer_scope_names(self, scope_body: Iterable[ast.stmt]) -> set[str]:
|
|
87
|
+
outer_scope_names: typing.Final[set[str]] = set()
|
|
88
|
+
for one_scope_node in iter_scope_child_nodes(scope_body):
|
|
89
|
+
if isinstance(one_scope_node, (ast.Global, ast.Nonlocal)):
|
|
90
|
+
outer_scope_names.update(one_scope_node.names)
|
|
91
|
+
return outer_scope_names
|
|
92
|
+
|
|
93
|
+
def validate_assignment(
|
|
94
|
+
self, ast_node: ast.Assign, *, skipped_names: set[str], mutable_flag_by_name: dict[str, bool]
|
|
95
|
+
) -> None:
|
|
96
|
+
for one_target in ast_node.targets:
|
|
97
|
+
for one_assigned_name in extract_assigned_names(one_target):
|
|
98
|
+
if one_assigned_name in skipped_names:
|
|
99
|
+
continue
|
|
100
|
+
if mutable_flag_by_name.get(one_assigned_name) is False:
|
|
101
|
+
self.append_violation(ast_node)
|
|
102
|
+
else:
|
|
103
|
+
mutable_flag_by_name.setdefault(one_assigned_name, False)
|
|
104
|
+
|
|
105
|
+
def validate_annotated_assignment(
|
|
106
|
+
self, ast_node: ast.AnnAssign, *, skipped_names: set[str], mutable_flag_by_name: dict[str, bool]
|
|
107
|
+
) -> None:
|
|
108
|
+
if not isinstance(ast_node.target, ast.Name):
|
|
109
|
+
return
|
|
110
|
+
assigned_name: typing.Final = ast_node.target.id
|
|
111
|
+
if assigned_name in skipped_names:
|
|
112
|
+
return
|
|
113
|
+
if mutable_flag_by_name.get(assigned_name) is False:
|
|
114
|
+
self.append_violation(ast_node)
|
|
115
|
+
return
|
|
116
|
+
is_mutable: typing.Final = check_is_mutable_annotation(ast_node.annotation)
|
|
117
|
+
if ast_node.value is None:
|
|
118
|
+
if is_mutable:
|
|
119
|
+
mutable_flag_by_name[assigned_name] = True
|
|
120
|
+
else:
|
|
121
|
+
mutable_flag_by_name.setdefault(assigned_name, is_mutable)
|
|
122
|
+
|
|
123
|
+
def append_violation(self, ast_node: ast.stmt) -> None:
|
|
124
|
+
self.violations.append(
|
|
125
|
+
Violation(
|
|
126
|
+
line_number=ast_node.lineno,
|
|
127
|
+
column_number=ast_node.col_offset,
|
|
128
|
+
violation_code=ViolationCodes.IMMUTABLE_VARIABLE,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
6
|
from community_of_python_flake8_plugin.constants import MAPPING_PROXY_TYPES
|
|
@@ -37,38 +38,33 @@ def is_dict_type_annotation(annotation: ast.expr | None) -> bool:
|
|
|
37
38
|
|
|
38
39
|
Returns False for TypedDict and other non-dict annotations.
|
|
39
40
|
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
41
|
+
# Handle simple name annotations like 'dict'
|
|
42
|
+
if isinstance(annotation, ast.Name):
|
|
43
|
+
return annotation.id == "dict"
|
|
44
|
+
# Handle attribute annotations like 'typing.Final'
|
|
45
|
+
if isinstance(annotation, ast.Attribute):
|
|
46
|
+
return annotation.attr == "dict"
|
|
47
|
+
# Handle subscript annotations like 'dict[str, int]' or 'Final[dict]'
|
|
48
|
+
if isinstance(annotation, ast.Subscript):
|
|
49
|
+
base_name: typing.Final = _get_base_name(annotation.value)
|
|
50
|
+
# Extract the inner type from Final[inner_type]
|
|
51
|
+
if base_name == "Final":
|
|
52
|
+
return is_dict_type_annotation(annotation.slice)
|
|
53
|
+
return base_name == "dict"
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_assignment_targets(ast_node: ast.Assign | ast.AnnAssign) -> list[ast.expr]:
|
|
58
|
+
if isinstance(ast_node, ast.Assign):
|
|
59
|
+
return ast_node.targets
|
|
60
|
+
return [ast_node.target] if ast_node.value is not None else []
|
|
66
61
|
|
|
67
62
|
|
|
68
63
|
@typing.final
|
|
64
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
69
65
|
class MappingProxyCheck(ast.NodeVisitor):
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
syntax_tree: ast.AST
|
|
67
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
72
68
|
|
|
73
69
|
def visit_Module(self, ast_node: ast.Module) -> None:
|
|
74
70
|
for one_statement in ast_node.body:
|
|
@@ -86,15 +82,8 @@ class MappingProxyCheck(ast.NodeVisitor):
|
|
|
86
82
|
return
|
|
87
83
|
|
|
88
84
|
# Check for dictionary literals assigned to module-level variables
|
|
89
|
-
assigned_value:
|
|
90
|
-
assignment_targets:
|
|
91
|
-
|
|
92
|
-
if isinstance(ast_node, ast.Assign):
|
|
93
|
-
assigned_value = ast_node.value
|
|
94
|
-
assignment_targets = ast_node.targets
|
|
95
|
-
else: # ast.AnnAssign
|
|
96
|
-
assigned_value = ast_node.value
|
|
97
|
-
assignment_targets = [ast_node.target] if ast_node.value is not None else []
|
|
85
|
+
assigned_value: typing.Final = ast_node.value
|
|
86
|
+
assignment_targets: typing.Final = _get_assignment_targets(ast_node)
|
|
98
87
|
|
|
99
88
|
# Only check module-level assignments (no parent function/class)
|
|
100
89
|
if assigned_value is not None and isinstance(assigned_value, ast.Dict) and assignment_targets:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import sys
|
|
4
5
|
import typing
|
|
5
6
|
from importlib import util as importlib_util
|
|
@@ -21,9 +22,10 @@ def check_is_stdlib_package(module_name: str) -> bool:
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@typing.final
|
|
25
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
24
26
|
class COP002StdlibImportCheck(ast.NodeVisitor):
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
syntax_tree: ast.AST
|
|
28
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
27
29
|
|
|
28
30
|
def visit_ImportFrom(self, ast_node: ast.ImportFrom) -> None:
|
|
29
31
|
if ast_node.module and ast_node.level == 0 and ast_node.module not in ALLOWED_STDLIB_FROM_IMPORTS:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
6
|
from community_of_python_flake8_plugin.constants import FINAL_CLASS_EXCLUDED_BASES, MIN_NAME_LENGTH
|
|
@@ -49,10 +50,10 @@ def check_is_fixture_decorator(decorator: ast.expr) -> bool:
|
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
@typing.final
|
|
53
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
52
54
|
class COP004NameLengthCheck(ast.NodeVisitor):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
self.syntax_tree: typing.Final[ast.AST] = tree
|
|
55
|
+
syntax_tree: ast.AST
|
|
56
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
56
57
|
|
|
57
58
|
def visit_AnnAssign(self, ast_node: ast.AnnAssign) -> None:
|
|
58
59
|
if isinstance(ast_node.target, ast.Name):
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
6
|
from community_of_python_flake8_plugin.constants import SCALAR_ANNOTATIONS
|
|
@@ -37,10 +38,10 @@ def check_is_scalar_annotation(annotation_node: ast.AST) -> bool:
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
@typing.final
|
|
41
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
40
42
|
class ScalarAnnotationCheck(ast.NodeVisitor):
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
self.syntax_tree: typing.Final[ast.AST] = syntax_tree
|
|
43
|
+
syntax_tree: ast.AST
|
|
44
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
44
45
|
|
|
45
46
|
def visit_AnnAssign(self, ast_node: ast.AnnAssign) -> None:
|
|
46
47
|
if isinstance(ast_node.target, ast.Name) and (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import ast
|
|
3
|
+
import dataclasses
|
|
3
4
|
import typing
|
|
4
5
|
from collections import defaultdict
|
|
5
6
|
|
|
@@ -93,9 +94,10 @@ def is_used_in_next_line(assign_node: ast.Assign | ast.AnnAssign, usage_nodes: l
|
|
|
93
94
|
|
|
94
95
|
|
|
95
96
|
@typing.final
|
|
97
|
+
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
|
|
96
98
|
class TempVarCheck(ast.NodeVisitor):
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
syntax_tree: ast.AST
|
|
100
|
+
violations: list[Violation] = dataclasses.field(default_factory=list)
|
|
99
101
|
|
|
100
102
|
def visit_FunctionDef(self, ast_node: ast.FunctionDef) -> None:
|
|
101
103
|
self._check_temporary_variables(ast_node)
|
|
@@ -18,7 +18,7 @@ if typing.TYPE_CHECKING:
|
|
|
18
18
|
class PluginCheckProtocol(typing.Protocol):
|
|
19
19
|
violations: list[Violation]
|
|
20
20
|
|
|
21
|
-
def __init__(self,
|
|
21
|
+
def __init__(self, *, syntax_tree: ast.AST) -> None: ...
|
|
22
22
|
def visit(self, node: ast.AST) -> None: ... # noqa: COP007,COP006,COP012
|
|
23
23
|
|
|
24
24
|
|
|
@@ -48,7 +48,7 @@ class CommunityOfPythonFlake8Plugin:
|
|
|
48
48
|
for one_attribute_name in dir(imported_module):
|
|
49
49
|
attribute = getattr(imported_module, one_attribute_name)
|
|
50
50
|
if isinstance(attribute, type) and one_attribute_name.endswith("Check") and hasattr(attribute, "visit"):
|
|
51
|
-
check_instance = attribute(self.ast_syntax_tree)
|
|
51
|
+
check_instance = attribute(syntax_tree=self.ast_syntax_tree)
|
|
52
52
|
check_instance.visit(self.ast_syntax_tree)
|
|
53
53
|
checks_collection.append(check_instance)
|
|
54
54
|
return checks_collection
|
|
@@ -59,3 +59,12 @@ class ViolationCodes:
|
|
|
59
59
|
FUNCTION_KEYWORD_ONLY_ARGS = ViolationCodeItem(
|
|
60
60
|
code="COP016", description="Add * or / when defining more than two regular arguments"
|
|
61
61
|
)
|
|
62
|
+
|
|
63
|
+
# Variable mutability violations
|
|
64
|
+
IMMUTABLE_VARIABLE = ViolationCodeItem(
|
|
65
|
+
code="COP017",
|
|
66
|
+
description=(
|
|
67
|
+
"Avoid reassigning variables. If reassignment is intended, annotate the first binding: "
|
|
68
|
+
"`from cop_extensions import Mutable` and `name: Mutable[T] = ...`"
|
|
69
|
+
),
|
|
70
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
__all__ = ["Mutable"]
|
|
6
|
+
|
|
7
|
+
_ValueType = typing.TypeVar("_ValueType")
|
|
8
|
+
|
|
9
|
+
if typing.TYPE_CHECKING:
|
|
10
|
+
# For type checkers Mutable[T] is just T with an extra marker; bare Mutable is also accepted.
|
|
11
|
+
Mutable: typing.TypeAlias = typing.Annotated[_ValueType, "mutable"] # noqa: COP005
|
|
12
|
+
else:
|
|
13
|
+
|
|
14
|
+
@typing.final
|
|
15
|
+
class _MutableMarker:
|
|
16
|
+
def __getitem__(self, item_value: _ValueType) -> _ValueType:
|
|
17
|
+
return item_value
|
|
18
|
+
|
|
19
|
+
Mutable = _MutableMarker() # noqa: COP005,COP017
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|