codesorter 0.2.5__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.5 → codesorter-0.2.7}/CHANGES.rst +32 -0
  2. {codesorter-0.2.5 → codesorter-0.2.7}/PKG-INFO +32 -15
  3. {codesorter-0.2.5 → codesorter-0.2.7}/README.rst +31 -14
  4. {codesorter-0.2.5 → codesorter-0.2.7}/codesorter/const.py +1 -1
  5. {codesorter-0.2.5 → codesorter-0.2.7}/codesorter/sort_code.py +18 -1
  6. codesorter-0.2.7/docs/_static/custom.css +26 -0
  7. {codesorter-0.2.5 → codesorter-0.2.7}/docs/code_overview/sort_code.rst +4 -3
  8. {codesorter-0.2.5 → codesorter-0.2.7}/docs/conf.py +2 -0
  9. {codesorter-0.2.5 → codesorter-0.2.7}/docs/index.rst +6 -0
  10. codesorter-0.2.7/docs/usage/barriers.rst +87 -0
  11. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/augmented_assignment_input.py +4 -3
  12. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/augmented_assignment_output.py +4 -3
  13. codesorter-0.2.7/tests/test_files/barrier_input.py +8 -0
  14. codesorter-0.2.7/tests/test_files/barrier_output.py +8 -0
  15. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/comprehension_dependency_input.py +6 -5
  16. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/comprehension_dependency_output.py +2 -1
  17. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/comprehensive_input.py +56 -58
  18. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/comprehensive_output.py +6 -8
  19. codesorter-0.2.7/tests/test_files/enum_member_alias_input.py +12 -0
  20. codesorter-0.2.7/tests/test_files/enum_member_alias_output.py +12 -0
  21. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/name_rebinding_input.py +5 -0
  22. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/name_rebinding_output.py +5 -0
  23. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_sort_code.py +50 -0
  24. codesorter-0.2.5/tests/test_files/barrier_input.py +0 -11
  25. codesorter-0.2.5/tests/test_files/barrier_output.py +0 -11
  26. {codesorter-0.2.5 → codesorter-0.2.7}/.github/dependabot.yml +0 -0
  27. {codesorter-0.2.5 → codesorter-0.2.7}/.github/workflows/ci.yml +0 -0
  28. {codesorter-0.2.5 → codesorter-0.2.7}/.github/workflows/pre-commit_autoupdate.yml +0 -0
  29. {codesorter-0.2.5 → codesorter-0.2.7}/.github/workflows/prepare_release.yml +0 -0
  30. {codesorter-0.2.5 → codesorter-0.2.7}/.github/workflows/pypi.yml +0 -0
  31. {codesorter-0.2.5 → codesorter-0.2.7}/.github/workflows/scorecard.yml +0 -0
  32. {codesorter-0.2.5 → codesorter-0.2.7}/.github/workflows/stale_action.yml +0 -0
  33. {codesorter-0.2.5 → codesorter-0.2.7}/.github/workflows/tag_release.yml +0 -0
  34. {codesorter-0.2.5 → codesorter-0.2.7}/.gitignore +0 -0
  35. {codesorter-0.2.5 → codesorter-0.2.7}/.pre-commit-config.yaml +0 -0
  36. {codesorter-0.2.5 → codesorter-0.2.7}/.pre-commit-hooks.yaml +0 -0
  37. {codesorter-0.2.5 → codesorter-0.2.7}/.python-version +0 -0
  38. {codesorter-0.2.5 → codesorter-0.2.7}/.readthedocs.yaml +0 -0
  39. {codesorter-0.2.5 → codesorter-0.2.7}/LICENSE.txt +0 -0
  40. {codesorter-0.2.5 → codesorter-0.2.7}/codesorter/__init__.py +0 -0
  41. {codesorter-0.2.5 → codesorter-0.2.7}/codesorter/cli.py +0 -0
  42. {codesorter-0.2.5 → codesorter-0.2.7}/codesorter/py.typed +0 -0
  43. {codesorter-0.2.5 → codesorter-0.2.7}/docs/Makefile +0 -0
  44. {codesorter-0.2.5 → codesorter-0.2.7}/docs/contributing/pre_commit.rst +0 -0
  45. {codesorter-0.2.5 → codesorter-0.2.7}/docs/docutils.conf +0 -0
  46. {codesorter-0.2.5 → codesorter-0.2.7}/docs/genindex.rst +0 -0
  47. {codesorter-0.2.5 → codesorter-0.2.7}/docs/make.bat +0 -0
  48. {codesorter-0.2.5 → codesorter-0.2.7}/docs/package_info/change_log.rst +0 -0
  49. {codesorter-0.2.5 → codesorter-0.2.7}/examples/after_example.py +0 -0
  50. {codesorter-0.2.5 → codesorter-0.2.7}/examples/before_example.py +0 -0
  51. {codesorter-0.2.5 → codesorter-0.2.7}/pyproject.toml +0 -0
  52. {codesorter-0.2.5 → codesorter-0.2.7}/tests/__init__.py +0 -0
  53. {codesorter-0.2.5 → codesorter-0.2.7}/tests/conftest.py +0 -0
  54. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/alias_function_input.py +0 -0
  55. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/alias_function_output.py +0 -0
  56. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/basic_function_input.py +0 -0
  57. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/basic_function_output.py +0 -0
  58. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/class_global_dependency_input.py +0 -0
  59. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/class_global_dependency_output.py +0 -0
  60. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/class_inheritance_input.py +0 -0
  61. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/class_inheritance_output.py +0 -0
  62. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/class_method_input.py +0 -0
  63. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/class_method_output.py +0 -0
  64. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/classmethod_input.py +0 -0
  65. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/classmethod_output.py +0 -0
  66. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/constant_ordering_input.py +0 -0
  67. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/constant_ordering_output.py +0 -0
  68. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/custom_decorators_input.py +0 -0
  69. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/custom_decorators_output.py +0 -0
  70. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/keyword_arguments_input.py +0 -0
  71. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/keyword_arguments_output.py +0 -0
  72. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/lazy_annotation_cycle_input.py +7 -7
  73. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/lazy_annotation_cycle_output.py +0 -0
  74. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/mixed_decorators_input.py +0 -0
  75. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/mixed_decorators_output.py +0 -0
  76. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/order_sensitive_input.py +0 -0
  77. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/order_sensitive_output.py +0 -0
  78. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/property_input.py +0 -0
  79. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/property_output.py +0 -0
  80. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/pytest_fixtures_input.py +0 -0
  81. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/pytest_fixtures_output.py +0 -0
  82. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/staticmethod_input.py +0 -0
  83. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/staticmethod_output.py +0 -0
  84. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/underscore_ordering_input.py +0 -0
  85. {codesorter-0.2.5 → codesorter-0.2.7}/tests/test_files/underscore_ordering_output.py +0 -0
  86. {codesorter-0.2.5 → codesorter-0.2.7}/uv.lock +0 -0
