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,36 @@
1
+ """Flake8 plugin for Elegant Objects violations.
2
+
3
+ This plugin detects violations of the Elegant Objects principles including
4
+ "-er" entities, null usage, mutable objects, static methods, and more.
5
+
6
+ Based on Yegor Bugayenko's principles from elegantobjects.org
7
+ """
8
+
9
+ import ast
10
+ from collections.abc import Iterator
11
+ from typing import Any
12
+
13
+ from .base import ElegantObjectsCore
14
+
15
+
16
+ class ElegantObjectsPlugin:
17
+ """Flake8 plugin to check for Elegant Objects violations."""
18
+
19
+ name = "flake8-elegant-objects"
20
+ version = "1.0.0"
21
+
22
+ def __init__(self, tree: ast.AST) -> None:
23
+ self.tree = tree
24
+ self._core = ElegantObjectsCore(tree)
25
+
26
+ def run(self) -> Iterator[tuple[int, int, str, type["ElegantObjectsPlugin"]]]:
27
+ """Run the checker and yield errors."""
28
+ violations = self._core.check_violations()
29
+ for violation in violations:
30
+ yield (violation.line, violation.column, violation.message, type(self))
31
+
32
+
33
+ # Entry point for flake8 plugin registration
34
+ def factory(_app: Any) -> type[ElegantObjectsPlugin]:
35
+ """Factory function for flake8 plugin."""
36
+ return ElegantObjectsPlugin
@@ -0,0 +1,68 @@
1
+ """Standalone command-line interface for flake8-elegant-objects."""
2
+
3
+ import argparse
4
+ import ast
5
+ import sys
6
+
7
+ from .base import ElegantObjectsCore
8
+
9
+
10
+ def main() -> None:
11
+ """Standalone command-line interface."""
12
+ parser = argparse.ArgumentParser(
13
+ description="Check Python files for Elegant Objects violations"
14
+ )
15
+ parser.add_argument("files", nargs="+", help="Python files to check")
16
+ parser.add_argument(
17
+ "--show-source",
18
+ action="store_true",
19
+ help="Show source code context for violations",
20
+ )
21
+
22
+ args = parser.parse_args()
23
+
24
+ total_errors = 0
25
+
26
+ for file_path in args.files:
27
+ if not file_path.endswith(".py"):
28
+ continue
29
+
30
+ try:
31
+ with open(file_path, encoding="utf-8") as f:
32
+ source = f.read()
33
+
34
+ tree = ast.parse(source, filename=file_path)
35
+ core = ElegantObjectsCore(tree)
36
+
37
+ file_errors = 0
38
+ violations = core.check_violations()
39
+
40
+ for violation in violations:
41
+ print(
42
+ f"{file_path}:{violation.line}:{violation.column}: {violation.message}"
43
+ )
44
+
45
+ if args.show_source:
46
+ lines = source.split("\n")
47
+ if 0 <= violation.line - 1 < len(lines):
48
+ print(f" {lines[violation.line - 1].strip()}")
49
+ print()
50
+
51
+ file_errors += 1
52
+ total_errors += 1
53
+
54
+ if file_errors == 0:
55
+ print(f"{file_path}: No violations found ✓")
56
+
57
+ except Exception as e:
58
+ print(f"Error processing {file_path}: {e}", file=sys.stderr)
59
+
60
+ if total_errors > 0:
61
+ print(f"\nTotal violations found: {total_errors}")
62
+ sys.exit(1)
63
+ else:
64
+ print("\nAll files comply with Elegant Objects principles! ✓")
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()
@@ -0,0 +1,180 @@
1
+ """Base classes and protocols for Elegant Objects checkers."""
2
+
3
+ import ast
4
+ from typing import Protocol
5
+
6
+
7
+ class ErrorCodes:
8
+ """Centralized error message definitions."""
9
+
10
+ # Naming violations (EO001-EO004)
11
+ EO001 = "EO001 Class name '{name}' violates -er principle (describes what it does, not what it is)"
12
+ EO002 = (
13
+ "EO002 Method name '{name}' violates -er principle (should be noun, not verb)"
14
+ )
15
+ EO003 = (
16
+ "EO003 Variable name '{name}' violates -er principle (should be noun, not verb)"
17
+ )
18
+ EO004 = (
19
+ "EO004 Function name '{name}' violates -er principle (should be noun, not verb)"
20
+ )
21
+ EO005 = "EO005 Null (None) usage violates EO principle (avoid None)"
22
+ EO006 = "EO006 Code in constructor violates EO principle (constructors should only assign parameters)"
23
+ EO007 = "EO007 Getter/setter method '{name}' violates EO principle (avoid getters/setters)"
24
+ EO008 = "EO008 Mutable object violation: '{name}' should be immutable"
25
+ EO009 = (
26
+ "EO009 Static method '{name}' violates EO principle (no static methods allowed)"
27
+ )
28
+ EO010 = "EO010 isinstance/type casting violates EO principle (avoid type discrimination)"
29
+ EO011 = "EO011 Public method '{name}' without contract (Protocol/ABC) violates EO principle"
30
+ EO012 = "EO012 Test method '{name}' contains non-assertThat statements (only assertThat allowed)"
31
+ EO013 = "EO013 ORM/ActiveRecord pattern '{name}' violates EO principle"
32
+ EO014 = "EO014 Implementation inheritance violates EO principle (class '{name}' inherits from non-abstract class)"
33
+
34
+
35
+ class Violation:
36
+ """Represents a detected violation."""
37
+
38
+ def __init__(self, line: int, column: int, message: str) -> None:
39
+ self._line = line
40
+ self._column = column
41
+ self._message = message
42
+
43
+ @property
44
+ def line(self) -> int:
45
+ return self._line
46
+
47
+ @property
48
+ def column(self) -> int:
49
+ return self._column
50
+
51
+ @property
52
+ def message(self) -> str:
53
+ return self._message
54
+
55
+
56
+ Violations = list[Violation]
57
+
58
+
59
+ class Source:
60
+ """Aggregation of AST node and current class context."""
61
+
62
+ def __init__(
63
+ self,
64
+ node: ast.AST,
65
+ current_class: ast.ClassDef | None = None,
66
+ tree: ast.AST | None = None,
67
+ ) -> None:
68
+ self._node = node
69
+ self._current_class = current_class
70
+ self._tree = tree
71
+
72
+ @property
73
+ def node(self) -> ast.AST:
74
+ return self._node
75
+
76
+ @property
77
+ def current_class(self) -> ast.ClassDef | None:
78
+ return self._current_class
79
+
80
+ @property
81
+ def tree(self) -> ast.AST | None:
82
+ return self._tree
83
+
84
+
85
+ class Principle(Protocol):
86
+ """Protocol for Elegant Objects principles analysis."""
87
+
88
+ def check(self, source: Source) -> Violations:
89
+ """Check source for violations and return list of detected violations."""
90
+ ...
91
+
92
+
93
+ def violation(node: ast.AST, message: str) -> Violations:
94
+ """Create a violation if node has location information."""
95
+ if hasattr(node, "lineno") and hasattr(node, "col_offset"):
96
+ return [Violation(node.lineno, node.col_offset, message)]
97
+ return []
98
+
99
+
100
+ def is_method(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
101
+ """Check if function is a method (has self parameter)."""
102
+ if not node.args.args:
103
+ return False
104
+ return node.args.args[0].arg in ("self", "cls")
105
+
106
+
107
+ def get_all_principles() -> list[Principle]:
108
+ """Get all available Elegant Objects principle checkers."""
109
+ # Import here to avoid circular imports
110
+ from .no_constructor_code import NoConstructorCode
111
+ from .no_er_name import NoErName
112
+ from .no_getters_setters import NoGettersSetters
113
+ from .no_implementation_inheritance import NoImplementationInheritance
114
+ from .no_impure_tests import NoImpureTests
115
+ from .no_mutable_objects import NoMutableObjects
116
+ from .no_null import NoNull
117
+ from .no_orm import NoOrm
118
+ from .no_public_methods_without_contracts import NoPublicMethodsWithoutContracts
119
+ from .no_static import NoStatic
120
+ from .no_type_discrimination import NoTypeDiscrimination
121
+
122
+ return [
123
+ NoErName(),
124
+ NoNull(),
125
+ NoConstructorCode(),
126
+ NoGettersSetters(),
127
+ NoMutableObjects(),
128
+ NoStatic(),
129
+ NoTypeDiscrimination(),
130
+ NoPublicMethodsWithoutContracts(),
131
+ NoImpureTests(),
132
+ NoOrm(),
133
+ NoImplementationInheritance(),
134
+ ]
135
+
136
+
137
+ class ElegantObjectsCore:
138
+ """Core analyzer for Elegant Objects violations."""
139
+
140
+ def __init__(self, tree: ast.AST) -> None:
141
+ self.tree = tree
142
+
143
+ def check_violations(self) -> list[Violation]:
144
+ """Check for all violations in the AST tree."""
145
+ violations = []
146
+ for violation in self._visit(self.tree, None):
147
+ violations.append(violation)
148
+ return violations
149
+
150
+ def _visit(
151
+ self, node: ast.AST, current_class: ast.ClassDef | None = None
152
+ ) -> list[Violation]:
153
+ """Visit AST nodes and check for violations."""
154
+ violations = []
155
+
156
+ if isinstance(node, ast.ClassDef):
157
+ current_class = node
158
+
159
+ # Check principles on current node
160
+ violations.extend(self._check_principles(node, current_class))
161
+
162
+ # Visit child nodes
163
+ for child in ast.iter_child_nodes(node):
164
+ violations.extend(self._visit(child, current_class))
165
+
166
+ return violations
167
+
168
+ def _check_principles(
169
+ self, node: ast.AST, current_class: ast.ClassDef | None
170
+ ) -> list[Violation]:
171
+ """Check all principles against the given node."""
172
+ violations = []
173
+ source = Source(node, current_class, self.tree)
174
+ principles = get_all_principles()
175
+
176
+ for principle in principles:
177
+ principle_violations = principle.check(source)
178
+ violations.extend(principle_violations)
179
+
180
+ return violations
@@ -0,0 +1,45 @@
1
+ """No constructor code principle checker for Elegant Objects violations."""
2
+
3
+ import ast
4
+
5
+ from .base import ErrorCodes, Source, Violations, is_method, violation
6
+
7
+
8
+ class NoConstructorCode:
9
+ """Checks for code in constructors beyond parameter assignments (EO006)."""
10
+
11
+ def check(self, source: Source) -> Violations:
12
+ """Check source for constructor code violations."""
13
+ node = source.node
14
+ if not isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
15
+ return []
16
+ return self._check_constructor_code(node)
17
+
18
+ def _check_constructor_code(
19
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef
20
+ ) -> Violations:
21
+ """Check for code in constructors beyond parameter assignments."""
22
+ if node.name != "__init__" or not is_method(node):
23
+ return []
24
+
25
+ violations = []
26
+ # Constructors should only contain assignments to self.attribute = parameter
27
+ for stmt in node.body:
28
+ if isinstance(stmt, ast.Assign):
29
+ # Check if it's a simple self.attr = param assignment
30
+ if len(stmt.targets) == 1 and isinstance(
31
+ stmt.targets[0], ast.Attribute
32
+ ):
33
+ target = stmt.targets[0]
34
+ if isinstance(target.value, ast.Name) and target.value.id == "self":
35
+ # This is a self.attr assignment, check if value is a simple name
36
+ if not isinstance(stmt.value, ast.Name):
37
+ violations.extend(violation(stmt, ErrorCodes.EO006))
38
+ else:
39
+ violations.extend(violation(stmt, ErrorCodes.EO006))
40
+ else:
41
+ violations.extend(violation(stmt, ErrorCodes.EO006))
42
+ elif not isinstance(stmt, ast.Pass): # Allow pass statements
43
+ violations.extend(violation(stmt, ErrorCodes.EO006))
44
+
45
+ return violations
@@ -0,0 +1,306 @@
1
+ """Naming violations checker for Elegant Objects principles."""
2
+
3
+ import ast
4
+ import re
5
+ from typing import ClassVar
6
+
7
+ from .base import ErrorCodes, Source, Violations, is_method, violation
8
+
9
+
10
+ class NoErName:
11
+ """Checks for naming violations in classes, methods, variables, and functions."""
12
+
13
+ # Hall of shame: common -er suffixes (from elegantobjects.org)
14
+ ER_SUFFIXES: ClassVar[set[str]] = {
15
+ "accumulator",
16
+ "adapter",
17
+ "aggregator",
18
+ "analyzer",
19
+ "broker",
20
+ "builder",
21
+ "calculator",
22
+ "checker",
23
+ "collector",
24
+ "compiler",
25
+ "compressor",
26
+ "consumer",
27
+ "controller",
28
+ "converter",
29
+ "coordinator",
30
+ "creator",
31
+ "decoder",
32
+ "decompressor",
33
+ "deserializer",
34
+ "dispatcher",
35
+ "displayer",
36
+ "encoder",
37
+ "evaluator",
38
+ "executor",
39
+ "exporter",
40
+ "factory",
41
+ "fetcher",
42
+ "filter",
43
+ "finder",
44
+ "formatter",
45
+ "generator",
46
+ "handler",
47
+ "helper",
48
+ "importer",
49
+ "interpreter",
50
+ "joiner",
51
+ "listener",
52
+ "loader",
53
+ "manager",
54
+ "mediator",
55
+ "merger",
56
+ "monitor",
57
+ "observer",
58
+ "orchestrator",
59
+ "organizer",
60
+ "parser",
61
+ "printer",
62
+ "processor",
63
+ "producer",
64
+ "provider",
65
+ "reader",
66
+ "renderer",
67
+ "reporter",
68
+ "router",
69
+ "runner",
70
+ "saver",
71
+ "scanner",
72
+ "scheduler",
73
+ "serializer",
74
+ "sorter",
75
+ "splitter",
76
+ "supplier",
77
+ "synchronizer",
78
+ "tracker",
79
+ "transformer",
80
+ "validator",
81
+ "worker",
82
+ "wrapper",
83
+ "writer",
84
+ }
85
+
86
+ # Common procedural verbs that should be nouns
87
+ PROCEDURAL_VERBS: ClassVar[set[str]] = {
88
+ "accumulate",
89
+ "add",
90
+ "aggregate",
91
+ "analyze",
92
+ "append",
93
+ "build",
94
+ "calculate",
95
+ "change",
96
+ "check",
97
+ "clean",
98
+ "clear",
99
+ "close",
100
+ "collect",
101
+ "compile",
102
+ "compress",
103
+ "control",
104
+ "convert",
105
+ "create",
106
+ "decode",
107
+ "decompress",
108
+ "delete",
109
+ "deserialize",
110
+ "dispatch",
111
+ "display",
112
+ "do",
113
+ "encode",
114
+ "evaluate",
115
+ "execute",
116
+ "export",
117
+ "fetch",
118
+ "filter",
119
+ "find",
120
+ "format",
121
+ "generate",
122
+ "get",
123
+ "handle",
124
+ "hide",
125
+ "import",
126
+ "insert",
127
+ "interpret",
128
+ "join",
129
+ "load",
130
+ "manage",
131
+ "merge",
132
+ "modify",
133
+ "open",
134
+ "organize",
135
+ "parse",
136
+ "pause",
137
+ "prepend",
138
+ "print",
139
+ "process",
140
+ "put",
141
+ "read",
142
+ "receive",
143
+ "refresh",
144
+ "remove",
145
+ "render",
146
+ "reset",
147
+ "resume",
148
+ "retrieve",
149
+ "route",
150
+ "run",
151
+ "save",
152
+ "schedule",
153
+ "search",
154
+ "send",
155
+ "serialize",
156
+ "set",
157
+ "show",
158
+ "sort",
159
+ "split",
160
+ "start",
161
+ "stop",
162
+ "store",
163
+ "transform",
164
+ "transmit",
165
+ "update",
166
+ "validate",
167
+ "write",
168
+ }
169
+
170
+ # Allowed exceptions (common patterns that are OK)
171
+ ALLOWED_EXCEPTIONS: ClassVar[set[str]] = {
172
+ "buffer",
173
+ "character",
174
+ "cluster",
175
+ "container",
176
+ "counter",
177
+ "error",
178
+ "footer",
179
+ "header",
180
+ "identifier",
181
+ "number",
182
+ "order",
183
+ "owner",
184
+ "parameter",
185
+ "pointer",
186
+ "register",
187
+ "server",
188
+ "timer",
189
+ "user",
190
+ }
191
+
192
+ def check(self, source: Source) -> Violations:
193
+ """Check source for naming violations."""
194
+ violations = []
195
+ node = source.node
196
+
197
+ if isinstance(node, ast.ClassDef):
198
+ violations.extend(self._check_class_name(node))
199
+ elif isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
200
+ violations.extend(self._check_function_name(node))
201
+ elif isinstance(node, ast.Assign):
202
+ violations.extend(self._check_variable_assignment(node))
203
+ elif isinstance(node, ast.AnnAssign):
204
+ violations.extend(self._check_annotated_assignment(node))
205
+
206
+ return violations
207
+
208
+ def _check_class_name(self, node: ast.ClassDef) -> Violations:
209
+ """Check if class name violates -er principle."""
210
+ name = node.name.lower()
211
+
212
+ # Skip if it's an allowed exception
213
+ if name in self.ALLOWED_EXCEPTIONS:
214
+ return []
215
+
216
+ # Check for -er suffixes (the hall of shame)
217
+ for suffix in self.ER_SUFFIXES:
218
+ if name.endswith(suffix):
219
+ return violation(node, ErrorCodes.EO001.format(name=node.name))
220
+
221
+ # Check for procedural patterns in compound names
222
+ if self._contains_procedural_pattern(name):
223
+ return violation(node, ErrorCodes.EO001.format(name=node.name))
224
+
225
+ return []
226
+
227
+ def _check_function_name(
228
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef
229
+ ) -> Violations:
230
+ """Check if function/method name violates -er principle."""
231
+ # Skip special methods (__init__, __str__, etc.)
232
+ if node.name.startswith("_"):
233
+ return []
234
+
235
+ # Skip common property patterns
236
+ if node.name in ("property", "getter", "setter"):
237
+ return []
238
+
239
+ # Check for procedural verbs
240
+ if self._starts_with_procedural_verb(node.name):
241
+ # Determine if it's a method or standalone function
242
+ error_code = ErrorCodes.EO002 if is_method(node) else ErrorCodes.EO004
243
+ return violation(node, error_code.format(name=node.name))
244
+
245
+ return []
246
+
247
+ def _check_variable_assignment(self, node: ast.Assign) -> Violations:
248
+ """Check variable names in assignments."""
249
+ violations = []
250
+ for target in node.targets:
251
+ if isinstance(target, ast.Name):
252
+ violations.extend(self._check_variable_name(target))
253
+ return violations
254
+
255
+ def _check_annotated_assignment(self, node: ast.AnnAssign) -> Violations:
256
+ """Check variable names in annotated assignments."""
257
+ if isinstance(node.target, ast.Name):
258
+ return self._check_variable_name(node.target)
259
+ return []
260
+
261
+ def _check_variable_name(self, node: ast.Name) -> Violations:
262
+ """Check if variable name violates -er principle."""
263
+ # Skip private variables and common patterns
264
+ if node.id.startswith("_") or node.id.isupper():
265
+ return []
266
+
267
+ name = node.id.lower()
268
+
269
+ # Skip if it's an allowed exception
270
+ if name in self.ALLOWED_EXCEPTIONS:
271
+ return []
272
+
273
+ # Check for -er suffixes
274
+ for suffix in self.ER_SUFFIXES:
275
+ if name.endswith(suffix):
276
+ return violation(node, ErrorCodes.EO003.format(name=node.id))
277
+
278
+ # Check for procedural verbs as variable names
279
+ if self._starts_with_procedural_verb(name):
280
+ return violation(node, ErrorCodes.EO003.format(name=node.id))
281
+
282
+ return []
283
+
284
+ def _contains_procedural_pattern(self, name: str) -> bool:
285
+ """Check if name contains procedural patterns."""
286
+ # Split camelCase/snake_case into words
287
+ words = re.findall(r"[a-z]+", name)
288
+
289
+ # Check if any word is a procedural verb
290
+ return any(word in self.PROCEDURAL_VERBS for word in words)
291
+
292
+ def _starts_with_procedural_verb(self, name: str) -> bool:
293
+ """Check if name starts with a procedural verb."""
294
+ # Split camelCase/snake_case and check first word
295
+ # First split on underscores, then on camelCase boundaries
296
+ words: list[str] = []
297
+ for part in name.split("_"):
298
+ # Split camelCase: insert space before uppercase letters
299
+ camel_split = re.sub(r"([a-z])([A-Z])", r"\1 \2", part)
300
+ words.extend(word.lower() for word in camel_split.split() if word)
301
+
302
+ if not words:
303
+ return False
304
+
305
+ first_word = words[0]
306
+ return first_word in self.PROCEDURAL_VERBS
@@ -0,0 +1,57 @@
1
+ """No getters/setters principle checker for Elegant Objects violations."""
2
+
3
+ import ast
4
+
5
+ from .base import ErrorCodes, Source, Violations, is_method, violation
6
+
7
+
8
+ class NoGettersSetters:
9
+ """Checks for getter/setter methods (EO007)."""
10
+
11
+ def check(self, source: Source) -> Violations:
12
+ """Check source for getter/setter violations."""
13
+ node = source.node
14
+ if not isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
15
+ return []
16
+ return self._check_getters_setters(node)
17
+
18
+ def _check_getters_setters(
19
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef
20
+ ) -> Violations:
21
+ """Check for getter/setter methods."""
22
+ if not is_method(node) or node.name.startswith("_"):
23
+ return []
24
+
25
+ # Skip methods with @property decorator
26
+ for decorator in node.decorator_list:
27
+ if isinstance(decorator, ast.Name) and decorator.id == "property":
28
+ return []
29
+
30
+ name = node.name.lower()
31
+ original_name = node.name
32
+
33
+ # Check for getter patterns
34
+ if (
35
+ name.startswith("get_")
36
+ or (
37
+ name.startswith("get")
38
+ and len(original_name) > 3
39
+ and original_name[3].isupper()
40
+ )
41
+ or name == "get"
42
+ ):
43
+ return violation(node, ErrorCodes.EO007.format(name=node.name))
44
+
45
+ # Check for setter patterns
46
+ if (
47
+ name.startswith("set_")
48
+ or (
49
+ name.startswith("set")
50
+ and len(original_name) > 3
51
+ and original_name[3].isupper()
52
+ )
53
+ or name == "set"
54
+ ):
55
+ return violation(node, ErrorCodes.EO007.format(name=node.name))
56
+
57
+ return []