codesorter 0.2.6__tar.gz → 0.2.7__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 (86) hide show
  1. {codesorter-0.2.6 → codesorter-0.2.7}/CHANGES.rst +15 -0
  2. {codesorter-0.2.6 → codesorter-0.2.7}/PKG-INFO +1 -1
  3. {codesorter-0.2.6 → codesorter-0.2.7}/codesorter/const.py +1 -1
  4. {codesorter-0.2.6 → codesorter-0.2.7}/codesorter/sort_code.py +18 -1
  5. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/augmented_assignment_input.py +4 -3
  6. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/augmented_assignment_output.py +4 -3
  7. codesorter-0.2.7/tests/test_files/barrier_input.py +8 -0
  8. codesorter-0.2.7/tests/test_files/barrier_output.py +8 -0
  9. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/comprehension_dependency_input.py +6 -5
  10. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/comprehension_dependency_output.py +2 -1
  11. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/comprehensive_input.py +56 -58
  12. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/comprehensive_output.py +6 -8
  13. codesorter-0.2.7/tests/test_files/enum_member_alias_input.py +12 -0
  14. codesorter-0.2.7/tests/test_files/enum_member_alias_output.py +12 -0
  15. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/name_rebinding_input.py +5 -0
  16. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/name_rebinding_output.py +5 -0
  17. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_sort_code.py +50 -0
  18. codesorter-0.2.6/tests/test_files/barrier_input.py +0 -11
  19. codesorter-0.2.6/tests/test_files/barrier_output.py +0 -11
  20. {codesorter-0.2.6 → codesorter-0.2.7}/.github/dependabot.yml +0 -0
  21. {codesorter-0.2.6 → codesorter-0.2.7}/.github/workflows/ci.yml +0 -0
  22. {codesorter-0.2.6 → codesorter-0.2.7}/.github/workflows/pre-commit_autoupdate.yml +0 -0
  23. {codesorter-0.2.6 → codesorter-0.2.7}/.github/workflows/prepare_release.yml +0 -0
  24. {codesorter-0.2.6 → codesorter-0.2.7}/.github/workflows/pypi.yml +0 -0
  25. {codesorter-0.2.6 → codesorter-0.2.7}/.github/workflows/scorecard.yml +0 -0
  26. {codesorter-0.2.6 → codesorter-0.2.7}/.github/workflows/stale_action.yml +0 -0
  27. {codesorter-0.2.6 → codesorter-0.2.7}/.github/workflows/tag_release.yml +0 -0
  28. {codesorter-0.2.6 → codesorter-0.2.7}/.gitignore +0 -0
  29. {codesorter-0.2.6 → codesorter-0.2.7}/.pre-commit-config.yaml +0 -0
  30. {codesorter-0.2.6 → codesorter-0.2.7}/.pre-commit-hooks.yaml +0 -0
  31. {codesorter-0.2.6 → codesorter-0.2.7}/.python-version +0 -0
  32. {codesorter-0.2.6 → codesorter-0.2.7}/.readthedocs.yaml +0 -0
  33. {codesorter-0.2.6 → codesorter-0.2.7}/LICENSE.txt +0 -0
  34. {codesorter-0.2.6 → codesorter-0.2.7}/README.rst +0 -0
  35. {codesorter-0.2.6 → codesorter-0.2.7}/codesorter/__init__.py +0 -0
  36. {codesorter-0.2.6 → codesorter-0.2.7}/codesorter/cli.py +0 -0
  37. {codesorter-0.2.6 → codesorter-0.2.7}/codesorter/py.typed +0 -0
  38. {codesorter-0.2.6 → codesorter-0.2.7}/docs/Makefile +0 -0
  39. {codesorter-0.2.6 → codesorter-0.2.7}/docs/_static/custom.css +0 -0
  40. {codesorter-0.2.6 → codesorter-0.2.7}/docs/code_overview/sort_code.rst +0 -0
  41. {codesorter-0.2.6 → codesorter-0.2.7}/docs/conf.py +0 -0
  42. {codesorter-0.2.6 → codesorter-0.2.7}/docs/contributing/pre_commit.rst +0 -0
  43. {codesorter-0.2.6 → codesorter-0.2.7}/docs/docutils.conf +0 -0
  44. {codesorter-0.2.6 → codesorter-0.2.7}/docs/genindex.rst +0 -0
  45. {codesorter-0.2.6 → codesorter-0.2.7}/docs/index.rst +0 -0
  46. {codesorter-0.2.6 → codesorter-0.2.7}/docs/make.bat +0 -0
  47. {codesorter-0.2.6 → codesorter-0.2.7}/docs/package_info/change_log.rst +0 -0
  48. {codesorter-0.2.6 → codesorter-0.2.7}/docs/usage/barriers.rst +0 -0
  49. {codesorter-0.2.6 → codesorter-0.2.7}/examples/after_example.py +0 -0
  50. {codesorter-0.2.6 → codesorter-0.2.7}/examples/before_example.py +0 -0
  51. {codesorter-0.2.6 → codesorter-0.2.7}/pyproject.toml +0 -0
  52. {codesorter-0.2.6 → codesorter-0.2.7}/tests/__init__.py +0 -0
  53. {codesorter-0.2.6 → codesorter-0.2.7}/tests/conftest.py +0 -0
  54. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/alias_function_input.py +0 -0
  55. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/alias_function_output.py +0 -0
  56. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/basic_function_input.py +0 -0
  57. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/basic_function_output.py +0 -0
  58. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/class_global_dependency_input.py +0 -0
  59. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/class_global_dependency_output.py +0 -0
  60. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/class_inheritance_input.py +0 -0
  61. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/class_inheritance_output.py +0 -0
  62. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/class_method_input.py +0 -0
  63. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/class_method_output.py +0 -0
  64. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/classmethod_input.py +0 -0
  65. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/classmethod_output.py +0 -0
  66. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/constant_ordering_input.py +0 -0
  67. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/constant_ordering_output.py +0 -0
  68. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/custom_decorators_input.py +0 -0
  69. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/custom_decorators_output.py +0 -0
  70. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/keyword_arguments_input.py +0 -0
  71. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/keyword_arguments_output.py +0 -0
  72. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/lazy_annotation_cycle_input.py +7 -7
  73. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/lazy_annotation_cycle_output.py +0 -0
  74. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/mixed_decorators_input.py +0 -0
  75. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/mixed_decorators_output.py +0 -0
  76. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/order_sensitive_input.py +0 -0
  77. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/order_sensitive_output.py +0 -0
  78. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/property_input.py +0 -0
  79. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/property_output.py +0 -0
  80. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/pytest_fixtures_input.py +0 -0
  81. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/pytest_fixtures_output.py +0 -0
  82. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/staticmethod_input.py +0 -0
  83. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/staticmethod_output.py +0 -0
  84. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/underscore_ordering_input.py +0 -0
  85. {codesorter-0.2.6 → codesorter-0.2.7}/tests/test_files/underscore_ordering_output.py +0 -0
  86. {codesorter-0.2.6 → codesorter-0.2.7}/uv.lock +0 -0
