codesorter 0.2.2__tar.gz → 0.2.4__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 (81) hide show
  1. {codesorter-0.2.2 → codesorter-0.2.4}/CHANGES.rst +24 -0
  2. {codesorter-0.2.2 → codesorter-0.2.4}/PKG-INFO +1 -1
  3. {codesorter-0.2.2 → codesorter-0.2.4}/codesorter/const.py +1 -1
  4. {codesorter-0.2.2 → codesorter-0.2.4}/codesorter/sort_code.py +71 -30
  5. codesorter-0.2.4/tests/test_files/barrier_input.py +11 -0
  6. codesorter-0.2.4/tests/test_files/barrier_output.py +11 -0
  7. codesorter-0.2.4/tests/test_files/name_rebinding_input.py +16 -0
  8. codesorter-0.2.4/tests/test_files/name_rebinding_output.py +16 -0
  9. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_sort_code.py +33 -0
  10. {codesorter-0.2.2 → codesorter-0.2.4}/.github/dependabot.yml +0 -0
  11. {codesorter-0.2.2 → codesorter-0.2.4}/.github/workflows/ci.yml +0 -0
  12. {codesorter-0.2.2 → codesorter-0.2.4}/.github/workflows/pre-commit_autoupdate.yml +0 -0
  13. {codesorter-0.2.2 → codesorter-0.2.4}/.github/workflows/prepare_release.yml +0 -0
  14. {codesorter-0.2.2 → codesorter-0.2.4}/.github/workflows/pypi.yml +0 -0
  15. {codesorter-0.2.2 → codesorter-0.2.4}/.github/workflows/scorecard.yml +0 -0
  16. {codesorter-0.2.2 → codesorter-0.2.4}/.github/workflows/stale_action.yml +0 -0
  17. {codesorter-0.2.2 → codesorter-0.2.4}/.github/workflows/tag_release.yml +0 -0
  18. {codesorter-0.2.2 → codesorter-0.2.4}/.gitignore +0 -0
  19. {codesorter-0.2.2 → codesorter-0.2.4}/.libcst.codemod.yaml +0 -0
  20. {codesorter-0.2.2 → codesorter-0.2.4}/.pre-commit-config.yaml +0 -0
  21. {codesorter-0.2.2 → codesorter-0.2.4}/.pre-commit-hooks.yaml +0 -0
  22. {codesorter-0.2.2 → codesorter-0.2.4}/.python-version +0 -0
  23. {codesorter-0.2.2 → codesorter-0.2.4}/.readthedocs.yaml +0 -0
  24. {codesorter-0.2.2 → codesorter-0.2.4}/LICENSE.txt +0 -0
  25. {codesorter-0.2.2 → codesorter-0.2.4}/README.rst +0 -0
  26. {codesorter-0.2.2 → codesorter-0.2.4}/codesorter/__init__.py +0 -0
  27. {codesorter-0.2.2 → codesorter-0.2.4}/codesorter/cli.py +0 -0
  28. {codesorter-0.2.2 → codesorter-0.2.4}/codesorter/py.typed +0 -0
  29. {codesorter-0.2.2 → codesorter-0.2.4}/docs/Makefile +0 -0
  30. {codesorter-0.2.2 → codesorter-0.2.4}/docs/code_overview/sort_code.rst +0 -0
  31. {codesorter-0.2.2 → codesorter-0.2.4}/docs/conf.py +0 -0
  32. {codesorter-0.2.2 → codesorter-0.2.4}/docs/contributing/pre_commit.rst +0 -0
  33. {codesorter-0.2.2 → codesorter-0.2.4}/docs/docutils.conf +0 -0
  34. {codesorter-0.2.2 → codesorter-0.2.4}/docs/genindex.rst +0 -0
  35. {codesorter-0.2.2 → codesorter-0.2.4}/docs/index.rst +0 -0
  36. {codesorter-0.2.2 → codesorter-0.2.4}/docs/make.bat +0 -0
  37. {codesorter-0.2.2 → codesorter-0.2.4}/docs/package_info/change_log.rst +0 -0
  38. {codesorter-0.2.2 → codesorter-0.2.4}/examples/after_example.py +0 -0
  39. {codesorter-0.2.2 → codesorter-0.2.4}/examples/before_example.py +0 -0
  40. {codesorter-0.2.2 → codesorter-0.2.4}/pyproject.toml +0 -0
  41. {codesorter-0.2.2 → codesorter-0.2.4}/tests/__init__.py +0 -0
  42. {codesorter-0.2.2 → codesorter-0.2.4}/tests/conftest.py +0 -0
  43. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/alias_function_input.py +0 -0
  44. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/alias_function_output.py +0 -0
  45. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/augmented_assignment_input.py +0 -0
  46. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/augmented_assignment_output.py +0 -0
  47. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/basic_function_input.py +0 -0
  48. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/basic_function_output.py +0 -0
  49. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/class_global_dependency_input.py +0 -0
  50. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/class_global_dependency_output.py +0 -0
  51. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/class_inheritance_input.py +0 -0
  52. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/class_inheritance_output.py +0 -0
  53. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/class_method_input.py +0 -0
  54. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/class_method_output.py +0 -0
  55. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/classmethod_input.py +0 -0
  56. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/classmethod_output.py +0 -0
  57. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/comprehension_dependency_input.py +0 -0
  58. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/comprehension_dependency_output.py +0 -0
  59. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/comprehensive_input.py +0 -0
  60. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/comprehensive_output.py +0 -0
  61. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/constant_ordering_input.py +0 -0
  62. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/constant_ordering_output.py +0 -0
  63. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/custom_decorators_input.py +0 -0
  64. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/custom_decorators_output.py +0 -0
  65. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/keyword_arguments_input.py +0 -0
  66. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/keyword_arguments_output.py +0 -0
  67. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/lazy_annotation_cycle_input.py +0 -0
  68. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/lazy_annotation_cycle_output.py +0 -0
  69. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/mixed_decorators_input.py +0 -0
  70. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/mixed_decorators_output.py +0 -0
  71. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/order_sensitive_input.py +0 -0
  72. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/order_sensitive_output.py +0 -0
  73. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/property_input.py +0 -0
  74. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/property_output.py +0 -0
  75. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/pytest_fixtures_input.py +0 -0
  76. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/pytest_fixtures_output.py +0 -0
  77. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/staticmethod_input.py +0 -0
  78. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/staticmethod_output.py +0 -0
  79. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/underscore_ordering_input.py +0 -0
  80. {codesorter-0.2.2 → codesorter-0.2.4}/tests/test_files/underscore_ordering_output.py +0 -0
  81. {codesorter-0.2.2 → codesorter-0.2.4}/uv.lock +0 -0