@@ -4,6 +4,38 @@
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
+
22
+ ********************
23
+ 0.2.6 (2026/06/14)
24
+ ********************
25
+
26
+ **Added**
27
+
28
+ - Documentation page describing how barriers limit sorting to segments between
29
+ side-effecting statements, and how to relocate a statement that should not be a
30
+ barrier.
31
+
32
+ **Changed**
33
+
34
+ - Refresh the documentation: the installation instructions use uv-native commands, the
35
+ class-method ordering reference and example now match the implementation, and the
36
+ barrier relocation example is split into separate, color-tinted before and after
37
+ blocks.
38
+
7
39
  ********************
8
40
  0.2.5 (2026/06/14)
9
41
  ********************
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codesorter
3
- Version: 0.2.5
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/
@@ -60,6 +60,15 @@ Description-Content-Type: text/x-rst
60
60
 
61
61
  CodeSorter is a LibCST codemod that automatically sorts and organizes Python code.
62
62
 
63
+ .. warning::
64
+
65
+ Only apply CodeSorter to a code base you own or maintain. Reordering an entire file
66
+ is a sweeping, opinionated change that conflicts with in-flight work and erases the
67
+ history of carefully chosen ordering. Opening a pull request that runs CodeSorter
68
+ across **someone else's** project is strongly discouraged — it is noisy,
69
+ unsolicited, and burdensome to review. Adopt it as a pre-commit hook in your own
70
+ repositories instead, where every contributor benefits from the consistent ordering.
71
+
63
72
  **********
64
73
  Features
65
74
  **********
@@ -85,10 +94,10 @@ From PyPI:
85
94
 