@@ -4,6 +4,21 @@
4
4
 
5
5
  codesorter follows `semantic versioning <https://semver.org/>`_.
6
6
 
7
+ ********************
8
+ 0.2.7 (2026/06/15)
9
+ ********************
10
+
11
+ **Fixed**
12
+
13
+ - Do not treat a class's own attribute as a dependency on a same-named outer definition.
14
+ An enum member or class variable named like the module constant that aliases it (for
15
+ example ``CACHE_MISS = _Sentinel.CACHE_MISS`` beside ``class _Sentinel(Enum):
16
+ CACHE_MISS = auto()``) previously forged a false ``class`` -> ``constant`` edge that
17
+ closed a cycle with the real ``constant`` -> ``class`` edge, hoisting the constant
18
+ above the class it references and raising ``NameError`` at import. A name bound in a
19
+ class's own body is now recognized as belonging to that class's namespace and imposes
20
+ no ordering on outer definitions.
21
+
7
22
  ********************
8
23
  0.2.6 (2026/06/14)
9
24
  ********************
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codesorter
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: A Python codemod that sorts and organizes code in your files.
5
5
  Project-URL: Change Log, https://codesorter.readthedocs.io/en/latest/package_info/change_log.html
6
6
  Project-URL: Documentation, https://codesorter.readthedocs.io/