@@ -4,6 +4,30 @@
4
4
 
5
5
  codesorter follows `semantic versioning <https://semver.org/>`_.
6
6
 
7
+ ********************
8
+ 0.2.4 (2026/06/14)
9
+ ********************
10
+
11
+ **Fixed**
12
+
13
+ - Never reorder a definition across a side-effecting statement. A bare statement such as
14
+ ``sys.path.insert(0, str(REPO_ROOT))`` is now a barrier, and sorting happens only
15
+ within each segment between barriers, so a constant the statement uses is no longer
16
+ hoisted after it (which raised ``NameError``).
17
+
18
+ ********************
19
+ 0.2.3 (2026/06/14)
20
+ ********************
21
+
22
+ **Fixed**
23
+
24
+ - Keep an assignment that rebinds a name also bound by a sibling in its original
25
+ position relative to that sibling. For example ``ten = cachedproperty(ten, ...)``
26
+ following ``def ten`` now stays after the method it wraps instead of being hoisted
27
+ ahead of it (which raised ``NameError`` and changed which binding wins). Property
28
+ getter/setter/deleter groups, which carry no assignment, are still ordered by their
29
+ sort key.
30
+
7
31
  ********************
8
32
  0.2.2 (2026/06/14)
9
33
  ********************
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codesorter
3
- Version: 0.2.2
3
+ Version: 0.2.4
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.2"
42
+ __version__ = "0.2.4"
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import enum
6
6
  import heapq
7
+ import itertools
7
8
  from collections import defaultdict
8
9
  from enum import auto
9
10
  from typing import TYPE_CHECKING, Protocol, TypeAlias, TypeVar, cast
