flake8-elegant-objects 1.0.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.
@@ -0,0 +1,75 @@
1
+ """No implementation inheritance principle checker for Elegant Objects violations."""
2
+
3
+ import ast
4
+
5
+ from .base import ErrorCodes, Source, Violations, violation
6
+
7
+
8
+ class NoImplementationInheritance:
9
+ """Checks for implementation inheritance violations (EO014)."""
10
+
11
+ def check(self, source: Source) -> Violations:
12
+ """Check source for implementation inheritance violations."""
13
+ node = source.node
14
+
15
+ if isinstance(node, ast.ClassDef):
16
+ return self._check_implementation_inheritance(node)
17
+
18
+ return []
19
+
20
+ def _check_implementation_inheritance(self, node: ast.ClassDef) -> Violations:
21
+ """Check for implementation inheritance violations."""
22
+ for base in node.bases:
23
+ is_abstract_base = False
24
+
25
+ if isinstance(base, ast.Name):
26
+ # Allow inheritance from abstract base classes and common patterns
27
+ allowed_bases = {
28
+ # Abstract bases
29
+ "ABC",
30
+ "Protocol",
31
+ # Exception hierarchy (standard pattern)
32
+ "Exception",
33
+ "BaseException",
34
+ "ValueError",
35
+ "TypeError",
36
+ "RuntimeError",
37
+ "AttributeError",
38
+ "KeyError",
39
+ "IndexError",
40
+ "ImportError",
41
+ "OSError",
42
+ # Standard library abstract bases
43
+ "Enum",
44
+ "IntEnum",
45
+ "Flag",
46
+ "IntFlag",
47
+ # Generic object (unavoidable in Python)
48
+ "object",
49
+ }
50
+ is_abstract_base = base.id in allowed_bases
51
+
52
+ elif isinstance(base, ast.Attribute):
53
+ # Check for module.AbstractBase patterns
54
+ if base.attr in {"Protocol", "ABC"}:
55
+ is_abstract_base = True
56
+ elif isinstance(base.value, ast.Name) and base.value.id in {
57
+ "abc",
58
+ "typing",
59
+ "collections",
60
+ "enum",
61
+ }:
62
+ is_abstract_base = True
63
+ # Check for imported ABC/Protocol
64
+ elif isinstance(base.value, ast.Name) and base.attr in {
65
+ "ABC",
66
+ "abstractmethod",
67
+ "Protocol",
68
+ }:
69
+ is_abstract_base = True
70
+
71
+ # If not an abstract base, it's implementation inheritance
72
+ if not is_abstract_base:
73
+ return violation(node, ErrorCodes.EO014.format(name=node.name))
74
+
75
+ return []
@@ -0,0 +1,127 @@
1
+ """No impure tests principle checker for Elegant Objects violations."""
2
+
3
+ import ast
4
+
5
+ from .base import ErrorCodes, Source, Violations, violation
6
+
7
+
8
+ class NoImpureTests:
9
+ """Checks for impure test methods violations (EO012)."""
10
+
11
+ def check(self, source: Source) -> Violations:
12
+ """Check source for impure test method violations."""
13
+ node = source.node
14
+
15
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
16
+ return self._check_test_methods(node)
17
+
18
+ return []
19
+
20
+ def _check_test_methods(
21
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef
22
+ ) -> Violations:
23
+ """Check that test methods only contain single assertion statements."""
24
+ if not node.name.startswith("test_"):
25
+ return []
26
+
27
+ violations = []
28
+ assertion_count = 0
29
+
30
+ for stmt in node.body:
31
+ if isinstance(stmt, ast.Pass):
32
+ continue # Allow pass statements
33
+
34
+ elif isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
35
+ # Check if it's an assertion
36
+ if self._is_assertion_call(stmt.value):
37
+ assertion_count += 1
38
+ continue
39
+ else:
40
+ # Non-assertion expression call
41
+ violations.extend(
42
+ violation(stmt, ErrorCodes.EO012.format(name=node.name))
43
+ )
44
+
45
+ elif isinstance(stmt, ast.Assert):
46
+ # Direct assert statement
47
+ assertion_count += 1
48
+ continue
49
+
50
+ elif isinstance(stmt, ast.With):
51
+ # Check for pytest.raises or similar context managers
52
+ if self._is_assertion_context_manager(stmt):
53
+ assertion_count += 1
54
+ continue
55
+ else:
56
+ violations.extend(
57
+ violation(stmt, ErrorCodes.EO012.format(name=node.name))
58
+ )
59
+
60
+ else:
61
+ # Any other statement (assignments, etc.) is a violation
62
+ violations.extend(
63
+ violation(stmt, ErrorCodes.EO012.format(name=node.name))
64
+ )
65
+
66
+ # Test must have exactly one assertion
67
+ if assertion_count == 0:
68
+ violations.extend(violation(node, ErrorCodes.EO012.format(name=node.name)))
69
+ elif assertion_count > 1:
70
+ violations.extend(violation(node, ErrorCodes.EO012.format(name=node.name)))
71
+
72
+ return violations
73
+
74
+ def _is_assertion_call(self, call: ast.Call) -> bool:
75
+ """Check if a call is an assertion."""
76
+ # Check for unittest style assertions (self.assertEqual, self.assertTrue, etc.)
77
+ if isinstance(call.func, ast.Attribute):
78
+ if call.func.attr.startswith("assert"):
79
+ return True
80
+ # Check for chained assertions like assertThat(...).isEqualTo(...)
81
+ if self._contains_assertion_in_chain(call):
82
+ return True
83
+
84
+ # Check for standalone assertion functions
85
+ if isinstance(call.func, ast.Name):
86
+ if call.func.id.startswith("assert") or call.func.id == "assertThat":
87
+ return True
88
+
89
+ return False
90
+
91
+ def _contains_assertion_in_chain(self, call: ast.Call) -> bool:
92
+ """Check if assertion exists anywhere in the call chain."""
93
+ current = call
94
+ while isinstance(current, ast.Call):
95
+ if isinstance(current.func, ast.Name):
96
+ if (
97
+ current.func.id.startswith("assert")
98
+ or current.func.id == "assertThat"
99
+ ):
100
+ return True
101
+ elif isinstance(current.func, ast.Attribute):
102
+ if (
103
+ current.func.attr.startswith("assert")
104
+ or current.func.attr == "assertThat"
105
+ ):
106
+ return True
107
+ # Move to the next level in the chain
108
+ if isinstance(current.func.value, ast.Call):
109
+ current = current.func.value
110
+ else:
111
+ break
112
+ else:
113
+ break
114
+ return False
115
+
116
+ def _is_assertion_context_manager(self, with_stmt: ast.With) -> bool:
117
+ """Check if with statement is for assertions like pytest.raises."""
118
+ for item in with_stmt.items:
119
+ if isinstance(item.context_expr, ast.Call):
120
+ if isinstance(item.context_expr.func, ast.Attribute):
121
+ # Check for pytest.raises, unittest.assertRaises, etc.
122
+ if item.context_expr.func.attr in {"raises", "assertRaises"}:
123
+ return True
124
+ elif isinstance(item.context_expr.func, ast.Name):
125
+ if item.context_expr.func.id in {"raises", "assertRaises"}:
126
+ return True
127
+ return False
@@ -0,0 +1,69 @@
1
+ """No mutable objects principle checker for Elegant Objects violations."""
2
+
3
+ import ast
4
+
5
+ from .base import ErrorCodes, Source, Violations, violation
6
+
7
+
8
+ class NoMutableObjects:
9
+ """Checks for mutable object violations (EO008)."""
10
+
11
+ def check(self, source: Source) -> Violations:
12
+ """Check source for mutable object violations."""
13
+ node = source.node
14
+ if not isinstance(node, ast.ClassDef):
15
+ return []
16
+ return self._check_mutable_class(node)
17
+
18
+ def _check_mutable_class(self, node: ast.ClassDef) -> Violations:
19
+ """Check for mutable class violations."""
20
+ violations = []
21
+
22
+ # Look for @dataclass decorator without frozen=True
23
+ has_dataclass = False
24
+ has_frozen = False
25
+
26
+ for decorator in node.decorator_list:
27
+ if isinstance(decorator, ast.Name) and decorator.id == "dataclass":
28
+ has_dataclass = True
29
+ elif isinstance(decorator, ast.Call):
30
+ if (
31
+ isinstance(decorator.func, ast.Name)
32
+ and decorator.func.id == "dataclass"
33
+ ):
34
+ has_dataclass = True
35
+ # Check for frozen=True
36
+ for keyword in decorator.keywords:
37
+ if keyword.arg == "frozen" and isinstance(
38
+ keyword.value, ast.Constant
39
+ ):
40
+ if keyword.value.value is True:
41
+ has_frozen = True
42
+
43
+ # If it's a dataclass without frozen=True, it's mutable
44
+ if has_dataclass and not has_frozen:
45
+ violations.extend(violation(node, ErrorCodes.EO008.format(name=node.name)))
46
+
47
+ # Check for mutable instance attributes in class body
48
+ for stmt in node.body:
49
+ if isinstance(stmt, ast.Assign):
50
+ for target in stmt.targets:
51
+ if isinstance(target, ast.Name):
52
+ # This is a class attribute, check if it's mutable
53
+ if self._is_mutable_type(stmt.value):
54
+ violations.extend(
55
+ violation(stmt, ErrorCodes.EO008.format(name=target.id))
56
+ )
57
+
58
+ return violations
59
+
60
+ def _is_mutable_type(self, node: ast.AST) -> bool:
61
+ """Check if a node represents a mutable type."""
62
+ if isinstance(node, ast.List | ast.Dict | ast.Set):
63
+ return True
64
+
65
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
66
+ mutable_types = {"list", "dict", "set", "bytearray"}
67
+ return node.func.id in mutable_types
68
+
69
+ return False
@@ -0,0 +1,50 @@
1
+ """No null principle checker for Elegant Objects violations."""
2
+
3
+ import ast
4
+
5
+ from .base import ErrorCodes, Source, Violations, violation
6
+
7
+
8
+ class NoNull:
9
+ """Checks for None usage violations (EO005)."""
10
+
11
+ def check(self, source: Source) -> Violations:
12
+ """Check source for None usage violations."""
13
+ node = source.node
14
+ if isinstance(node, ast.Constant) and node.value is None:
15
+ # Skip None in type annotations
16
+ if self._is_in_type_annotation(node, source.tree):
17
+ return []
18
+ return violation(node, ErrorCodes.EO005)
19
+ return []
20
+
21
+ def _is_in_type_annotation(
22
+ self, target_node: ast.AST, tree: ast.AST | None
23
+ ) -> bool:
24
+ """Check if the target node is within a type annotation context."""
25
+ if not tree:
26
+ return False
27
+
28
+ # Find all annotation contexts in the tree
29
+ for node in ast.walk(tree):
30
+ # Function return annotations
31
+ if (
32
+ isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef)
33
+ and node.returns
34
+ ):
35
+ if self._node_in_subtree(target_node, node.returns):
36
+ return True
37
+ # Parameter annotations
38
+ elif isinstance(node, ast.arg) and node.annotation:
39
+ if self._node_in_subtree(target_node, node.annotation):
40
+ return True
41
+ # Variable annotations
42
+ elif isinstance(node, ast.AnnAssign) and node.annotation:
43
+ if self._node_in_subtree(target_node, node.annotation):
44
+ return True
45
+
46
+ return False
47
+
48
+ def _node_in_subtree(self, target: ast.AST, tree: ast.AST) -> bool:
49
+ """Check if target node is within the tree."""
50
+ return any(child is target for child in ast.walk(tree))
@@ -0,0 +1,97 @@
1
+ """No ORM principle checker for Elegant Objects violations."""
2
+
3
+ import ast
4
+
5
+ from .base import ErrorCodes, Source, Violations, violation
6
+
7
+
8
+ class NoOrm:
9
+ """Checks for ORM/ActiveRecord pattern violations (EO013)."""
10
+
11
+ def check(self, source: Source) -> Violations:
12
+ """Check source for ORM pattern violations."""
13
+ node = source.node
14
+
15
+ if isinstance(node, ast.Call):
16
+ return self._check_orm_patterns(node)
17
+
18
+ return []
19
+
20
+ def _check_orm_patterns(self, node: ast.Call) -> Violations:
21
+ """Check for ORM/ActiveRecord patterns."""
22
+ if not isinstance(node.func, ast.Attribute):
23
+ return []
24
+
25
+ orm_methods = {
26
+ "save",
27
+ "delete",
28
+ "destroy",
29
+ "update",
30
+ "create",
31
+ "reload",
32
+ "find",
33
+ "find_by",
34
+ "where",
35
+ "filter",
36
+ "filter_by",
37
+ "get",
38
+ "get_or_create",
39
+ "select",
40
+ "insert",
41
+ "update_all",
42
+ "delete_all",
43
+ "execute",
44
+ "query",
45
+ "order_by",
46
+ "group_by",
47
+ "having",
48
+ "limit",
49
+ "offset",
50
+ "join",
51
+ "includes",
52
+ "eager_load",
53
+ "preload",
54
+ "create_table",
55
+ "drop_table",
56
+ "add_column",
57
+ "remove_column",
58
+ }
59
+ if node.func.attr not in orm_methods:
60
+ return []
61
+
62
+ # Check if this is a valid non-ORM usage
63
+ if self._is_allowed_method_usage(node.func.value):
64
+ return []
65
+
66
+ return violation(node, ErrorCodes.EO013.format(name=node.func.attr))
67
+
68
+ def _is_allowed_method_usage(self, value: ast.AST) -> bool:
69
+ """Check if the method usage is allowed (not ORM)."""
70
+ # Built-in types
71
+ if isinstance(value, ast.Name) and value.id in {
72
+ "list",
73
+ "dict",
74
+ "set",
75
+ "tuple",
76
+ "str",
77
+ "int",
78
+ "float",
79
+ "bool",
80
+ }:
81
+ return True
82
+
83
+ # Allow methods on list/dict variables
84
+ if isinstance(value, ast.Name) and value.id.endswith("_list"):
85
+ return True
86
+
87
+ # Literal values
88
+ if isinstance(value, ast.Constant | ast.List | ast.Dict | ast.Tuple | ast.Set):
89
+ return True
90
+
91
+ # Constructor calls
92
+ return (
93
+ isinstance(value, ast.Call)
94
+ and isinstance(value.func, ast.Name)
95
+ and value.func.id
96
+ in {"open", "int", "str", "list", "dict", "set", "tuple", "bool", "float"}
97
+ )
@@ -0,0 +1,125 @@
1
+ """No public methods without contracts principle checker for Python."""
2
+
3
+ import ast
4
+
5
+ from .base import ErrorCodes, Principle, Source, Violations, is_method, violation
6
+
7
+
8
+ class NoPublicMethodsWithoutContracts(Principle):
9
+ """Check that public methods are defined by contracts (Protocol/ABC)."""
10
+
11
+ def check(self, source: Source) -> Violations:
12
+ """Check for public methods without contracts."""
13
+ violations: Violations = []
14
+
15
+ if not isinstance(source.node, ast.FunctionDef):
16
+ return violations
17
+
18
+ if not source.current_class or not is_method(source.node):
19
+ return violations
20
+
21
+ if source.node.name.startswith("_"):
22
+ return violations
23
+
24
+ if source.node.name.startswith("__") and source.node.name.endswith("__"):
25
+ return violations
26
+ if self._class_has_contract(source.current_class, source.tree):
27
+ if not self._method_from_contract(
28
+ source.node.name, source.current_class, source.tree
29
+ ):
30
+ violations.extend(
31
+ violation(
32
+ source.node, ErrorCodes.EO011.format(name=source.node.name)
33
+ )
34
+ )
35
+ else:
36
+ violations.extend(
37
+ violation(source.node, ErrorCodes.EO011.format(name=source.node.name))
38
+ )
39
+
40
+ return violations
41
+
42
+ def _class_has_contract(
43
+ self, class_node: ast.ClassDef, tree: ast.AST | None
44
+ ) -> bool:
45
+ """Check if class implements any Protocol or ABC."""
46
+ if not class_node.bases:
47
+ return False
48
+
49
+ for base in class_node.bases:
50
+ base_name = self._get_base_name(base)
51
+ if not base_name:
52
+ continue
53
+
54
+ if self._is_protocol_or_abc(base_name, tree):
55
+ return True
56
+
57
+ return False
58
+
59
+ def _method_from_contract(
60
+ self, method_name: str, class_node: ast.ClassDef, tree: ast.AST | None
61
+ ) -> bool:
62
+ """Check if method is defined in any of the class's contracts."""
63
+ for base in class_node.bases:
64
+ base_name = self._get_base_name(base)
65
+ if not base_name:
66
+ continue
67
+
68
+ base_class = self._find_class_def(base_name, tree)
69
+ if not base_class:
70
+ if self._is_protocol_or_abc(base_name, tree):
71
+ return True
72
+ continue
73
+
74
+ if self._has_method(base_class, method_name):
75
+ if self._is_protocol_or_abc(base_name, tree):
76
+ return True
77
+
78
+ return False
79
+
80
+ def _get_base_name(self, base: ast.expr) -> str | None:
81
+ """Extract base class name from AST node."""
82
+ if isinstance(base, ast.Name):
83
+ return base.id
84
+ elif isinstance(base, ast.Attribute):
85
+ return base.attr
86
+ return None
87
+
88
+ def _is_protocol_or_abc(self, class_name: str, tree: ast.AST | None) -> bool:
89
+ """Check if a class is a Protocol or ABC."""
90
+ if class_name in ("Protocol", "ABC", "ABCMeta"):
91
+ return True
92
+
93
+ if class_name.endswith("Protocol") or class_name.endswith("ABC"):
94
+ return True
95
+
96
+ if tree:
97
+ class_def = self._find_class_def(class_name, tree)
98
+ if class_def:
99
+ for base in class_def.bases:
100
+ base_name = self._get_base_name(base)
101
+ if base_name and self._is_protocol_or_abc(base_name, None):
102
+ return True
103
+
104
+ return False
105
+
106
+ def _find_class_def(
107
+ self, class_name: str, tree: ast.AST | None
108
+ ) -> ast.ClassDef | None:
109
+ """Find class definition in the AST tree."""
110
+ if not tree:
111
+ return None
112
+
113
+ for node in ast.walk(tree):
114
+ if isinstance(node, ast.ClassDef) and node.name == class_name:
115
+ return node
116
+
117
+ return None
118
+
119
+ def _has_method(self, class_node: ast.ClassDef, method_name: str) -> bool:
120
+ """Check if class has a method with given name."""
121
+ for node in class_node.body:
122
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
123
+ if node.name == method_name:
124
+ return True
125
+ return False
@@ -0,0 +1,31 @@
1
+ """No static methods principle checker for Elegant Objects violations."""
2
+
3
+ import ast
4
+
5
+ from .base import ErrorCodes, Source, Violations, violation
6
+
7
+
8
+ class NoStatic:
9
+ """Checks for static method violations (EO009)."""
10
+
11
+ def check(self, source: Source) -> Violations:
12
+ """Check source for static method violations."""
13
+ node = source.node
14
+
15
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
16
+ return self._check_static_methods(node)
17
+
18
+ return []
19
+
20
+ def _check_static_methods(
21
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef
22
+ ) -> Violations:
23
+ """Check for static methods violations."""
24
+ # Check for @staticmethod decorator
25
+ for decorator in node.decorator_list:
26
+ if isinstance(decorator, ast.Name) and decorator.id in {
27
+ "staticmethod",
28
+ "classmethod",
29
+ }:
30
+ return violation(node, ErrorCodes.EO009.format(name=node.name))
31
+ return []
@@ -0,0 +1,34 @@
1
+ """No type discrimination principle checker for Elegant Objects violations."""
2
+
3
+ import ast
4
+
5
+ from .base import ErrorCodes, Source, Violations, violation
6
+
7
+
8
+ class NoTypeDiscrimination:
9
+ """Checks for type discrimination violations (EO010)."""
10
+
11
+ def check(self, source: Source) -> Violations:
12
+ """Check source for type discrimination violations."""
13
+ node = source.node
14
+
15
+ if isinstance(node, ast.Call):
16
+ return self._check_isinstance_usage(node)
17
+
18
+ return []
19
+
20
+ def _check_isinstance_usage(self, node: ast.Call) -> Violations:
21
+ """Check for isinstance, type casting, or reflection usage."""
22
+ if isinstance(node.func, ast.Name):
23
+ forbidden_funcs = {
24
+ "isinstance",
25
+ "type",
26
+ "hasattr",
27
+ "getattr",
28
+ "setattr",
29
+ "delattr",
30
+ "callable",
31
+ }
32
+ if node.func.id in forbidden_funcs:
33
+ return violation(node, ErrorCodes.EO010)
34
+ return []