community-of-python-flake8-plugin 0.1.0__py3-none-any.whl
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/__init__.py +4 -0
- community_of_python_flake8_plugin/checks/README.md +35 -0
- community_of_python_flake8_plugin/checks/__init__.py +78 -0
- community_of_python_flake8_plugin/checks/cop001.py +53 -0
- community_of_python_flake8_plugin/checks/cop002.py +48 -0
- community_of_python_flake8_plugin/checks/cop003.py +76 -0
- community_of_python_flake8_plugin/checks/cop004.py +166 -0
- community_of_python_flake8_plugin/checks/cop005.py +111 -0
- community_of_python_flake8_plugin/checks/cop006.py +72 -0
- community_of_python_flake8_plugin/checks/cop007.py +55 -0
- community_of_python_flake8_plugin/checks/cop008.py +69 -0
- community_of_python_flake8_plugin/checks/cop009.py +63 -0
- community_of_python_flake8_plugin/checks/cop010.py +84 -0
- community_of_python_flake8_plugin/constants.py +93 -0
- community_of_python_flake8_plugin/plugin.py +25 -0
- community_of_python_flake8_plugin/py.typed +0 -0
- community_of_python_flake8_plugin/utils.py +35 -0
- community_of_python_flake8_plugin/violation_codes.py +26 -0
- community_of_python_flake8_plugin/violations.py +15 -0
- community_of_python_flake8_plugin-0.1.0.dist-info/METADATA +27 -0
- community_of_python_flake8_plugin-0.1.0.dist-info/RECORD +23 -0
- community_of_python_flake8_plugin-0.1.0.dist-info/WHEEL +4 -0
- community_of_python_flake8_plugin-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Community of Python Flake8 Plugin - Check Files
|
|
2
|
+
|
|
3
|
+
This directory contains individual check files, each implementing a specific rule (COP error code).
|
|
4
|
+
|
|
5
|
+
## Current Checks
|
|
6
|
+
|
|
7
|
+
| File | Error Code | Description |
|
|
8
|
+
|------|------------|-------------|
|
|
9
|
+
| `cop001.py` | COP001 | Use module import when importing more than two names |
|
|
10
|
+
| `cop002.py` | COP002 | Import standard library modules as whole modules |
|
|
11
|
+
| `cop003.py` | COP003 | Avoid explicit scalar type annotations |
|
|
12
|
+
| `cop004.py` | COP004 | Name must be at least 8 characters |
|
|
13
|
+
| `cop005.py` | COP005 | Function name must be a verb |
|
|
14
|
+
| `cop006.py` | COP006 | Avoid get_ prefix in async function names |
|
|
15
|
+
| `cop007.py` | COP007 | Avoid temporary variables used only once |
|
|
16
|
+
| `cop008.py` | COP008 | Classes should be marked typing.final |
|
|
17
|
+
| `cop009.py` | COP009 | Wrap module dictionaries with types.MappingProxyType |
|
|
18
|
+
| `cop010.py` | COP010 | Use dataclasses with kw_only=True, slots=True, frozen=True |
|
|
19
|
+
|
|
20
|
+
## File Structure
|
|
21
|
+
|
|
22
|
+
Each check file follows this pattern:
|
|
23
|
+
- Contains a single class that inherits from `ast.NodeVisitor`
|
|
24
|
+
- Implements visit methods for the AST nodes it needs to check
|
|
25
|
+
- Stores violations in a `self.violations` list
|
|
26
|
+
- Each file is responsible for exactly one error code
|
|
27
|
+
|
|
28
|
+
## Adding New Checks
|
|
29
|
+
|
|
30
|
+
To add a new check:
|
|
31
|
+
1. Create a new file `copXXX.py` where XXX is the next available error code
|
|
32
|
+
2. Implement a check class following the pattern of existing checks
|
|
33
|
+
3. Add the import and instantiation in `__init__.py`
|
|
34
|
+
4. Add tests in the test file
|
|
35
|
+
5. Update this README
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from community_of_python_flake8_plugin.checks.cop001 import COP001Check
|
|
6
|
+
from community_of_python_flake8_plugin.checks.cop002 import COP002Check
|
|
7
|
+
from community_of_python_flake8_plugin.checks.cop003 import COP003Check
|
|
8
|
+
from community_of_python_flake8_plugin.checks.cop004 import COP004Check
|
|
9
|
+
from community_of_python_flake8_plugin.checks.cop005 import COP005Check
|
|
10
|
+
from community_of_python_flake8_plugin.checks.cop006 import COP006Check
|
|
11
|
+
from community_of_python_flake8_plugin.checks.cop007 import COP007Check
|
|
12
|
+
from community_of_python_flake8_plugin.checks.cop008 import COP008Check
|
|
13
|
+
from community_of_python_flake8_plugin.checks.cop009 import COP009Check
|
|
14
|
+
from community_of_python_flake8_plugin.checks.cop010 import COP010Check
|
|
15
|
+
from community_of_python_flake8_plugin.utils import check_module_has_all_declaration
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if typing.TYPE_CHECKING:
|
|
19
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def execute_all_validations(syntax_tree: ast.AST) -> list[Violation]:
|
|
23
|
+
contains_all_declaration: typing.Final = (
|
|
24
|
+
check_module_has_all_declaration(syntax_tree) if isinstance(syntax_tree, ast.Module) else False
|
|
25
|
+
)
|
|
26
|
+
collected_violations: typing.Final[list[Violation]] = []
|
|
27
|
+
|
|
28
|
+
# COP001: Use module import when importing more than two names
|
|
29
|
+
cop001_validator: typing.Final = COP001Check(contains_all_declaration)
|
|
30
|
+
cop001_validator.visit(syntax_tree)
|
|
31
|
+
collected_violations.extend(cop001_validator.violations)
|
|
32
|
+
|
|
33
|
+
# COP002: Import standard library modules as whole modules
|
|
34
|
+
cop002_validator: typing.Final = COP002Check()
|
|
35
|
+
cop002_validator.visit(syntax_tree)
|
|
36
|
+
collected_violations.extend(cop002_validator.violations)
|
|
37
|
+
|
|
38
|
+
# COP003: Avoid explicit scalar type annotations
|
|
39
|
+
cop003_validator: typing.Final = COP003Check(syntax_tree)
|
|
40
|
+
cop003_validator.visit(syntax_tree)
|
|
41
|
+
collected_violations.extend(cop003_validator.violations)
|
|
42
|
+
|
|
43
|
+
# COP004: Name must be at least 8 characters
|
|
44
|
+
cop004_validator: typing.Final = COP004Check(syntax_tree)
|
|
45
|
+
cop004_validator.visit(syntax_tree)
|
|
46
|
+
collected_violations.extend(cop004_validator.violations)
|
|
47
|
+
|
|
48
|
+
# COP005: Function identifier must be a verb
|
|
49
|
+
cop005_validator: typing.Final = COP005Check(syntax_tree)
|
|
50
|
+
cop005_validator.visit(syntax_tree)
|
|
51
|
+
collected_violations.extend(cop005_validator.violations)
|
|
52
|
+
|
|
53
|
+
# COP006: Avoid get_ prefix in async function names
|
|
54
|
+
cop006_validator: typing.Final = COP006Check()
|
|
55
|
+
cop006_validator.visit(syntax_tree)
|
|
56
|
+
collected_violations.extend(cop006_validator.violations)
|
|
57
|
+
|
|
58
|
+
# COP007: Avoid temporary variables used only once
|
|
59
|
+
cop007_validator: typing.Final = COP007Check()
|
|
60
|
+
cop007_validator.visit(syntax_tree)
|
|
61
|
+
collected_violations.extend(cop007_validator.violations)
|
|
62
|
+
|
|
63
|
+
# COP008: Classes should be marked typing.final
|
|
64
|
+
cop008_validator: typing.Final = COP008Check()
|
|
65
|
+
cop008_validator.visit(syntax_tree)
|
|
66
|
+
collected_violations.extend(cop008_validator.violations)
|
|
67
|
+
|
|
68
|
+
# COP009: Wrap module dictionaries with types.MappingProxyType
|
|
69
|
+
cop009_validator: typing.Final = COP009Check()
|
|
70
|
+
cop009_validator.visit(syntax_tree)
|
|
71
|
+
collected_violations.extend(cop009_validator.violations)
|
|
72
|
+
|
|
73
|
+
# COP010: Use dataclasses with kw_only=True, slots=True, frozen=True
|
|
74
|
+
cop010_validator: typing.Final = COP010Check()
|
|
75
|
+
cop010_validator.visit(syntax_tree)
|
|
76
|
+
collected_violations.extend(cop010_validator.violations)
|
|
77
|
+
|
|
78
|
+
return collected_violations
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import typing
|
|
4
|
+
from importlib import util as importlib_util
|
|
5
|
+
|
|
6
|
+
from community_of_python_flake8_plugin.violation_codes import ViolationCodes as ViolationCode
|
|
7
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_module_path_exists(module_name: str) -> bool:
|
|
11
|
+
try:
|
|
12
|
+
return importlib_util.find_spec(module_name) is not None
|
|
13
|
+
except (ModuleNotFoundError, ValueError):
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
MAX_IMPORT_NAMES: typing.Final = 2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@typing.final
|
|
21
|
+
class COP001Check(ast.NodeVisitor):
|
|
22
|
+
def __init__(self, contains_all_declaration: bool) -> None:
|
|
23
|
+
self.contains_all_declaration = contains_all_declaration
|
|
24
|
+
self.violations: list[Violation] = []
|
|
25
|
+
|
|
26
|
+
def visit_ImportFrom(self, ast_node: ast.ImportFrom) -> None:
|
|
27
|
+
if ast_node.module and ast_node.level == 0:
|
|
28
|
+
self.validate_import_size(ast_node)
|
|
29
|
+
self.generic_visit(ast_node)
|
|
30
|
+
|
|
31
|
+
def validate_import_size(self, ast_node: ast.ImportFrom) -> None:
|
|
32
|
+
if len(ast_node.names) <= MAX_IMPORT_NAMES:
|
|
33
|
+
return
|
|
34
|
+
if self.contains_all_declaration:
|
|
35
|
+
return
|
|
36
|
+
module_name: typing.Final = ast_node.module
|
|
37
|
+
if module_name is not None and module_name.endswith(".settings"):
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
contains_module_import: typing.Final = any(
|
|
41
|
+
isinstance(identifier, ast.alias)
|
|
42
|
+
and module_name is not None
|
|
43
|
+
and check_module_path_exists(f"{module_name}.{identifier.name}")
|
|
44
|
+
for identifier in ast_node.names
|
|
45
|
+
)
|
|
46
|
+
if not contains_module_import:
|
|
47
|
+
self.violations.append(
|
|
48
|
+
Violation(
|
|
49
|
+
line_number=ast_node.lineno,
|
|
50
|
+
column_number=ast_node.col_offset,
|
|
51
|
+
violation_code=ViolationCode.MODULE_IMPORT,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
import typing
|
|
5
|
+
from importlib import util as importlib_util
|
|
6
|
+
|
|
7
|
+
from community_of_python_flake8_plugin.constants import ALLOWED_STDLIB_FROM_IMPORTS
|
|
8
|
+
from community_of_python_flake8_plugin.violation_codes import ViolationCodes as ViolationCode
|
|
9
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_is_stdlib_module(module_name: str) -> bool:
|
|
13
|
+
return module_name in sys.stdlib_module_names
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_is_stdlib_package(module_name: str) -> bool:
|
|
17
|
+
if not check_is_stdlib_module(module_name):
|
|
18
|
+
return False
|
|
19
|
+
module_spec: typing.Final = importlib_util.find_spec(module_name)
|
|
20
|
+
return module_spec is not None and module_spec.submodule_search_locations is not None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@typing.final
|
|
24
|
+
class COP002Check(ast.NodeVisitor):
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self.violations: list[Violation] = []
|
|
27
|
+
|
|
28
|
+
def visit_ImportFrom(self, ast_node: ast.ImportFrom) -> None:
|
|
29
|
+
if ast_node.module and ast_node.level == 0 and ast_node.module not in ALLOWED_STDLIB_FROM_IMPORTS:
|
|
30
|
+
self.validate_stdlib_import(ast_node)
|
|
31
|
+
self.generic_visit(ast_node)
|
|
32
|
+
|
|
33
|
+
def validate_stdlib_import(self, ast_node: ast.ImportFrom) -> None:
|
|
34
|
+
module_name: typing.Final = ast_node.module
|
|
35
|
+
if module_name is None:
|
|
36
|
+
return
|
|
37
|
+
if module_name == "__future__":
|
|
38
|
+
return
|
|
39
|
+
if (check_is_stdlib_module(module_name) and not check_is_stdlib_package(module_name)) or (
|
|
40
|
+
"." in module_name and check_is_stdlib_package(module_name.split(".")[0])
|
|
41
|
+
):
|
|
42
|
+
self.violations.append(
|
|
43
|
+
Violation(
|
|
44
|
+
line_number=ast_node.lineno,
|
|
45
|
+
column_number=ast_node.col_offset,
|
|
46
|
+
violation_code=ViolationCode.STDLIB_IMPORT,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from community_of_python_flake8_plugin.constants import SCALAR_ANNOTATIONS
|
|
6
|
+
from community_of_python_flake8_plugin.utils import find_parent_class_definition
|
|
7
|
+
from community_of_python_flake8_plugin.violation_codes import ViolationCodes as ViolationCode
|
|
8
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def check_is_literal_value(value: ast.AST) -> bool:
|
|
12
|
+
if isinstance(value, ast.Constant):
|
|
13
|
+
return True
|
|
14
|
+
return bool(isinstance(value, (ast.List, ast.Tuple, ast.Set, ast.Dict)))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_is_final_annotation(annotation: ast.AST) -> bool:
|
|
18
|
+
if isinstance(annotation, ast.Name):
|
|
19
|
+
return annotation.id == "Final"
|
|
20
|
+
if isinstance(annotation, ast.Attribute):
|
|
21
|
+
return annotation.attr == "Final"
|
|
22
|
+
if isinstance(annotation, ast.Subscript):
|
|
23
|
+
return check_is_final_annotation(annotation.value)
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_is_scalar_annotation(annotation: ast.AST) -> bool:
|
|
28
|
+
if isinstance(annotation, ast.Name):
|
|
29
|
+
return annotation.id in SCALAR_ANNOTATIONS
|
|
30
|
+
if isinstance(annotation, ast.Attribute):
|
|
31
|
+
return annotation.attr in SCALAR_ANNOTATIONS
|
|
32
|
+
if isinstance(annotation, ast.Subscript):
|
|
33
|
+
if check_is_final_annotation(annotation.value):
|
|
34
|
+
return check_is_scalar_annotation(annotation.slice)
|
|
35
|
+
return check_is_scalar_annotation(annotation.value)
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def find_parent_function(syntax_tree: ast.AST, ast_node: ast.AST) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
|
|
40
|
+
for potential_parent in ast.walk(syntax_tree):
|
|
41
|
+
if isinstance(potential_parent, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
42
|
+
for child in ast.walk(potential_parent):
|
|
43
|
+
if child is ast_node:
|
|
44
|
+
return potential_parent
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@typing.final
|
|
49
|
+
class COP003Check(ast.NodeVisitor):
|
|
50
|
+
def __init__(self, syntax_tree: ast.AST) -> None:
|
|
51
|
+
self.syntax_tree = syntax_tree
|
|
52
|
+
self.violations: list[Violation] = []
|
|
53
|
+
|
|
54
|
+
def visit_AnnAssign(self, ast_node: ast.AnnAssign) -> None:
|
|
55
|
+
if isinstance(ast_node.target, ast.Name):
|
|
56
|
+
parent_class: typing.Final = find_parent_class_definition(self.syntax_tree, ast_node)
|
|
57
|
+
parent_function: typing.Final = find_parent_function(self.syntax_tree, ast_node)
|
|
58
|
+
in_class_body: typing.Final = parent_class is not None and parent_function is None
|
|
59
|
+
|
|
60
|
+
if not in_class_body:
|
|
61
|
+
self.validate_scalar_annotation(ast_node)
|
|
62
|
+
self.generic_visit(ast_node)
|
|
63
|
+
|
|
64
|
+
def validate_scalar_annotation(self, ast_node: ast.AnnAssign) -> None:
|
|
65
|
+
if ast_node.value is None:
|
|
66
|
+
return
|
|
67
|
+
if not check_is_literal_value(ast_node.value):
|
|
68
|
+
return
|
|
69
|
+
if check_is_scalar_annotation(ast_node.annotation):
|
|
70
|
+
self.violations.append(
|
|
71
|
+
Violation(
|
|
72
|
+
line_number=ast_node.lineno,
|
|
73
|
+
column_number=ast_node.col_offset,
|
|
74
|
+
violation_code=ViolationCode.SCALAR_ANNOTATION,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import typing
|
|
4
|
+
|
|
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 find_parent_class_definition
|
|
7
|
+
from community_of_python_flake8_plugin.violation_codes import ViolationCodes as ViolationCode
|
|
8
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def check_is_ignored_name(identifier: str) -> bool:
|
|
12
|
+
if identifier == "_":
|
|
13
|
+
return True
|
|
14
|
+
if identifier.isupper():
|
|
15
|
+
return True
|
|
16
|
+
if identifier in {"value", "values", "pattern"}:
|
|
17
|
+
return True
|
|
18
|
+
if identifier.startswith("__") and identifier.endswith("__"):
|
|
19
|
+
return True
|
|
20
|
+
return bool(identifier.startswith("_"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check_is_whitelisted_annotation(annotation: ast.expr | None) -> bool:
|
|
24
|
+
if annotation is None:
|
|
25
|
+
return False
|
|
26
|
+
if isinstance(annotation, ast.Name):
|
|
27
|
+
return annotation.id in {"fixture", "Faker"}
|
|
28
|
+
if isinstance(annotation, ast.Attribute) and isinstance(annotation.value, ast.Name):
|
|
29
|
+
return annotation.value.id in {"pytest", "faker"}
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_is_pytest_fixture(ast_node: ast.AST) -> bool:
|
|
34
|
+
if not isinstance(ast_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
35
|
+
return False
|
|
36
|
+
return any(check_is_fixture_decorator(decorator) for decorator in ast_node.decorator_list)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def check_is_fixture_decorator(decorator: ast.expr) -> bool:
|
|
40
|
+
if isinstance(decorator, ast.Name):
|
|
41
|
+
return decorator.id == "fixture"
|
|
42
|
+
if isinstance(decorator, ast.Attribute):
|
|
43
|
+
return decorator.attr == "fixture" and isinstance(decorator.value, ast.Name) and decorator.value.id == "pytest"
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_inherits_from_whitelisted_class(class_node: ast.ClassDef) -> bool:
|
|
48
|
+
for base_class in class_node.bases:
|
|
49
|
+
if isinstance(base_class, ast.Name) and base_class.id in FINAL_CLASS_EXCLUDED_BASES:
|
|
50
|
+
return True
|
|
51
|
+
if isinstance(base_class, ast.Attribute) and base_class.attr in FINAL_CLASS_EXCLUDED_BASES:
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@typing.final
|
|
57
|
+
class COP004Check(ast.NodeVisitor):
|
|
58
|
+
def __init__(self, syntax_tree: ast.AST) -> None:
|
|
59
|
+
self.syntax_tree = syntax_tree
|
|
60
|
+
self.violations: list[Violation] = []
|
|
61
|
+
|
|
62
|
+
def visit_AnnAssign(self, ast_node: ast.AnnAssign) -> None:
|
|
63
|
+
if isinstance(ast_node.target, ast.Name):
|
|
64
|
+
parent_class: typing.Final = find_parent_class_definition(self.syntax_tree, ast_node)
|
|
65
|
+
self.validate_name_length(ast_node.target.id, ast_node, parent_class)
|
|
66
|
+
self.generic_visit(ast_node)
|
|
67
|
+
|
|
68
|
+
def visit_Assign(self, ast_node: ast.Assign) -> None:
|
|
69
|
+
for target in ast_node.targets:
|
|
70
|
+
if isinstance(target, ast.Name):
|
|
71
|
+
parent_class = find_parent_class_definition(self.syntax_tree, ast_node)
|
|
72
|
+
self.validate_name_length(target.id, ast_node, parent_class)
|
|
73
|
+
self.generic_visit(ast_node)
|
|
74
|
+
|
|
75
|
+
def visit_FunctionDef(self, ast_node: ast.FunctionDef) -> None:
|
|
76
|
+
parent_class: typing.Final = find_parent_class_definition(self.syntax_tree, ast_node)
|
|
77
|
+
self.validate_function_name(ast_node, parent_class)
|
|
78
|
+
self.validate_function_args(ast_node)
|
|
79
|
+
self.generic_visit(ast_node)
|
|
80
|
+
|
|
81
|
+
def visit_AsyncFunctionDef(self, ast_node: ast.AsyncFunctionDef) -> None:
|
|
82
|
+
parent_class: typing.Final = find_parent_class_definition(self.syntax_tree, ast_node)
|
|
83
|
+
self.validate_function_name(ast_node, parent_class)
|
|
84
|
+
self.validate_function_args(ast_node)
|
|
85
|
+
self.generic_visit(ast_node)
|
|
86
|
+
|
|
87
|
+
def visit_ClassDef(self, ast_node: ast.ClassDef) -> None:
|
|
88
|
+
if not ast_node.name.startswith("Test"):
|
|
89
|
+
self.validate_class_name_length(ast_node)
|
|
90
|
+
self.generic_visit(ast_node)
|
|
91
|
+
|
|
92
|
+
def validate_name_length(self, identifier: str, ast_node: ast.stmt, parent_class: ast.ClassDef | None) -> None:
|
|
93
|
+
if check_is_ignored_name(identifier):
|
|
94
|
+
return
|
|
95
|
+
# Only apply parent class exemption for assignments within classes
|
|
96
|
+
if (
|
|
97
|
+
parent_class
|
|
98
|
+
and isinstance(ast_node, (ast.AnnAssign, ast.Assign))
|
|
99
|
+
and check_inherits_from_whitelisted_class(parent_class)
|
|
100
|
+
):
|
|
101
|
+
return
|
|
102
|
+
if len(identifier) < MIN_NAME_LENGTH:
|
|
103
|
+
self.violations.append(
|
|
104
|
+
Violation(
|
|
105
|
+
line_number=ast_node.lineno,
|
|
106
|
+
column_number=ast_node.col_offset,
|
|
107
|
+
violation_code=ViolationCode.NAME_LENGTH,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def validate_function_name(
|
|
112
|
+
self, ast_node: ast.FunctionDef | ast.AsyncFunctionDef, parent_class: ast.ClassDef | None
|
|
113
|
+
) -> None:
|
|
114
|
+
if ast_node.name == "main":
|
|
115
|
+
return
|
|
116
|
+
if check_is_ignored_name(ast_node.name):
|
|
117
|
+
return
|
|
118
|
+
if parent_class and check_inherits_from_whitelisted_class(parent_class):
|
|
119
|
+
return
|
|
120
|
+
if check_is_pytest_fixture(ast_node):
|
|
121
|
+
return
|
|
122
|
+
if len(ast_node.name) < MIN_NAME_LENGTH:
|
|
123
|
+
self.violations.append(
|
|
124
|
+
Violation(
|
|
125
|
+
line_number=ast_node.lineno,
|
|
126
|
+
column_number=ast_node.col_offset,
|
|
127
|
+
violation_code=ViolationCode.NAME_LENGTH,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def validate_function_args(self, ast_node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
132
|
+
arguments: typing.Final = ast_node.args
|
|
133
|
+
for argument in arguments.posonlyargs + arguments.args + arguments.kwonlyargs:
|
|
134
|
+
self.validate_argument_name_length(argument)
|
|
135
|
+
if arguments.vararg is not None:
|
|
136
|
+
self.validate_argument_name_length(arguments.vararg)
|
|
137
|
+
if arguments.kwarg is not None:
|
|
138
|
+
self.validate_argument_name_length(arguments.kwarg)
|
|
139
|
+
|
|
140
|
+
def validate_argument_name_length(self, argument: ast.arg) -> None:
|
|
141
|
+
if argument.arg in {"self", "cls"}:
|
|
142
|
+
return
|
|
143
|
+
if check_is_ignored_name(argument.arg):
|
|
144
|
+
return
|
|
145
|
+
if check_is_whitelisted_annotation(argument.annotation):
|
|
146
|
+
return
|
|
147
|
+
if len(argument.arg) < MIN_NAME_LENGTH:
|
|
148
|
+
self.violations.append(
|
|
149
|
+
Violation(
|
|
150
|
+
line_number=argument.lineno,
|
|
151
|
+
column_number=argument.col_offset,
|
|
152
|
+
violation_code=ViolationCode.NAME_LENGTH,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def validate_class_name_length(self, ast_node: ast.ClassDef) -> None:
|
|
157
|
+
if check_is_ignored_name(ast_node.name):
|
|
158
|
+
return
|
|
159
|
+
if len(ast_node.name) < MIN_NAME_LENGTH:
|
|
160
|
+
self.violations.append(
|
|
161
|
+
Violation(
|
|
162
|
+
line_number=ast_node.lineno,
|
|
163
|
+
column_number=ast_node.col_offset,
|
|
164
|
+
violation_code=ViolationCode.NAME_LENGTH,
|
|
165
|
+
)
|
|
166
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from community_of_python_flake8_plugin.constants import FINAL_CLASS_EXCLUDED_BASES, VERB_PREFIXES
|
|
6
|
+
from community_of_python_flake8_plugin.utils import find_parent_class_definition
|
|
7
|
+
from community_of_python_flake8_plugin.violation_codes import ViolationCodes as ViolationCode
|
|
8
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def check_is_ignored_name(identifier: str) -> bool:
|
|
12
|
+
if identifier == "_":
|
|
13
|
+
return True
|
|
14
|
+
if identifier.isupper():
|
|
15
|
+
return True
|
|
16
|
+
if identifier in {"value", "values", "pattern"}:
|
|
17
|
+
return True
|
|
18
|
+
if identifier.startswith("__") and identifier.endswith("__"):
|
|
19
|
+
return True
|
|
20
|
+
return bool(identifier.startswith("_"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check_is_verb_name(identifier: str) -> bool:
|
|
24
|
+
return any(identifier == verb or identifier.startswith(f"{verb}_") for verb in VERB_PREFIXES)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_is_property(ast_node: ast.AST) -> bool:
|
|
28
|
+
if not isinstance(ast_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
29
|
+
return False
|
|
30
|
+
return any(check_is_property_decorator(decorator) for decorator in ast_node.decorator_list)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_is_property_decorator(decorator: ast.expr) -> bool:
|
|
34
|
+
if isinstance(decorator, ast.Name):
|
|
35
|
+
return decorator.id == "property"
|
|
36
|
+
if isinstance(decorator, ast.Attribute) and decorator.attr in {"property", "setter", "cached_property"}:
|
|
37
|
+
if isinstance(decorator.value, ast.Name) and decorator.value.id == "functools":
|
|
38
|
+
return decorator.attr == "cached_property"
|
|
39
|
+
return decorator.attr in {"property", "setter"}
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_is_pytest_fixture(ast_node: ast.AST) -> bool:
|
|
44
|
+
if not isinstance(ast_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
45
|
+
return False
|
|
46
|
+
return any(check_is_fixture_decorator(decorator) for decorator in ast_node.decorator_list)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_is_fixture_decorator(decorator: ast.expr) -> bool:
|
|
50
|
+
if isinstance(decorator, ast.Name):
|
|
51
|
+
return decorator.id == "fixture"
|
|
52
|
+
if isinstance(decorator, ast.Attribute):
|
|
53
|
+
return decorator.attr == "fixture" and isinstance(decorator.value, ast.Name) and decorator.value.id == "pytest"
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def retrieve_parent_class(syntax_tree: ast.AST, ast_node: ast.AST) -> ast.ClassDef | None:
|
|
58
|
+
return find_parent_class_definition(syntax_tree, ast_node)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@typing.final
|
|
62
|
+
class COP005Check(ast.NodeVisitor):
|
|
63
|
+
def __init__(self, syntax_tree: ast.AST) -> None:
|
|
64
|
+
self.syntax_tree = syntax_tree
|
|
65
|
+
self.violations: list[Violation] = []
|
|
66
|
+
|
|
67
|
+
def visit_FunctionDef(self, ast_node: ast.FunctionDef) -> None:
|
|
68
|
+
parent_class: typing.Final = retrieve_parent_class(self.syntax_tree, ast_node)
|
|
69
|
+
self.validate_function_name(ast_node, parent_class)
|
|
70
|
+
self.generic_visit(ast_node)
|
|
71
|
+
|
|
72
|
+
def visit_AsyncFunctionDef(self, ast_node: ast.AsyncFunctionDef) -> None:
|
|
73
|
+
parent_class: typing.Final = retrieve_parent_class(self.syntax_tree, ast_node)
|
|
74
|
+
self.validate_function_name(ast_node, parent_class)
|
|
75
|
+
self.generic_visit(ast_node)
|
|
76
|
+
|
|
77
|
+
def validate_function_name(
|
|
78
|
+
self, ast_node: ast.FunctionDef | ast.AsyncFunctionDef, parent_class: ast.ClassDef | None
|
|
79
|
+
) -> None:
|
|
80
|
+
should_skip: typing.Final = (
|
|
81
|
+
ast_node.name == "main"
|
|
82
|
+
or (ast_node.name.startswith("__") and ast_node.name.endswith("__"))
|
|
83
|
+
or check_is_ignored_name(ast_node.name)
|
|
84
|
+
or (parent_class and self.check_inherits_from_whitelisted_class(parent_class))
|
|
85
|
+
or check_is_property(ast_node)
|
|
86
|
+
or check_is_pytest_fixture(ast_node)
|
|
87
|
+
or check_is_verb_name(ast_node.name)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if should_skip:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
min_acronym_length: typing.Final = 3
|
|
94
|
+
if len(ast_node.name) < min_acronym_length: # Short names are likely acronyms or special cases
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
self.violations.append(
|
|
98
|
+
Violation(
|
|
99
|
+
line_number=ast_node.lineno,
|
|
100
|
+
column_number=ast_node.col_offset,
|
|
101
|
+
violation_code=ViolationCode.FUNCTION_VERB,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def check_inherits_from_whitelisted_class(self, ast_node: ast.ClassDef) -> bool:
|
|
106
|
+
for base_class in ast_node.bases:
|
|
107
|
+
if isinstance(base_class, ast.Name) and base_class.id in FINAL_CLASS_EXCLUDED_BASES:
|
|
108
|
+
return True
|
|
109
|
+
if isinstance(base_class, ast.Attribute) and base_class.attr in FINAL_CLASS_EXCLUDED_BASES:
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from community_of_python_flake8_plugin.violation_codes import ViolationCodes as ViolationCode
|
|
6
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_is_ignored_name(identifier: str) -> bool:
|
|
10
|
+
if identifier == "_":
|
|
11
|
+
return True
|
|
12
|
+
if identifier.isupper():
|
|
13
|
+
return True
|
|
14
|
+
if identifier in {"value", "values", "pattern"}:
|
|
15
|
+
return True
|
|
16
|
+
if identifier.startswith("__") and identifier.endswith("__"):
|
|
17
|
+
return True
|
|
18
|
+
return bool(identifier.startswith("_"))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check_is_property(ast_node: ast.AST) -> bool:
|
|
22
|
+
if not isinstance(ast_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
23
|
+
return False
|
|
24
|
+
return any(check_is_property_decorator(decorator) for decorator in ast_node.decorator_list)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_is_property_decorator(decorator: ast.expr) -> bool:
|
|
28
|
+
if isinstance(decorator, ast.Name):
|
|
29
|
+
return decorator.id == "property"
|
|
30
|
+
if isinstance(decorator, ast.Attribute) and decorator.attr in {"property", "setter", "cached_property"}:
|
|
31
|
+
if isinstance(decorator.value, ast.Name) and decorator.value.id == "functools":
|
|
32
|
+
return decorator.attr == "cached_property"
|
|
33
|
+
return decorator.attr in {"property", "setter"}
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def check_is_pytest_fixture(ast_node: ast.AST) -> bool:
|
|
38
|
+
if not isinstance(ast_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
39
|
+
return False
|
|
40
|
+
return any(check_is_fixture_decorator(decorator) for decorator in ast_node.decorator_list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_is_fixture_decorator(decorator: ast.expr) -> bool:
|
|
44
|
+
if isinstance(decorator, ast.Name):
|
|
45
|
+
return decorator.id == "fixture"
|
|
46
|
+
if isinstance(decorator, ast.Attribute):
|
|
47
|
+
return decorator.attr == "fixture" and isinstance(decorator.value, ast.Name) and decorator.value.id == "pytest"
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@typing.final
|
|
52
|
+
class COP006Check(ast.NodeVisitor):
|
|
53
|
+
def __init__(self) -> None:
|
|
54
|
+
self.violations: list[Violation] = []
|
|
55
|
+
|
|
56
|
+
def visit_AsyncFunctionDef(self, ast_node: ast.AsyncFunctionDef) -> None:
|
|
57
|
+
self._check_get_prefix(ast_node)
|
|
58
|
+
self.generic_visit(ast_node)
|
|
59
|
+
|
|
60
|
+
def _check_get_prefix(self, ast_node: ast.AsyncFunctionDef) -> None:
|
|
61
|
+
if check_is_property(ast_node) or check_is_pytest_fixture(ast_node):
|
|
62
|
+
return
|
|
63
|
+
if check_is_ignored_name(ast_node.name):
|
|
64
|
+
return
|
|
65
|
+
if ast_node.name.startswith("get_"):
|
|
66
|
+
self.violations.append(
|
|
67
|
+
Violation(
|
|
68
|
+
line_number=ast_node.lineno,
|
|
69
|
+
column_number=ast_node.col_offset,
|
|
70
|
+
violation_code=ViolationCode.ASYNC_GET_PREFIX,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from community_of_python_flake8_plugin.violation_codes import ViolationCodes as ViolationCode
|
|
6
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def collect_assignments(ast_node: ast.AST) -> dict[str, list[ast.AST]]:
|
|
10
|
+
assigned: typing.Final[dict[str, list[ast.AST]]] = {}
|
|
11
|
+
for child in ast.walk(ast_node):
|
|
12
|
+
if isinstance(child, ast.Assign):
|
|
13
|
+
for target in child.targets:
|
|
14
|
+
if isinstance(target, ast.Name):
|
|
15
|
+
assigned.setdefault(target.id, []).append(child)
|
|
16
|
+
if isinstance(child, ast.AnnAssign) and isinstance(child.target, ast.Name):
|
|
17
|
+
assigned.setdefault(child.target.id, []).append(child)
|
|
18
|
+
return assigned
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def collect_load_counts(ast_node: ast.AST) -> dict[str, int]:
|
|
22
|
+
load_counts_dict: typing.Final[dict[str, int]] = {}
|
|
23
|
+
for child in ast.walk(ast_node):
|
|
24
|
+
if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Load):
|
|
25
|
+
load_counts_dict[child.id] = load_counts_dict.get(child.id, 0) + 1
|
|
26
|
+
return load_counts_dict
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@typing.final
|
|
30
|
+
class COP007Check(ast.NodeVisitor):
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self.violations: list[Violation] = []
|
|
33
|
+
|
|
34
|
+
def visit_FunctionDef(self, ast_node: ast.FunctionDef) -> None:
|
|
35
|
+
self._check_temporary_variables(ast_node)
|
|
36
|
+
self.generic_visit(ast_node)
|
|
37
|
+
|
|
38
|
+
def visit_AsyncFunctionDef(self, ast_node: ast.AsyncFunctionDef) -> None:
|
|
39
|
+
self._check_temporary_variables(ast_node)
|
|
40
|
+
self.generic_visit(ast_node)
|
|
41
|
+
|
|
42
|
+
def _check_temporary_variables(self, ast_node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
43
|
+
assigned: typing.Final = collect_assignments(ast_node)
|
|
44
|
+
load_counts: typing.Final = collect_load_counts(ast_node)
|
|
45
|
+
for statement in ast_node.body:
|
|
46
|
+
if isinstance(statement, ast.Return) and isinstance(statement.value, ast.Name):
|
|
47
|
+
identifier = statement.value.id
|
|
48
|
+
if len(assigned.get(identifier, [])) == 1 and load_counts.get(identifier, 0) == 1:
|
|
49
|
+
self.violations.append(
|
|
50
|
+
Violation(
|
|
51
|
+
line_number=statement.lineno,
|
|
52
|
+
column_number=statement.col_offset,
|
|
53
|
+
violation_code=ViolationCode.TEMPORARY_VARIABLE,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from community_of_python_flake8_plugin.constants import FINAL_CLASS_EXCLUDED_BASES
|
|
6
|
+
from community_of_python_flake8_plugin.violation_codes import ViolationCodes as ViolationCode
|
|
7
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_is_true_literal(ast_node: ast.AST | None) -> bool:
|
|
11
|
+
return isinstance(ast_node, ast.Constant) and ast_node.value is True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def contains_final_decorator(ast_node: ast.ClassDef) -> bool:
|
|
15
|
+
for decorator in ast_node.decorator_list:
|
|
16
|
+
target_name = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
17
|
+
if isinstance(target_name, ast.Name) and target_name.id == "final":
|
|
18
|
+
return True
|
|
19
|
+
if isinstance(target_name, ast.Attribute) and target_name.attr == "final":
|
|
20
|
+
return True
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def check_inherits_from_whitelisted_class(ast_node: ast.ClassDef) -> bool:
|
|
25
|
+
for base_class in ast_node.bases:
|
|
26
|
+
if isinstance(base_class, ast.Name) and base_class.id in FINAL_CLASS_EXCLUDED_BASES:
|
|
27
|
+
return True
|
|
28
|
+
if isinstance(base_class, ast.Attribute) and base_class.attr in FINAL_CLASS_EXCLUDED_BASES:
|
|
29
|
+
return True
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def retrieve_dataclass_decorator(ast_node: ast.ClassDef) -> ast.expr | None:
|
|
34
|
+
for decorator in ast_node.decorator_list:
|
|
35
|
+
target_name = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
36
|
+
if isinstance(target_name, ast.Name) and target_name.id == "dataclass":
|
|
37
|
+
return decorator
|
|
38
|
+
if isinstance(target_name, ast.Attribute) and target_name.attr == "dataclass":
|
|
39
|
+
return decorator
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def check_is_dataclass(ast_node: ast.ClassDef) -> bool:
|
|
44
|
+
return retrieve_dataclass_decorator(ast_node) is not None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@typing.final
|
|
48
|
+
class COP008Check(ast.NodeVisitor):
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self.violations: list[Violation] = []
|
|
51
|
+
|
|
52
|
+
def visit_ClassDef(self, ast_node: ast.ClassDef) -> None:
|
|
53
|
+
self._check_final_decorator(ast_node)
|
|
54
|
+
self.generic_visit(ast_node)
|
|
55
|
+
|
|
56
|
+
def _check_final_decorator(self, ast_node: ast.ClassDef) -> None:
|
|
57
|
+
if (
|
|
58
|
+
not check_is_dataclass(ast_node)
|
|
59
|
+
and not contains_final_decorator(ast_node)
|
|
60
|
+
and not ast_node.name.startswith("Test")
|
|
61
|
+
and not check_inherits_from_whitelisted_class(ast_node)
|
|
62
|
+
):
|
|
63
|
+
self.violations.append(
|
|
64
|
+
Violation(
|
|
65
|
+
line_number=ast_node.lineno,
|
|
66
|
+
column_number=ast_node.col_offset,
|
|
67
|
+
violation_code=ViolationCode.FINAL_CLASS,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from community_of_python_flake8_plugin.constants import MAPPING_PROXY_TYPES
|
|
6
|
+
from community_of_python_flake8_plugin.violation_codes import ViolationCodes as ViolationCode
|
|
7
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_is_mapping_literal(value: ast.AST | None) -> bool:
|
|
11
|
+
if isinstance(value, ast.Dict):
|
|
12
|
+
return True
|
|
13
|
+
if isinstance(value, ast.Call):
|
|
14
|
+
if check_is_typed_dict_call(value):
|
|
15
|
+
return False
|
|
16
|
+
return any(isinstance(argument, ast.Dict) for argument in value.args)
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def check_is_typed_dict_call(value: ast.Call) -> bool:
|
|
21
|
+
if isinstance(value.func, ast.Name) and value.func.id == "TypedDict":
|
|
22
|
+
return True
|
|
23
|
+
if isinstance(value.func, ast.Attribute) and value.func.attr == "TypedDict":
|
|
24
|
+
return isinstance(value.func.value, ast.Name) and value.func.value.id in {
|
|
25
|
+
"typing",
|
|
26
|
+
"typing_extensions",
|
|
27
|
+
}
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_is_mapping_proxy_call(value: ast.AST | None) -> bool:
|
|
32
|
+
if not isinstance(value, ast.Call):
|
|
33
|
+
return False
|
|
34
|
+
if isinstance(value.func, ast.Name):
|
|
35
|
+
return value.func.id in MAPPING_PROXY_TYPES
|
|
36
|
+
if isinstance(value.func, ast.Attribute):
|
|
37
|
+
return value.func.attr in MAPPING_PROXY_TYPES
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@typing.final
|
|
42
|
+
class COP009Check(ast.NodeVisitor):
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
self.violations: list[Violation] = []
|
|
45
|
+
|
|
46
|
+
def visit_Module(self, ast_node: ast.Module) -> None:
|
|
47
|
+
for statement in ast_node.body:
|
|
48
|
+
self._check_module_assignment(statement)
|
|
49
|
+
self.generic_visit(ast_node)
|
|
50
|
+
|
|
51
|
+
def _check_module_assignment(self, statement: ast.stmt) -> None:
|
|
52
|
+
value = None
|
|
53
|
+
if isinstance(statement, (ast.Assign, ast.AnnAssign)):
|
|
54
|
+
value = statement.value
|
|
55
|
+
|
|
56
|
+
if value and check_is_mapping_literal(value) and not check_is_mapping_proxy_call(value):
|
|
57
|
+
self.violations.append(
|
|
58
|
+
Violation(
|
|
59
|
+
line_number=statement.lineno,
|
|
60
|
+
column_number=statement.col_offset,
|
|
61
|
+
violation_code=ViolationCode.MAPPING_PROXY,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from community_of_python_flake8_plugin.violation_codes import ViolationCodes as ViolationCode
|
|
6
|
+
from community_of_python_flake8_plugin.violations import Violation
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_is_true_literal(ast_node: ast.AST | None) -> bool:
|
|
10
|
+
return isinstance(ast_node, ast.Constant) and ast_node.value is True
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def retrieve_dataclass_decorator(ast_node: ast.ClassDef) -> ast.expr | None:
|
|
14
|
+
for decorator in ast_node.decorator_list:
|
|
15
|
+
target_name = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
16
|
+
if isinstance(target_name, ast.Name) and target_name.id == "dataclass":
|
|
17
|
+
return decorator
|
|
18
|
+
if isinstance(target_name, ast.Attribute) and target_name.attr == "dataclass":
|
|
19
|
+
return decorator
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check_is_dataclass(ast_node: ast.ClassDef) -> bool:
|
|
24
|
+
return retrieve_dataclass_decorator(ast_node) is not None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check_is_exception_class(_node: ast.ClassDef) -> bool:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_is_inheriting(ast_node: ast.ClassDef) -> bool:
|
|
32
|
+
return len(ast_node.bases) > 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def check_dataclass_has_keyword(decorator: ast.expr, identifier: str, value: bool | None = None) -> bool:
|
|
36
|
+
if not isinstance(decorator, ast.Call):
|
|
37
|
+
return False
|
|
38
|
+
for keyword in decorator.keywords:
|
|
39
|
+
if keyword.arg != identifier:
|
|
40
|
+
continue
|
|
41
|
+
if value is None:
|
|
42
|
+
return True
|
|
43
|
+
return isinstance(keyword.value, ast.Constant) and keyword.value.value is value
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_dataclass_has_required_args(decorator: ast.expr, *, require_slots: bool, require_frozen: bool) -> bool:
|
|
48
|
+
if not isinstance(decorator, ast.Call):
|
|
49
|
+
return False
|
|
50
|
+
keywords: typing.Final = {keyword.arg: keyword.value for keyword in decorator.keywords if keyword.arg}
|
|
51
|
+
if not check_is_true_literal(keywords.get("kw_only")):
|
|
52
|
+
return False
|
|
53
|
+
if require_slots and not check_is_true_literal(keywords.get("slots")):
|
|
54
|
+
return False
|
|
55
|
+
return not (require_frozen and not check_is_true_literal(keywords.get("frozen")))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@typing.final
|
|
59
|
+
class COP010Check(ast.NodeVisitor):
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self.violations: list[Violation] = []
|
|
62
|
+
|
|
63
|
+
def visit_ClassDef(self, ast_node: ast.ClassDef) -> None:
|
|
64
|
+
self._check_dataclass_config(ast_node)
|
|
65
|
+
self.generic_visit(ast_node)
|
|
66
|
+
|
|
67
|
+
def _check_dataclass_config(self, ast_node: ast.ClassDef) -> None:
|
|
68
|
+
if not check_is_dataclass(ast_node):
|
|
69
|
+
return
|
|
70
|
+
decorator: typing.Final = retrieve_dataclass_decorator(ast_node)
|
|
71
|
+
if decorator is None:
|
|
72
|
+
return
|
|
73
|
+
if check_is_inheriting(ast_node):
|
|
74
|
+
return
|
|
75
|
+
require_slots: typing.Final = not check_dataclass_has_keyword(decorator, "init", value=False)
|
|
76
|
+
require_frozen: typing.Final = require_slots and not check_is_exception_class(ast_node)
|
|
77
|
+
if not check_dataclass_has_required_args(decorator, require_slots=require_slots, require_frozen=require_frozen):
|
|
78
|
+
self.violations.append(
|
|
79
|
+
Violation(
|
|
80
|
+
line_number=ast_node.lineno,
|
|
81
|
+
column_number=ast_node.col_offset,
|
|
82
|
+
violation_code=ViolationCode.DATACLASS_CONFIG,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
MIN_NAME_LENGTH: typing.Final = 8
|
|
6
|
+
|
|
7
|
+
VERB_PREFIXES: typing.Final = {
|
|
8
|
+
"validate",
|
|
9
|
+
"test",
|
|
10
|
+
"execute",
|
|
11
|
+
"visit",
|
|
12
|
+
"get",
|
|
13
|
+
"cancel",
|
|
14
|
+
"retrieve",
|
|
15
|
+
"lock",
|
|
16
|
+
"assert",
|
|
17
|
+
"extract",
|
|
18
|
+
"enrich",
|
|
19
|
+
"run",
|
|
20
|
+
"patch",
|
|
21
|
+
"build",
|
|
22
|
+
"start",
|
|
23
|
+
"ping",
|
|
24
|
+
"prepare",
|
|
25
|
+
"publish",
|
|
26
|
+
"request",
|
|
27
|
+
"ack",
|
|
28
|
+
"nack",
|
|
29
|
+
"reject",
|
|
30
|
+
"stop",
|
|
31
|
+
"route",
|
|
32
|
+
"begin",
|
|
33
|
+
"subscribe",
|
|
34
|
+
"connect",
|
|
35
|
+
"close",
|
|
36
|
+
"enter",
|
|
37
|
+
"exit",
|
|
38
|
+
"take",
|
|
39
|
+
"dump",
|
|
40
|
+
"iter",
|
|
41
|
+
"escape",
|
|
42
|
+
"unescape",
|
|
43
|
+
"add",
|
|
44
|
+
"contains",
|
|
45
|
+
"unsubscribe",
|
|
46
|
+
"resubscribe",
|
|
47
|
+
"commit",
|
|
48
|
+
"raise",
|
|
49
|
+
"mock",
|
|
50
|
+
"produce",
|
|
51
|
+
"consume",
|
|
52
|
+
"track",
|
|
53
|
+
"wait",
|
|
54
|
+
"bootstrap",
|
|
55
|
+
"calculate",
|
|
56
|
+
"check",
|
|
57
|
+
"collect",
|
|
58
|
+
"compute",
|
|
59
|
+
"convert",
|
|
60
|
+
"create",
|
|
61
|
+
"delete",
|
|
62
|
+
"fetch",
|
|
63
|
+
"find",
|
|
64
|
+
"format",
|
|
65
|
+
"generate",
|
|
66
|
+
"handle",
|
|
67
|
+
"has",
|
|
68
|
+
"is",
|
|
69
|
+
"list",
|
|
70
|
+
"load",
|
|
71
|
+
"make",
|
|
72
|
+
"parse",
|
|
73
|
+
"process",
|
|
74
|
+
"read",
|
|
75
|
+
"receive",
|
|
76
|
+
"remove",
|
|
77
|
+
"render",
|
|
78
|
+
"resolve",
|
|
79
|
+
"save",
|
|
80
|
+
"send",
|
|
81
|
+
"set",
|
|
82
|
+
"should",
|
|
83
|
+
"update",
|
|
84
|
+
"write",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
SCALAR_ANNOTATIONS: typing.Final = {"int", "str", "float", "bool", "bytes", "complex"}
|
|
88
|
+
|
|
89
|
+
MAPPING_PROXY_TYPES: typing.Final = {"MappingProxyType"}
|
|
90
|
+
|
|
91
|
+
ALLOWED_STDLIB_FROM_IMPORTS: typing.Final = {"collections.abc"}
|
|
92
|
+
|
|
93
|
+
FINAL_CLASS_EXCLUDED_BASES: typing.Final = {"BaseModel", "RootModel", "ModelFactory"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
if typing.TYPE_CHECKING:
|
|
6
|
+
import ast
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from community_of_python_flake8_plugin.checks import execute_all_validations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typing.final
|
|
14
|
+
class CommunityOfPythonFlake8Plugin:
|
|
15
|
+
name = "community-of-python-flake8-plugin" # noqa: COP004
|
|
16
|
+
version = "0.1.27" # noqa: COP004
|
|
17
|
+
|
|
18
|
+
def __init__(self, tree: ast.AST) -> None: # noqa: COP004
|
|
19
|
+
self.ast_tree = tree
|
|
20
|
+
|
|
21
|
+
def run(self) -> Iterable[tuple[int, int, str, type[object]]]: # noqa: COP004
|
|
22
|
+
violations_list: typing.Final = execute_all_validations(self.ast_tree)
|
|
23
|
+
for violation in violations_list:
|
|
24
|
+
violation_message = f"{violation.violation_code.code} {violation.violation_code.description}"
|
|
25
|
+
yield violation.line_number, violation.column_number, violation_message, type(self)
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def check_module_has_all_declaration(ast_node: ast.Module) -> bool:
|
|
6
|
+
for statement in ast_node.body:
|
|
7
|
+
if isinstance(statement, ast.Assign) and any(
|
|
8
|
+
isinstance(target, ast.Name) and target.id == "__all__" for target in statement.targets
|
|
9
|
+
):
|
|
10
|
+
return True
|
|
11
|
+
if (
|
|
12
|
+
isinstance(statement, ast.AnnAssign)
|
|
13
|
+
and isinstance(statement.target, ast.Name)
|
|
14
|
+
and statement.target.id == "__all__"
|
|
15
|
+
):
|
|
16
|
+
return True
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_parent_class_definition(syntax_tree: ast.AST, ast_node: ast.AST) -> ast.ClassDef | None:
|
|
21
|
+
for potential_parent in ast.walk(syntax_tree):
|
|
22
|
+
if isinstance(potential_parent, ast.ClassDef):
|
|
23
|
+
for child in ast.walk(potential_parent):
|
|
24
|
+
if child is ast_node:
|
|
25
|
+
return potential_parent
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def find_parent_function(syntax_tree: ast.AST, ast_node: ast.AST) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
|
|
30
|
+
for potential_parent in ast.walk(syntax_tree):
|
|
31
|
+
if isinstance(potential_parent, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
32
|
+
for child in ast.walk(potential_parent):
|
|
33
|
+
if child is ast_node:
|
|
34
|
+
return potential_parent
|
|
35
|
+
return None
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import dataclasses
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@typing.final
|
|
7
|
+
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
|
|
8
|
+
class ViolationCode:
|
|
9
|
+
code: str # noqa: COP004
|
|
10
|
+
description: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typing.final
|
|
14
|
+
class ViolationCodes:
|
|
15
|
+
MODULE_IMPORT = ViolationCode(code="COP001", description="Use module import when importing more than two names")
|
|
16
|
+
STDLIB_IMPORT = ViolationCode(code="COP002", description="Import standard library modules as whole modules")
|
|
17
|
+
SCALAR_ANNOTATION = ViolationCode(code="COP003", description="Avoid explicit scalar type annotations")
|
|
18
|
+
NAME_LENGTH = ViolationCode(code="COP004", description="Name must be at least 8 characters")
|
|
19
|
+
FUNCTION_VERB = ViolationCode(code="COP005", description="Function identifier must be a verb")
|
|
20
|
+
ASYNC_GET_PREFIX = ViolationCode(code="COP006", description="Avoid get_ prefix in async function names")
|
|
21
|
+
TEMPORARY_VARIABLE = ViolationCode(code="COP007", description="Avoid temporary variables used only once")
|
|
22
|
+
FINAL_CLASS = ViolationCode(code="COP008", description="Classes should be marked typing.final")
|
|
23
|
+
MAPPING_PROXY = ViolationCode(code="COP009", description="Wrap module dictionaries with types.MappingProxyType")
|
|
24
|
+
DATACLASS_CONFIG = ViolationCode(
|
|
25
|
+
code="COP010", description="Use dataclasses with kw_only=True, slots=True, frozen=True"
|
|
26
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import dataclasses
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
if typing.TYPE_CHECKING:
|
|
7
|
+
from .violation_codes import ViolationCode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@typing.final
|
|
11
|
+
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
|
|
12
|
+
class Violation:
|
|
13
|
+
line_number: int
|
|
14
|
+
column_number: int
|
|
15
|
+
violation_code: ViolationCode
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: community-of-python-flake8-plugin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Community of Python flake8 plugin
|
|
5
|
+
Author: Lev Vereshchagin
|
|
6
|
+
Author-email: Lev Vereshchagin <mail@vrslev.com>
|
|
7
|
+
Requires-Dist: flake8
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# community-of-python-flake8-plugin
|
|
12
|
+
|
|
13
|
+
Community of Python flake8 plugin with custom code style checks.
|
|
14
|
+
|
|
15
|
+
## Run with uv
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv run --with flake8 --with "community-of-python-flake8-plugin @ git+https://github.com/community-of-python/pylines.git@code-style-tests#subdirectory=community-of-python-flake8-plugin" -- flake8 --select COP --exclude .venv .
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## flake8 config (pyproject.toml)
|
|
22
|
+
|
|
23
|
+
```toml
|
|
24
|
+
[tool.flake8]
|
|
25
|
+
select = ["COP"]
|
|
26
|
+
exclude = [".venv"]
|
|
27
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
community_of_python_flake8_plugin/__init__.py,sha256=nbK0ThjxTd-7g9SrYc_YWtuqyfl9OkXcJca5nAxYG3s,129
|
|
2
|
+
community_of_python_flake8_plugin/checks/README.md,sha256=FHhhvoL-73kcgrjnogiGhXSaBixgamJs1pKyZgTFkl0,1528
|
|
3
|
+
community_of_python_flake8_plugin/checks/__init__.py,sha256=A0n1YctyMM36vm-3SODvVULWBO2HeE4C7OwA8lCrmbw,3458
|
|
4
|
+
community_of_python_flake8_plugin/checks/cop001.py,sha256=6fsyF-4F9So0p-uHgVW7_AZZKnUX7ovNtHyFtkkiw7c,1855
|
|
5
|
+
community_of_python_flake8_plugin/checks/cop002.py,sha256=SPM3WDOSGBhcXa5NkYkYcFZXQZntbt7clDGHzuOH2PU,1839
|
|
6
|
+
community_of_python_flake8_plugin/checks/cop003.py,sha256=1qoEOaih3takr9NbdUoCJNjmaHCKoFO00luyQBqRd60,3074
|
|
7
|
+
community_of_python_flake8_plugin/checks/cop004.py,sha256=8yiTYJ980BbuMjWu2KxOS0bn523Ys1tvUEbgkLGKnGI,6840
|
|
8
|
+
community_of_python_flake8_plugin/checks/cop005.py,sha256=XYUPbYbimU23tPHpGbuvHAe4ioGnlZ0j6Jm-ARWnUgo,4503
|
|
9
|
+
community_of_python_flake8_plugin/checks/cop006.py,sha256=yQdIC1iIAn-rO28h5_uYaY7-nj27aNMQy0UrrqxOeZQ,2698
|
|
10
|
+
community_of_python_flake8_plugin/checks/cop007.py,sha256=EpGP9oNETtURswA4M7mAIfJYjGxFLuqmahoyZu96_d8,2390
|
|
11
|
+
community_of_python_flake8_plugin/checks/cop008.py,sha256=JBqWPya04QepAwQ3uYcSpWxgMr58airTYEK7SHZm9Mg,2661
|
|
12
|
+
community_of_python_flake8_plugin/checks/cop009.py,sha256=08TbX_7HzshNWVpbR_276PH-ZkCTgVuEKtJEfwQ4yoQ,2223
|
|
13
|
+
community_of_python_flake8_plugin/checks/cop010.py,sha256=-SnaeNVADDl5_FhsPUriekaWMz7kn--7-tKn4g1JssM,3260
|
|
14
|
+
community_of_python_flake8_plugin/constants.py,sha256=Xp7jJRBZx1hKeSqhG22APKNc3DgwsHwMVrYYl4F1ZHs,1467
|
|
15
|
+
community_of_python_flake8_plugin/plugin.py,sha256=sqGiWRUiROV4Ooxvn-87BE1c47V2oP3xylpTQQvCWkE,866
|
|
16
|
+
community_of_python_flake8_plugin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
community_of_python_flake8_plugin/utils.py,sha256=Mee2tp9ps-zRHEC3muxYf5HqlrKHum6FQgPOXRCEMxY,1318
|
|
18
|
+
community_of_python_flake8_plugin/violation_codes.py,sha256=ZwQPTLGJHHTSeUcNzvj1XuRcgoBNoTKLSkuTXQnu7jo,1365
|
|
19
|
+
community_of_python_flake8_plugin/violations.py,sha256=gIXWHs4QEQ1zh-u5uMBJ4jXo4ERb8MwyZfmI4iNChTE,315
|
|
20
|
+
community_of_python_flake8_plugin-0.1.0.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
21
|
+
community_of_python_flake8_plugin-0.1.0.dist-info/entry_points.txt,sha256=TVYG2EjuPUWGB7OMdNP3d2shi9sRmDK_K92mDYDpxkQ,97
|
|
22
|
+
community_of_python_flake8_plugin-0.1.0.dist-info/METADATA,sha256=4hiQa_diUQTWfY5Ejag1wpaNHgLwJM_BhlDRKVpO9cI,737
|
|
23
|
+
community_of_python_flake8_plugin-0.1.0.dist-info/RECORD,,
|