flake8-elegant-objects 2.0.0__tar.gz → 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/PKG-INFO +1 -1
  2. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/__init__.py +1 -1
  3. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_impure_tests.py +14 -15
  4. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_null.py +21 -27
  5. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/pyproject.toml +1 -1
  6. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_impure_tests.py +15 -0
  7. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_null.py +7 -2
  8. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/uv.lock +1 -1
  9. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/.github/workflows/lint.yml +0 -0
  10. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/.github/workflows/publish.yml +0 -0
  11. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/.github/workflows/test.yml +0 -0
  12. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/.gitignore +0 -0
  13. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/CHANGELOG.md +0 -0
  14. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/LICENSE +0 -0
  15. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/README.md +0 -0
  16. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/__main__.py +0 -0
  17. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/base.py +0 -0
  18. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/links.py +0 -0
  19. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_constructor_code.py +0 -0
  20. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_er_name.py +0 -0
  21. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_getters_setters.py +0 -0
  22. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_implementation_inheritance.py +0 -0
  23. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_mutable_objects/__init__.py +0 -0
  24. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_mutable_objects/base.py +0 -0
  25. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_mutable_objects/copy_on_write_checker.py +0 -0
  26. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_mutable_objects/core.py +0 -0
  27. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_mutable_objects/deep_checker.py +0 -0
  28. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_mutable_objects/pattern_detectors.py +0 -0
  29. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_mutable_objects/shared_state_checker.py +0 -0
  30. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_open_classes.py +0 -0
  31. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_orm.py +0 -0
  32. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_public_methods_without_contracts.py +0 -0
  33. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_static.py +0 -0
  34. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/no_type_discrimination.py +0 -0
  35. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/flake8_elegant_objects/noqa.py +0 -0
  36. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/__init__.py +0 -0
  37. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_cli.py +0 -0
  38. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_flake8_plugin.py +0 -0
  39. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_integration.py +0 -0
  40. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_links.py +0 -0
  41. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_constructor_code.py +0 -0
  42. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_er_name.py +0 -0
  43. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_getters_setters.py +0 -0
  44. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_implementation_inheritance.py +0 -0
  45. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_mutable_objects.py +0 -0
  46. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_open_classes.py +0 -0
  47. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_orm.py +0 -0
  48. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_public_methods_without_contracts.py +0 -0
  49. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_static.py +0 -0
  50. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_no_type_discrimination.py +0 -0
  51. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_noqa.py +0 -0
  52. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_packaging.py +0 -0
  53. {flake8_elegant_objects-2.0.0 → flake8_elegant_objects-2.0.1}/tests/test_self_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flake8-elegant-objects
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: Flake8 plugin enforcing Elegant Objects principles: no -er naming, no null, no getters/setters, immutable objects
5
5
  Project-URL: Homepage, https://github.com/AntonProkopyev/flake8-elegant-objects
6
6
  Project-URL: Repository, https://github.com/AntonProkopyev/flake8-elegant-objects.git
@@ -18,7 +18,7 @@ class ElegantObjectsPlugin:
18
18
  """Flake8 plugin to check for Elegant Objects violations."""
19
19
 
20
20
  name = "flake8-elegant-objects"
21
- version = "2.0.0"
21
+ version = "2.0.1"
22
22
 
23
23
  def __init__(self, tree: ast.AST) -> None:
24
24
  self.tree = tree
@@ -146,25 +146,24 @@ class NoImpureTests(Principle):
146
146
  """Check if assertion exists anywhere in the call chain."""
147
147
  current = call
148
148
  while CALL.covers(current):
149
+ # A plain name is the head of the chain: there is nothing
150
+ # beneath it to descend into, so it answers and the walk ends.
151
+ # Left to loop, it would answer the same question forever.
149
152
  if NAME.covers(current.func):
150
- if (
153
+ return (
151
154
  current.func.id.startswith("assert")
152
155
  or current.func.id == "assertThat"
153
- ):
154
- return True
155
- elif ATTRIBUTE.covers(current.func):
156
- if (
157
- current.func.attr.startswith("assert")
158
- or current.func.attr == "assertThat"
159
- ):
160
- return True
161
- # Move to the next level in the chain
162
- if CALL.covers(current.func.value):
163
- current = current.func.value
164
- else:
165
- break
166
- else:
156
+ )
157
+ if not ATTRIBUTE.covers(current.func):
167
158
  break
159
+ if (
160
+ current.func.attr.startswith("assert")
161
+ or current.func.attr == "assertThat"
162
+ ):
163
+ return True
164
+ if not CALL.covers(current.func.value):
165
+ break
166
+ current = current.func.value
168
167
  return False
169
168
 
170
169
  def _is_assertion_context_manager(self, with_stmt: ast.With) -> bool:
@@ -5,7 +5,8 @@ from collections.abc import Iterator
5
5
  from types import NoneType
6
6
  from typing import final
7
7
 
8
- from .base import EO005, REPORT, Instance, Principle, Source, Violations
8
+ from .base import EO005, REPORT, Instance, Parents, Principle, Source, Violations
9
+ from .base import NOTHING as ABSENT
9
10
 