86
95
  .. code-block:: bash
87
96
 
88
- # using uv and add to the lint dependency group
97
+ # install the codesorter CLI as a standalone tool
98
+ uv tool install codesorter
99
+ # or add it to a project's lint dependency group
89
100
  uv add --group lint codesorter
90
- # or using pip
91
- pip install codesorter
92
101
 
93
102
  From Source:
94
103
 
@@ -96,7 +105,7 @@ From Source:
96
105
 
97
106
  git clone https://github.com/praw-dev/CodeSorter.git
98
107
  cd CodeSorter
99
- uv pip install -e .
108
+ uv tool install .
100
109
 
101
110
  Development Installation:
102
111
 
@@ -137,7 +146,7 @@ commit:
137
146
  # .pre-commit-config.yaml
138
147
  repos:
139
148
  - repo: https://github.com/praw-dev/CodeSorter
140
- rev: v0.0.3
149
+ rev: v0.2.5
141
150
  hooks:
142
151
  - id: codesorter
143
152
 
@@ -147,7 +156,7 @@ Or use the check-only variant, which fails the hook without modifying files:
147
156
 
148
157
  repos:
149
158
  - repo: https://github.com/praw-dev/CodeSorter
150
- rev: v0.0.3
159
+ rev: v0.2.5
151
160
  hooks:
152
161
  - id: codesorter-check
153
162
 
@@ -195,10 +204,18 @@ Function Sorting
195
204
  Class Method Sorting
196
205
  ====================
197
206
 
198
- - Methods are sorted with special consideration for decorators: - ``@property`` methods
199
- (getters, setters, deleters) - ``@staticmethod`` methods - ``@classmethod`` methods -
200
- Regular instance methods
201
- - Methods within classes maintain their logical grouping
207
+ - Methods are grouped by kind, in this order:
208
+
209
+ - ``@abstractmethod`` methods
210
+ - pytest fixtures (``autouse`` fixtures first)
211
+ - ``@staticmethod`` methods
212
+ - ``@classmethod`` methods
213
+ - cached properties and ``@property`` methods (getter, then setter, then deleter)
214
+ - ``@contextmanager`` methods
215
+ - regular instance methods
216
+
217
+ - Within each group, methods are sorted alphabetically, with leading-underscore
218
+ (``_private`` and ``__dunder__``) names ahead of public ones
202
219
 
203
220
  Pytest Fixture Sorting
204
221
  ======================
@@ -231,14 +248,14 @@ Example Transformation
231
248
  .. code-block:: python
232
249
 
233
250
  class MyClass:
234
- @property
235
- def a_property(self):
236
- pass
237
-
238
251
  @staticmethod
239
252
  def b_static():
240
253
  pass
241
254
 
255
+ @property
256
+ def a_property(self):
257
+ pass
258
+
242
259
  def z_method(self):
243
260
  pass
244
261
 
@@ -4,6 +4,15 @@
4
4
 
5
5
  CodeSorter is a LibCST codemod that automatically sorts and organizes Python code.
6
6
 
7
+ .. warning::
8
+
9
+ Only apply CodeSorter to a code base you own or maintain. Reordering an entire file
10
+ is a sweeping, opinionated change that conflicts with in-flight work and erases the
11
+ history of carefully chosen ordering. Opening a pull request that runs CodeSorter
12
+ across **someone else's** project is strongly discouraged — it is noisy,
13
+ unsolicited, and burdensome to review. Adopt it as a pre-commit hook in your own
14
+ repositories instead, where every contributor benefits from the consistent ordering.
15
+
7
16
  **********
8
17
  Features
9
18
  **********
@@ -29,10 +38,10 @@ From PyPI:
29
38
 
30
39
  .. code-block:: bash
31
40
 
32
- # using uv and add to the lint dependency group
41
+ # install the codesorter CLI as a standalone tool
42
+ uv tool install codesorter
43
+ # or add it to a project's lint dependency group
33
44
  uv add --group lint codesorter
34
- # or using pip
35
- pip install codesorter
36
45
 
37
46
  From Source:
38
47
 
@@ -40,7 +49,7 @@ From Source:
40
49
 
41
50
  git clone https://github.com/praw-dev/CodeSorter.git
42
51
  cd CodeSorter