@@ -272,12 +273,27 @@ class SortCodeCommand(VisitorBasedCodemodCommand, m.MatcherDecoratableTransforme
272
273
  name_to_indexes[_sortable_name(item)].append(index)
273
274
  dependents: list[set[int]] = [set() for _ in items]
274
275
  indegree = [0] * len(items)
276
+
277
+ def _add_edge(earlier: int, later: int) -> None:
278
+ if earlier != later and later not in dependents[earlier]:
279
+ dependents[earlier].add(later)
280
+ indegree[later] += 1
281
+
275
282
  for index, item in enumerate(items):
276
283
  for dependency in self.dependencies.get(_sortable_name(item), ()):
277
284
  for dependency_index in name_to_indexes.get(dependency, ()):
278
- if dependency_index != index and index not in dependents[dependency_index]:
279
- dependents[dependency_index].add(index)
280
- indegree[index] += 1
285
+ _add_edge(dependency_index, index)
286
+ # An assignment that rebinds a name also bound by a sibling (for example
287
+ # ``def x`` followed by ``x = wraps(x)``) must keep its original position, since
288
+ # the later binding wins at runtime and may reference the earlier one. Groups of
289
+ # same-named methods (property getter/setter/deleter) are excluded — they carry
290
+ # no assignment and are ordered safely by their sort key.
291
+ for indexes in name_to_indexes.values():
292
+ for earlier, later in itertools.pairwise(indexes):
293
+ if isinstance(items[earlier], cst.SimpleStatementLine) or isinstance(
294
+ items[later], cst.SimpleStatementLine
295
+ ):
296
+ _add_edge(earlier, later)
281
297
  return dependents, indegree
282
298
 
283
299
  def _get_dependencies( # noqa: C901
@@ -472,6 +488,44 @@ class SortCodeCommand(VisitorBasedCodemodCommand, m.MatcherDecoratableTransforme
472
488
  property_type,
473
489
  )
474
490
 
491
+ def _reorder_segment(
492
+ self,
493
+ body: Sequence[cst.BaseStatement],
494
+ new_body: list[cst.BaseStatement],
495
+ *,
496
+ anchor_indexes: list[int],
497
+ trailers_of: dict[int, list[int]],
498
+ ) -> None:
499
+ """Reorder one barrier-free segment of sortable members in place within ``new_body``."""
500
+ if not anchor_indexes:
501
+ return
502
+ positions: list[int] = []
503
+ anchor_nodes: list[_Sortable] = []
504
+ trailers_by_anchor: dict[int, list[cst.BaseStatement]] = {}
505
+ for index in anchor_indexes:
506
+ trailer_indexes = sorted(trailers_of.get(index, []))
507
+ positions.append(index)
508
+ positions.extend(trailer_indexes)
509
+ anchor = cast("_Sortable", body[index])
510
+ anchor_nodes.append(anchor)
511
+ trailers_by_anchor[id(anchor)] = [body[trailer] for trailer in trailer_indexes]
512
+ # Blank-line separators are positional: the n-th anchor in the segment keeps the
513
+ # n-th separator. Trailers are excluded so they stay tight to their anchor.
514
+ anchor_separators = [_split_leading_lines(node)[0] for node in anchor_nodes]
515
+ trailer_ids = {id(trailer) for trailers in trailers_by_anchor.values() for trailer in trailers}
516
+ flattened: list[cst.BaseStatement] = []
517
+ for anchor in self._sorted_items(anchor_nodes):
518
+ flattened.append(anchor)
519
+ flattened.extend(trailers_by_anchor[id(anchor)])
520
+ anchor_index = 0
521
+ for position, member in zip(sorted(positions), flattened, strict=True):
522
+ if id(member) in trailer_ids:
523
+ new_body[position] = member
524
+ continue
525
+ _, attached = _split_leading_lines(member)
526
+ new_body[position] = member.with_changes(leading_lines=[*anchor_separators[anchor_index], *attached])
527
+ anchor_index += 1
528
+
475
529
  def _resolve_dependents(self, node: _Sortable) -> None:
476
530
  dependencies, _ = self._get_dependencies(node)
477
531
  name = _sortable_name(node)
@@ -497,40 +551,27 @@ class SortCodeCommand(VisitorBasedCodemodCommand, m.MatcherDecoratableTransforme
497
551
  Blank-line spacing stays with each slot while comment lines travel with their
498
552
  statement, so reordering never shifts a blank line onto a different statement.
499
553
 
554
+ Sortable members are only reordered within a segment of consecutive sortable
555
+ members; any other statement (an import, a bare expression such as
556
+ ``sys.path.insert(...)``, an ``if`` block) is a barrier that no definition may
557
+ cross, since the barrier may depend on a definition beside it.
558
+
500
559
  """
501
560
  anchors = self._anchor_trailers(body, sort_constants=sort_constants)
502
561
  trailers_of: defaultdict[int, list[int]] = defaultdict(list)
503
562
  for trailer_index, anchor_index in anchors.items():
504
563
  trailers_of[anchor_index].append(trailer_index)
505
- positions: list[int] = []
506
- anchor_nodes: list[_Sortable] = []
507
- trailers_by_anchor: dict[int, list[cst.BaseStatement]] = {}
508
- for index, member in enumerate(body):
509
- if index in anchors or not self._is_sortable(member, sort_constants=sort_constants):
510
- continue
511
- trailer_indexes = sorted(trailers_of.get(index, []))
512
- positions.append(index)
513
- positions.extend(trailer_indexes)
514
- anchor = cast("_Sortable", member)
515
- anchor_nodes.append(anchor)
516
- trailers_by_anchor[id(anchor)] = [body[trailer] for trailer in trailer_indexes]
517
- # Blank-line separators are positional: the n-th anchor in the body keeps the
518
- # n-th separator. Trailers are excluded so they stay tight to their anchor.
519
- anchor_separators = [_split_leading_lines(node)[0] for node in anchor_nodes]
520
- trailer_ids = {id(trailer) for trailers in trailers_by_anchor.values() for trailer in trailers}
521
- flattened: list[cst.BaseStatement] = []
522
- for anchor in self._sorted_items(anchor_nodes):
523
- flattened.append(anchor)
524
- flattened.extend(trailers_by_anchor[id(anchor)])
525
564
  new_body = list(body)
526
- anchor_index = 0
527
- for position, member in zip(sorted(positions), flattened, strict=True):
528
- if id(member) in trailer_ids:
529
- new_body[position] = member
565
+ segment: list[int] = []
566
+ for index, member in enumerate(body):
567
+ if index in anchors:
568
+ continue # a trailer, reordered together with its anchor
569
+ if self._is_sortable(member, sort_constants=sort_constants):
570
+ segment.append(index)
530
571
  continue
531
- _, attached = _split_leading_lines(member)
532
- new_body[position] = member.with_changes(leading_lines=[*anchor_separators[anchor_index], *attached])
533
- anchor_index += 1
572
+ self._reorder_segment(body, new_body, anchor_indexes=segment, trailers_of=trailers_of)
573
+ segment = []
574
+ self._reorder_segment(body, new_body, anchor_indexes=segment, trailers_of=trailers_of)
534
575
  return new_body
535
576
 
536
577
  def _sorted_items(
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,16 @@
1
+ """An assignment that rebinds a method name stays after the method it wraps."""
2
+
3
+
4
+ class Klass:
5
+ def ten(self):
6
+ return 10
7
+
8
+ ten = cachedproperty(ten, doc="Return 10.")
9
+
10
+ def alpha(self):
11
+ """A method that sorts first alphabetically."""
12
+ return 1
13
+
14
+ def beta(self):
15
+ """A plain method."""
16
+ return 2
@@ -0,0 +1,16 @@
1
+ """An assignment that rebinds a method name stays after the method it wraps."""
2
+
3
+
4
+ class Klass:
5
+ def alpha(self):
6
+ """A method that sorts first alphabetically."""
7
+ return 1
8
+
9
+ def beta(self):
10
+ """A plain method."""
11
+ return 2
12
+
13
+ def ten(self):
14
+ return 10
15
+
16
+ ten = cachedproperty(ten, doc="Return 10.")
@@ -48,6 +48,22 @@ class TestSortCodeCommand:
48
48
 
49
49
  assert expected_code == result.code
50
50
 
51
+ def test_barrier(self, test_files):
52
+ """Test that definitions are not reordered across a side-effecting statement.
53
+
54
+ A bare statement such as ``sys.path.insert(0, str(REPO_ROOT))`` is a barrier:
55
+ the constants before it (which it may use) must stay before it, so sorting
56
+ happens only within each segment between barriers rather than across the whole
57
+ module.
58
+
59
+ """
60
+ input_code, expected_code = test_files
61
+ context = CodemodContext()
62
+ command = SortCodeCommand(context)
63
+ result = command.transform_module(cst.parse_module(input_code))
64
+
65
+ assert expected_code == result.code
66
+
51
67
  def test_basic_function(self, test_files):
52
68
  """Test that functions are sorted correctly."""
53
69
  input_code, expected_code = test_files
@@ -217,6 +233,23 @@ from pathlib import Path
217
233
  assert "import sys" in result.code
218
234
  assert "from pathlib import Path" in result.code
219
235
 
236
+ def test_name_rebinding(self, test_files):
237
+ """Test that an assignment rebinding a method name stays after that method.
238
+
239
+ ``ten = cachedproperty(ten, ...)`` binds the same name as the ``def ten`` above
240
+ it and wraps that method, so the assignment must keep its position after the
241
+ method (moving it ahead would raise ``NameError`` and change which binding
242
+ wins). Property getter/setter/deleter groups are unaffected — they carry no
243
+ assignment.
244
+
245
+ """
246
+ input_code, expected_code = test_files
247
+ context = CodemodContext()
248
+ command = SortCodeCommand(context)
249
+ result = command.transform_module(cst.parse_module(input_code))
250
+
251
+ assert expected_code == result.code
252
+
220
253
  def test_order_sensitive(self, test_files):
221
254
  """Test that enum, dataclass, and named-tuple member order is preserved."""
222
255
  input_code, expected_code = test_files
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
File without changes