10
11
  ANN_ASSIGN = Instance(ast.AnnAssign)
11
12
  ARG = Instance(ast.arg)
@@ -34,7 +35,7 @@ class NoNull(Principle):
34
35
  return self._check_implicit_returns(node)
35
36
  if CONSTANT.covers(node) and NOTHING.covers(node.value):
36
37
  # Skip None in type annotations
37
- if self._is_in_type_annotation(node, source.tree):
38
+ if self._annotated(node, source.parents):
38
39
  return []
39
40
  return REPORT.of(node, EO005)
40
41
  return []
@@ -64,30 +65,23 @@ class NoNull(Principle):
64
65
  if RETURN.covers(inner):
65
66
  yield inner
66
67
 
67
- def _is_in_type_annotation(
68
- self, target_node: ast.AST, tree: ast.AST | None
69
- ) -> bool:
70
- """Check if the target node is within a type annotation context."""
71
- if not tree:
72
- return False
73
-
74
- # Find all annotation contexts in the tree
75
- for node in ast.walk(tree):
76
- # Function return annotations
77
- if FUNCTION.covers(node) and node.returns:
78
- if self._node_in_subtree(target_node, node.returns):
79
- return True
80
- # Parameter annotations
81
- elif ARG.covers(node) and node.annotation:
82
- if self._node_in_subtree(target_node, node.annotation):
83
- return True
84
- # Variable annotations
85
- elif ANN_ASSIGN.covers(node) and node.annotation:
86
- if self._node_in_subtree(target_node, node.annotation):
87
- return True
68
+ def _annotated(self, node: ast.AST, parents: Parents) -> bool:
69
+ """Whether this None sits inside a type annotation.
88
70
 
71
+ The question used to be asked downwards: walk the whole tree,
72
+ collect every annotation, look for the node inside it. That is the
73
+ tree once per None, and a file with hundreds of them pays for the
74
+ tree hundreds of times. Asked upwards, the answer costs the depth
75
+ of the nesting and nothing more.
76
+ """
77
+ held = node
78
+ above = parents.above(held)
79
+ while above is not ABSENT:
80
+ if FUNCTION.covers(above) and above.returns is held:
81
+ return True
82
+ if ARG.covers(above) and above.annotation is held:
83
+ return True
84
+ if ANN_ASSIGN.covers(above) and above.annotation is held:
85
+ return True
86
+ held, above = above, parents.above(above)
89
87
  return False
90
-
91
- def _node_in_subtree(self, target: ast.AST, tree: ast.AST) -> bool:
92
- """Check if target node is within the tree."""
93
- return any(child is target for child in ast.walk(tree))
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flake8-elegant-objects"
7
- version = "2.0.0"
7
+ version = "2.0.1"
8
8
  description = "Flake8 plugin enforcing Elegant Objects principles: no -er naming, no null, no getters/setters, immutable objects"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -194,3 +194,18 @@ def test_cents_are_counted(self):
194
194
  violations = self._check_code(code)
195
195
  assert len(violations) == 1
196
196
  assert "one assertion" in violations[0]
197
+
198
+ def test_call_on_a_call_terminates(self) -> None:
199
+ """A chain headed by a plain call must not loop forever.
200
+
201
+ `helper().clear()` walks down to a Name that is not an assertion.
202
+ The walk once had no way out of that case and hung the whole run,
203
+ so this test is here to keep the exit.
204
+ """
205
+ code = """
206
+ def test_cents_are_counted(self):
207
+ _loaded_tools().clear()
208
+ assert money.cents() == 100
209
+ """
210
+ violations = self._check_code(code)
211
+ assert len(violations) == 1
@@ -2,7 +2,7 @@
2
2
 
3
3
  import ast
4
4
 
5
- from flake8_elegant_objects.base import Source
5
+ from flake8_elegant_objects.base import Parents, Source
6
6
  from flake8_elegant_objects.no_null import NoNull
7
7
 
8
8
 
@@ -14,9 +14,14 @@ class TestNoNullPrinciple:
14
14
  tree = ast.parse(code)
15
15
  checker = NoNull()
16
16
  violations = []
17
+ held: dict[ast.AST, ast.AST] = {}
18
+ for node in ast.walk(tree):
19
+ for child in ast.iter_child_nodes(node):
20
+ held[child] = node
21
+ parents = Parents(held)
17
22
 
18
23
  def visit(node: ast.AST) -> None:
19
- source = Source(node, None, tree)
24
+ source = Source(node, None, tree, parents)
20
25
  violations.extend(checker.check(source))
21
26
  for child in ast.iter_child_nodes(node):
22
27
  visit(child)
@@ -108,7 +108,7 @@ wheels = [
108
108
 
109
109
  [[package]]
110
110
  name = "flake8-elegant-objects"
111
- version = "2.0.0"
111
+ version = "2.0.1"
112
112
  source = { editable = "." }
113
113
 
114
114
  [package.dev-dependencies]