43
- uv pip install -e .
52
+ uv tool install .
44
53
 
45
54
  Development Installation:
46
55
 
@@ -81,7 +90,7 @@ commit:
81
90
  # .pre-commit-config.yaml
82
91
  repos:
83
92
  - repo: https://github.com/praw-dev/CodeSorter
84
- rev: v0.0.3
93
+ rev: v0.2.5
85
94
  hooks:
86
95
  - id: codesorter
87
96
 
@@ -91,7 +100,7 @@ Or use the check-only variant, which fails the hook without modifying files:
91
100
 
92
101
  repos:
93
102
  - repo: https://github.com/praw-dev/CodeSorter
94
- rev: v0.0.3
103
+ rev: v0.2.5
95
104
  hooks:
96
105
  - id: codesorter-check
97
106
 
@@ -139,10 +148,18 @@ Function Sorting
139
148
  Class Method Sorting
140
149
  ====================
141
150
 
142
- - Methods are sorted with special consideration for decorators: - ``@property`` methods
143
- (getters, setters, deleters) - ``@staticmethod`` methods - ``@classmethod`` methods -
144
- Regular instance methods
145
- - Methods within classes maintain their logical grouping
151
+ - Methods are grouped by kind, in this order:
152
+
153
+ - ``@abstractmethod`` methods
154
+ - pytest fixtures (``autouse`` fixtures first)
155
+ - ``@staticmethod`` methods
156
+ - ``@classmethod`` methods
157
+ - cached properties and ``@property`` methods (getter, then setter, then deleter)
158
+ - ``@contextmanager`` methods
159
+ - regular instance methods
160
+
161
+ - Within each group, methods are sorted alphabetically, with leading-underscore
162
+ (``_private`` and ``__dunder__``) names ahead of public ones
146
163
 
147
164
  Pytest Fixture Sorting
148
165
  ======================
@@ -175,14 +192,14 @@ Example Transformation
175
192
  .. code-block:: python
176
193
 
177
194
  class MyClass:
178
- @property
179
- def a_property(self):
180
- pass
181
-
182
195
  @staticmethod
183
196
  def b_static():
184
197
  pass
185
198
 
199
+ @property
200
+ def a_property(self):
201
+ pass
202
+
186
203
  def z_method(self):
187
204
  pass
188
205
 
@@ -39,4 +39,4 @@ PLAIN_DECORATOR_PARTS = 1
39
39
 
40
40
  PROPERTY_DECORATOR_PARTS = 2
41
41
 
42
- __version__ = "0.2.5"
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,
@@ -0,0 +1,26 @@
1
+ /* Tint the before/after code blocks on the barriers page.
2
+ Translucent fills layer over the page background, so they read correctly in
3
+ both the light and dark Furo themes. */
4
+ .codesorter-before,
5
+ .codesorter-after {
6
+ border-left: 4px solid;
7
+ border-radius: 0.2rem;
8
+ margin-bottom: 1rem;
9
+ padding-left: 0.4rem;
10
+ }
11
+
12
+ .codesorter-before {
13
+ background-color: rgba(229, 83, 75, 0.08);
14
+ border-left-color: #e5534b;
15
+ }
16
+
17
+ .codesorter-after {
18
+ background-color: rgba(63, 185, 80, 0.08);
19
+ border-left-color: #3fb950;
20
+ }
21
+
22
+ /* Let the container tint show through instead of Furo's code background. */
23
+ .codesorter-before div.highlight,
24
+ .codesorter-after div.highlight {
25
+ background: transparent;
26
+ }
@@ -2,9 +2,10 @@
2
2
  Sort Code
3
3
  ###########
4
4
 
5
- The :class:`.SortCodeCommand` is the libcst codemod that performs the reordering. It is
6
- applied by the CLI, but can also be used directly as a libcst codemod (for example via
7
- ``python -m libcst.tool codemod codesorter.sort_code.SortCodeCommand``).
5
+ The :class:`.SortCodeCommand` is the libcst codemod that performs the reordering. The
6
+ ``codesorter`` CLI applies it to the files you pass it; you can also use it directly as
7
+ a libcst codemod in your own tooling by constructing it with a ``CodemodContext`` and
8
+ calling ``transform_module`` (see the programmatic example in the README).
8
9
 
9
10
  .. autoclass:: codesorter.sort_code.SortCodeCommand