@@ -39,4 +39,4 @@ PLAIN_DECORATOR_PARTS = 1
39
39
 
40
40
  PROPERTY_DECORATOR_PARTS = 2
41
41
 
42
- __version__ = "0.2.6"
42
+ __version__ = "0.2.7"
@@ -336,7 +336,24 @@ class SortCodeCommand(VisitorBasedCodemodCommand, m.MatcherDecoratableTransforme
336
336
  # an annotation forging a false cycle with a real value-level edge.
337
337
  if id(found["name"]) in self._lazy_annotation_names:
338
338
  continue
339
- found_name = cst.ensure_type(found["name"], cst.Name).value
339
+ name_node = cst.ensure_type(found["name"], cst.Name)
340
+ found_name = name_node.value
341
+ # A name bound at this class's own body level (an enum member or
342
+ # class variable) belongs to the class's namespace, so it must not
343
+ # forge a dependency on a same-named outer definition. Otherwise an
344
+ # enum member named like the module constant that aliases it (for
345
+ # example ``CACHE_MISS = _Sentinel.CACHE_MISS``) creates a false cycle
346
+ # that hoists the constant above the class it depends on. The scope
347
+ # must be ``node``'s own class scope; a name bound in the *enclosing*
348
+ # class (a sibling method an alias assignment references) is a real
349
+ # dependency and is kept.
350
+ name_scope = self.get_metadata(md.ScopeProvider, name_node, None)
351
+ if (
352
+ isinstance(name_scope, md.ClassScope)
353
+ and name_scope.node == node
354
+ and name_scope.assignments[found_name]
355
+ ):
356
+ continue
340
357
  is_import = isinstance(
341
358
  next(iter(meta.assignments[found_name])),
342
359
  md.ImportAssignment,
@@ -1,12 +1,13 @@
1
1
  """Augmented assignments stay anchored to the constant they augment."""
2
2
 
3
- from mod import extra, more
3
+ import enum
4
+ import json
4
5
 
5
6
  ZEBRA = 1
6
7
  __version__ = "1.0"
7
8
  __all__ = ["Widget"]
8
- __all__ += extra.__all__
9
- __all__ += more.__all__
9
+ __all__ += json.__all__
10
+ __all__ += enum.__all__
10
11
  APPLE = 2
11
12
 
12
13
 
@@ -1,12 +1,13 @@
1
1
  """Augmented assignments stay anchored to the constant they augment."""
2
2
 
3
- from mod import extra, more
3
+ import enum
4
+ import json
4
5
 
5
6
  APPLE = 2
6
7
  ZEBRA = 1
7
8
  __all__ = ["Widget"]
8
- __all__ += extra.__all__
9
- __all__ += more.__all__
9
+ __all__ += json.__all__
10
+ __all__ += enum.__all__
10
11
  __version__ = "1.0"
11
12
 
12
13
 
@@ -0,0 +1,8 @@
1
+ """Definitions never cross a side-effecting statement that may depend on them."""
2
+
3
+ ZEBRA = 1
4
+ MIDDLE = 5
5
+ repr(MIDDLE) # a barrier: a bare statement splits the surrounding definitions
6
+
7
+ BANANA = 3
8
+ APPLE = 2
@@ -0,0 +1,8 @@
1
+ """Definitions never cross a side-effecting statement that may depend on them."""
2
+
3
+ MIDDLE = 5
4
+ ZEBRA = 1
5
+ repr(MIDDLE) # a barrier: a bare statement splits the surrounding definitions
6
+
7
+ APPLE = 2
8
+ BANANA = 3
@@ -1,13 +1,14 @@
1
1
  """A module-level comprehension references a function eagerly, so it depends on it."""
2
2
 
3
- placeholders = {name: build(name) for name in ["a", "b"]}
3
+
4
+ def lazy():
5
+ """A comprehension here is deferred, so it imposes no ordering."""
6
+ return [build(name) for name in ["c", "d"]]
4
7
 
5
8
 
6
9
  def build(name):
7
- """Used eagerly by the module-level comprehension above."""
10
+ """Used eagerly by the module-level comprehension below."""
8
11
  return f"value-{name}"
9
12
 
10
13
 
11
- def lazy():
12
- """A comprehension here is deferred, so it imposes no ordering."""
13
- return [build(name) for name in ["c", "d"]]
14
+ placeholders = {name: build(name) for name in ["a", "b"]}
@@ -1,7 +1,8 @@
1
1
  """A module-level comprehension references a function eagerly, so it depends on it."""
2
2
 
3
+
3
4
  def build(name):
4
- """Used eagerly by the module-level comprehension above."""
5
+ """Used eagerly by the module-level comprehension below."""
5
6
  return f"value-{name}"
6
7
 
7
8
 
@@ -1,3 +1,5 @@
1
+ """A comprehensive mix of constants, decorators, inheritance, and pytest fixtures."""
2
+
1
3
  import os
2
4
  from abc import ABC, abstractmethod
3
5
  from functools import wraps
@@ -10,7 +12,34 @@ API_KEY = "test-key-123"
10
12
  DEBUG_MODE = os.getenv("DEBUG", "false").lower() == "true"
11
13
 
12
14
 
13
- # Functions with decorators
15
+ # Custom decorators (defined before the functions that use them)
16
+ def cache_result(func):
17
+ """Cache the result of a function."""
18
+ cache = {}
19
+
20
+ @wraps(func)
21
+ def wrapper(*args, **kwargs):
22
+ key = str(args) + str(kwargs)
23
+ if key not in cache:
24
+ cache[key] = func(*args, **kwargs)
25
+ return cache[key]
26
+
27
+ return wrapper
28
+
29
+
30
+ def validate_input(func):
31
+ """Validate input parameters."""
32
+
33
+ @wraps(func)
34
+ def wrapper(*args, **kwargs):
35
+ for arg in args:
36
+ if arg is None:
37
+ raise ValueError("Input cannot be None")
38
+ return func(*args, **kwargs)
39
+
40
+ return wrapper
41
+
42
+
14
43
  @cache_result
15
44
  def expensive_calculation(n):
16
45
  """Perform an expensive calculation."""
@@ -23,24 +52,21 @@ def process_data(data):
23
52
  return [item.upper() for item in data]
24
53
 
25
54
 
26
- class Dog(Mammal):
27
- """Dog class."""
55
+ # Base classes with inheritance (each base defined before its subclass)
56
+ class Animal(ABC):
57
+ """Base class for all animals."""
28
58
 
29
- def __init__(self, name, age, fur_color, breed):
30
- super().__init__(name, age, fur_color)
31
- self.breed = breed
59
+ def __init__(self, name, age):
60
+ self.name = name
61
+ self.age = age
32
62
 
63
+ @abstractmethod
33
64
  def make_sound(self):
34
- """Make a dog sound."""
35
- return "Woof!"
36
-
37
- def get_breed_info(self):
38
- """Get breed information."""
39
- return f"Breed: {self.breed}"
65
+ """Make a sound."""
40
66
 
41
- def fetch(self):
42
- """Fetch behavior."""
43
- return f"{self.name} is fetching"
67
+ def get_info(self):
68
+ """Get animal information."""
69
+ return f"{self.name} is {self.age} years old"
44
70
 
45
71
 
46
72
  class Mammal(Animal):
@@ -59,29 +85,31 @@ class Mammal(Animal):
59
85
  return f"Fur color: {self.fur_color}"
60
86
 
61
87
 
62
- # Base classes with inheritance
63
- class Animal(ABC):
64
- """Base class for all animals."""
88
+ class Dog(Mammal):
89
+ """Dog class."""
65
90
 
66
- def __init__(self, name, age):
67
- self.name = name
68
- self.age = age
91
+ def __init__(self, name, age, fur_color, breed):
92
+ super().__init__(name, age, fur_color)
93
+ self.breed = breed
69
94
 
70
- @abstractmethod
71
95
  def make_sound(self):
72
- """Make a sound."""
96
+ """Make a dog sound."""
97
+ return "Woof!"
73
98
 
74
- def get_info(self):
75
- """Get animal information."""
76
- return f"{self.name} is {self.age} years old"
99
+ def get_breed_info(self):
100
+ """Get breed information."""
101
+ return f"Breed: {self.breed}"
102
+
103
+ def fetch(self):
104
+ """Fetch behavior."""
105
+ return f"{self.name} is fetching"
77
106
 
78
107
 
79
- # Classes with global dependencies
80
108
  class DatabaseManager:
81
109
  """Manages database connections using global config."""
82
110
 
83
111
  def __init__(self):
84
- self.host = "localhost" # Would normally use DATABASE_URL
112
+ self.host = "localhost"
85
113
  self.port = 5432
86
114
  self.database = "test_db"
87
115
 
@@ -109,40 +137,11 @@ class APIClient:
109
137
  return DEBUG_MODE
110
138
 
111
139
 
112
- def validate_input(func):
113
- """Validate input parameters."""
114
-
115
- @wraps(func)
116
- def wrapper(*args, **kwargs):
117
- for arg in args:
118
- if arg is None:
119
- raise ValueError("Input cannot be None")
120
- return func(*args, **kwargs)
121
-
122
- return wrapper
123
-
124
-
125
140
  def regular_function():
126
141
  """A regular function without decorators."""
127
142
  return "regular"
128
143
 
129
144
 
130
- # Custom decorators
131
- def cache_result(func):
132
- """Cache the result of a function."""
133
- cache = {}
134
-
135
- @wraps(func)
136
- def wrapper(*args, **kwargs):
137
- key = str(args) + str(kwargs)
138
- if key not in cache:
139
- cache[key] = func(*args, **kwargs)
140
- return cache[key]
141
-
142
- return wrapper
143
-
144
-
145
- # Test functions
146
145
  def test_something(database_connection, sample_data):
147
146
  """Test function using fixtures."""
148
147
  assert database_connection["connected"]
@@ -171,7 +170,6 @@ def sample_data():
171
170
  return ["item1", "item2", "item3"]
172
171
 
173
172
 
174
- # Pytest fixtures
175
173
  @pytest.fixture(scope="session")
176
174
  def database_connection():
177
175
  """Provide a database connection for testing."""
@@ -1,10 +1,12 @@
1
+ """A comprehensive mix of constants, decorators, inheritance, and pytest fixtures."""
2
+
1
3
  import os
2
4
  from abc import ABC, abstractmethod
3
5
  from functools import wraps
4
6
 
5
7
  import pytest
6
- API_KEY = "test-key-123"
7
8
 
9
+ API_KEY = "test-key-123"
8
10
  # Global variables
9
11
  DATABASE_URL = "sqlite:///test.db"
10
12
  DEBUG_MODE = os.getenv("DEBUG", "false").lower() == "true"
@@ -25,7 +27,7 @@ class APIClient:
25
27
  return f"GET {self.base_url}{endpoint}"
26
28
 
27
29
 
28
- # Base classes with inheritance
30
+ # Base classes with inheritance (each base defined before its subclass)
29
31
  class Animal(ABC):
30
32
  """Base class for all animals."""
31
33
 
@@ -42,12 +44,11 @@ class Animal(ABC):
42
44
  return f"{self.name} is {self.age} years old"
43
45
 
44
46
 
45
- # Classes with global dependencies
46
47
  class DatabaseManager:
47
48
  """Manages database connections using global config."""
48
49
 
49
50
  def __init__(self):
50
- self.host = "localhost" # Would normally use DATABASE_URL
51
+ self.host = "localhost"
51
52
  self.port = 5432
52
53
  self.database = "test_db"
53
54
 
@@ -105,7 +106,6 @@ def setup_test_environment():
105
106
  API_KEY = "test-key-123"
106
107
 
107
108
 
108
- # Pytest fixtures
109
109
  @pytest.fixture(scope="session")
110
110
  def database_connection():
111
111
  """Provide a database connection for testing."""
@@ -118,7 +118,7 @@ def sample_data():
118
118
  return ["item1", "item2", "item3"]
119
119
 
120
120
 
121
- # Custom decorators
121
+ # Custom decorators (defined before the functions that use them)
122
122
  def cache_result(func):
123
123
  """Cache the result of a function."""
124
124
  cache = {}
@@ -133,7 +133,6 @@ def cache_result(func):
133
133
  return wrapper
134
134
 
135
135
 
136
- # Functions with decorators
137
136
  @cache_result
138
137
  def expensive_calculation(n):
139
138
  """Perform an expensive calculation."""
@@ -161,7 +160,6 @@ def test_global_dependencies():
161
160
  assert api_client.is_debug_mode() == DEBUG_MODE
162
161
 
163
162
 
164
- # Test functions
165
163
  def test_something(database_connection, sample_data):
166
164
  """Test function using fixtures."""
167
165
  assert database_connection["connected"]
@@ -0,0 +1,12 @@
1
+ from enum import Enum, auto
2
+
3
+
4
+ def helper():
5
+ return CACHE_MISS
6
+
7
+
8
+ class _Sentinel(Enum):
9
+ CACHE_MISS = auto()
10
+
11
+
12
+ CACHE_MISS = _Sentinel.CACHE_MISS
@@ -0,0 +1,12 @@
1
+ from enum import Enum, auto
2
+
3
+
4
+ class _Sentinel(Enum):
5
+ CACHE_MISS = auto()
6
+
7
+
8
+ CACHE_MISS = _Sentinel.CACHE_MISS
9
+
10
+
11
+ def helper():
12
+ return CACHE_MISS
@@ -1,6 +1,11 @@
1
1
  """An assignment that rebinds a method name stays after the method it wraps."""
2
2
 
3
3
 
4
+ def cachedproperty(method, *, doc=None):
5
+ """Minimal stand-in so the rebinding has something to call."""
6
+ return property(method)
7
+
8
+
4
9
  class Klass:
5
10
  def ten(self):
6
11
  return 10
@@ -1,6 +1,11 @@
1
1
  """An assignment that rebinds a method name stays after the method it wraps."""
2
2
 
3
3
 
4
+ def cachedproperty(method, *, doc=None):
5
+ """Minimal stand-in so the rebinding has something to call."""
6
+ return property(method)
7
+
8
+
4
9
  class Klass:
5
10
  def alpha(self):
6
11
  """A method that sorts first alphabetically."""
@@ -1,5 +1,9 @@
1
1
  """Tests for the SortCodeCommand codemod."""
2
2
 
3
+ import io
4
+ from contextlib import redirect_stderr, redirect_stdout
5
+ from pathlib import Path
6
+
3
7
  import libcst as cst
4
8
  import pytest
5
9
  from libcst.codemod import CodemodContext
@@ -183,6 +187,52 @@ class MyClass:
183
187
  result = command.transform_module(cst.parse_module(code))
184
188
  assert result.code.strip() == ""
185
189
 
190
+ def test_enum_member_alias(self, test_files):
191
+ """Test that a class attribute sharing a name with an outer constant is not a dep.
192
+
193
+ An enum member (or class variable) named like the module constant that aliases
194
+ it (``CACHE_MISS = _Sentinel.CACHE_MISS``) is bound in the class's own
195
+ namespace, so it must not forge a dependency from the class back onto the
196
+ constant. Without that guard the false ``_Sentinel`` -> ``CACHE_MISS`` edge
197
+ closes a cycle with the real ``CACHE_MISS`` -> ``_Sentinel`` edge, and
198
+ cycle-breaking hoists the constant above the class it references (raising
199
+ NameError at import).
200
+
201
+ """
202
+ input_code, expected_code = test_files
203
+ context = CodemodContext()
204
+ command = SortCodeCommand(context)
205
+ result = command.transform_module(cst.parse_module(input_code))
206
+
207
+ assert expected_code == result.code
208
+
209
+ def test_fixture_files_execute(self):
210
+ """Every input/output fixture must import cleanly with no side effects.
211
+
212
+ Executing each file in a fresh namespace is a stronger guard than parsing: it
213
+ catches a fixture that parses but does not run (an invalid input, or a codemod
214
+ output that raises ``NameError`` because a definition was hoisted above one it
215
+ depends on). Each file is also required to be self-contained and side-effect
216
+ free, so anything written to stdout/stderr fails the test.
217
+
218
+ """
219
+ test_files_dir = Path(__file__).parent / "test_files"
220
+ fixtures = sorted(test_files_dir.glob("*.py"))
221
+ assert fixtures, "no fixture files found"
222
+ failures = []
223
+ for path in fixtures:
224
+ captured = io.StringIO()
225
+ try:
226
+ code = compile(path.read_text(encoding="utf-8"), str(path), "exec")
227
+ with redirect_stdout(captured), redirect_stderr(captured):
228
+ exec(code, {"__name__": "__fixture__"}) # noqa: S102
229
+ except Exception as error: # noqa: BLE001
230
+ failures.append(f"{path.name}: {type(error).__name__}: {error}")
231
+ continue
232
+ if captured.getvalue():
233
+ failures.append(f"{path.name}: wrote to stdout/stderr {captured.getvalue()!r}")
234
+ assert not failures, "fixtures that do not execute cleanly:\n" + "\n".join(failures)
235
+
186
236
  def test_keyword_arguments(self, test_files):
187
237
  """Test that keyword arguments, keyword-only params, and dict keys are sorted."""
188
238
  input_code, expected_code = test_files
@@ -1,11 +0,0 @@
1
- """Definitions never cross a side-effecting statement that may depend on them."""
2
-
3
- import sys
4
- from pathlib import Path
5
-
6
- ZEBRA = 1
7
- REPO_ROOT = Path(__file__).resolve().parent
8
- sys.path.insert(0, str(REPO_ROOT))
9
-
10
- BANANA = 3
11
- APPLE = 2
@@ -1,11 +0,0 @@
1
- """Definitions never cross a side-effecting statement that may depend on them."""
2
-
3
- import sys
4
- from pathlib import Path
5
-
6
- REPO_ROOT = Path(__file__).resolve().parent
7
- ZEBRA = 1
8
- sys.path.insert(0, str(REPO_ROOT))
9
-
10
- APPLE = 2
11
- BANANA = 3
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -3,12 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
 
6
- class Apple(_Base):
7
- """References the Instr alias only in a lazy annotation."""
8
-
9
- nxt: list[Instr]
10
-
11
-
12
6
  class _Base:
13
7
  """A shared base class."""
14
8
 
@@ -17,10 +11,16 @@ class Zebra(_Base):
17
11
  """Another member of the Instr union."""
18
12
 
19
13
 
20
- Instr = Apple | Zebra
14
+ class Apple(_Base):
15
+ """References the Instr alias only in a lazy annotation."""
16
+
17
+ nxt: list[Instr]
21
18
 
22
19
 
23
20
  class User:
24
21
  """Uses the Instr alias in a lazy annotation."""
25
22
 
26
23
  items: list[Instr]
24
+
25
+
26
+ Instr = Apple | Zebra
File without changes