10
11
 
@@ -16,6 +16,8 @@ extensions = [
16
16
  "sphinx.ext.intersphinx",
17
17
  "sphinx_autodoc_typehints",
18
18
  ]
19
+ html_css_files = ["custom.css"]
20
+ html_static_path = ["_static"]
19
21
  html_theme = "furo"
20
22
  intersphinx_mapping = {
21
23
  "python": ("https://docs.python.org/3", None),
@@ -1,5 +1,11 @@
1
1
  .. include:: ../README.rst
2
2
 
3
+ .. toctree::
4
+ :hidden:
5
+ :caption: Usage
6
+
7
+ usage/barriers
8
+
3
9
  .. toctree::
4
10
  :hidden:
5
11
  :caption: Code Overview
@@ -0,0 +1,87 @@
1
+ ######################
2
+ Sorting and Barriers
3
+ ######################
4
+
5
+ CodeSorter reorders the classes, functions, and assignments within a module or class
6
+ body, but only within a *segment* of consecutive definitions. Any statement that is not
7
+ a sortable definition acts as a **barrier**, and no definition is ever moved across one.
8
+
9
+ Barriers include bare expression statements (a function call on its own line such as
10
+ ``sys.path.insert(0, str(ROOT))``), ``if`` / ``for`` / ``while`` / ``with`` blocks,
11
+ ``import`` statements, and similar. A barrier splits the surrounding definitions into
12
+ independent segments, each sorted on its own.
13
+
14
+ ********************
15
+ Why barriers exist
16
+ ********************
17
+
18
+ A statement sitting between definitions may depend on one of them, or be depended upon,
19
+ in ways that reordering would break. There are two cases, and only the first is visible
20
+ to a static tool:
21
+
22
+ 1. The statement *references* a definition that would otherwise move after it:
23
+
24
+ .. code-block:: python
25
+
26
+ ROOT = Path(__file__).resolve().parent
27
+ sys.path.insert(0, str(ROOT)) # uses ROOT
28
+
29
+ If ``ROOT`` were sorted after the ``sys.path.insert`` call, the module would raise
30
+ ``NameError`` on import.
31
+
32
+ 2. A later definition depends on the statement's *side effect*, which no name reference
33
+ reveals:
34
+
35
+ .. code-block:: python
36
+
37
+ configure_settings()
38
+ DEFAULT_TIMEOUT = settings.get("timeout") # reads what configure_settings() set up
39
+
40
+ ``DEFAULT_TIMEOUT`` does not mention ``configure_settings`` by name, yet it must run
41
+ after it. Moving the constant ahead of the call would silently read an unconfigured
42
+ value — a wrong result rather than a crash.
43
+
44
+ Because side effects are opaque, CodeSorter cannot prove that crossing an arbitrary
45
+ statement is safe, so it treats every non-definition statement as a barrier.
46
+
47
+ **********************
48
+ Relocating a barrier
49
+ **********************
50
+
51
+ The cost of this rule is small: a barrier that genuinely sits in the middle of a block
52
+ of definitions keeps the definitions on either side from sorting together. If a
53
+ statement legitimately should *not* separate your definitions — you know it has no
54
+ ordering relationship with them — move it yourself so it no longer sits between them.
55
+ For example, hoist a one-off call to the top of the module (just below the imports) or
56
+ to the bottom.
57
+
58
+ **Before** — the call sits between the classes, splitting them into two segments, so
59
+ ``Alpha`` and ``Beta`` cannot sort together:
60
+
61
+ .. code-block:: python
62
+ :class: codesorter-before
63
+
64
+ class Beta: ...
65
+
66
+
67
+ warnings.filterwarnings("ignore")
68
+
69
+
70
+ class Alpha: ...
71
+
72
+ **After** — relocate the call above both classes; ``Alpha`` and ``Beta`` now form a
73
+ single segment and sort into order:
74
+
75
+ .. code-block:: python
76
+ :class: codesorter-after
77
+
78
+ warnings.filterwarnings("ignore")
79
+
80
+
81
+ class Alpha: ...
82
+
83
+
84
+ class Beta: ...
85
+
86
+ CodeSorter will not perform this move for you, precisely because it cannot prove the
87
+ move preserves behavior — only you know whether the statement is safe to relocate.
@@ -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
@@ -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