codesorter 0.2.3__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.3 → codesorter-0.2.4}/CHANGES.rst +19 -8
  2. {codesorter-0.2.3 → codesorter-0.2.4}/PKG-INFO +1 -1
  3. {codesorter-0.2.3 → codesorter-0.2.4}/codesorter/const.py +1 -1
  4. {codesorter-0.2.3 → codesorter-0.2.4}/codesorter/sort_code.py +52 -27
  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.3 → codesorter-0.2.4}/tests/test_sort_code.py +16 -0
  8. {codesorter-0.2.3 → codesorter-0.2.4}/.github/dependabot.yml +0 -0
  9. {codesorter-0.2.3 → codesorter-0.2.4}/.github/workflows/ci.yml +0 -0
  10. {codesorter-0.2.3 → codesorter-0.2.4}/.github/workflows/pre-commit_autoupdate.yml +0 -0
  11. {codesorter-0.2.3 → codesorter-0.2.4}/.github/workflows/prepare_release.yml +0 -0
  12. {codesorter-0.2.3 → codesorter-0.2.4}/.github/workflows/pypi.yml +0 -0
  13. {codesorter-0.2.3 → codesorter-0.2.4}/.github/workflows/scorecard.yml +0 -0
  14. {codesorter-0.2.3 → codesorter-0.2.4}/.github/workflows/stale_action.yml +0 -0
  15. {codesorter-0.2.3 → codesorter-0.2.4}/.github/workflows/tag_release.yml +0 -0
  16. {codesorter-0.2.3 → codesorter-0.2.4}/.gitignore +0 -0
  17. {codesorter-0.2.3 → codesorter-0.2.4}/.libcst.codemod.yaml +0 -0
  18. {codesorter-0.2.3 → codesorter-0.2.4}/.pre-commit-config.yaml +0 -0
  19. {codesorter-0.2.3 → codesorter-0.2.4}/.pre-commit-hooks.yaml +0 -0
  20. {codesorter-0.2.3 → codesorter-0.2.4}/.python-version +0 -0
  21. {codesorter-0.2.3 → codesorter-0.2.4}/.readthedocs.yaml +0 -0
  22. {codesorter-0.2.3 → codesorter-0.2.4}/LICENSE.txt +0 -0
  23. {codesorter-0.2.3 → codesorter-0.2.4}/README.rst +0 -0
  24. {codesorter-0.2.3 → codesorter-0.2.4}/codesorter/__init__.py +0 -0
  25. {codesorter-0.2.3 → codesorter-0.2.4}/codesorter/cli.py +0 -0
  26. {codesorter-0.2.3 → codesorter-0.2.4}/codesorter/py.typed +0 -0
  27. {codesorter-0.2.3 → codesorter-0.2.4}/docs/Makefile +0 -0
  28. {codesorter-0.2.3 → codesorter-0.2.4}/docs/code_overview/sort_code.rst +0 -0
  29. {codesorter-0.2.3 → codesorter-0.2.4}/docs/conf.py +0 -0
  30. {codesorter-0.2.3 → codesorter-0.2.4}/docs/contributing/pre_commit.rst +0 -0
  31. {codesorter-0.2.3 → codesorter-0.2.4}/docs/docutils.conf +0 -0
  32. {codesorter-0.2.3 → codesorter-0.2.4}/docs/genindex.rst +0 -0
  33. {codesorter-0.2.3 → codesorter-0.2.4}/docs/index.rst +0 -0
  34. {codesorter-0.2.3 → codesorter-0.2.4}/docs/make.bat +0 -0
  35. {codesorter-0.2.3 → codesorter-0.2.4}/docs/package_info/change_log.rst +0 -0
  36. {codesorter-0.2.3 → codesorter-0.2.4}/examples/after_example.py +0 -0
  37. {codesorter-0.2.3 → codesorter-0.2.4}/examples/before_example.py +0 -0
  38. {codesorter-0.2.3 → codesorter-0.2.4}/pyproject.toml +0 -0
  39. {codesorter-0.2.3 → codesorter-0.2.4}/tests/__init__.py +0 -0
  40. {codesorter-0.2.3 → codesorter-0.2.4}/tests/conftest.py +0 -0
  41. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/alias_function_input.py +0 -0
  42. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/alias_function_output.py +0 -0
  43. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/augmented_assignment_input.py +0 -0
  44. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/augmented_assignment_output.py +0 -0
  45. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/basic_function_input.py +0 -0
  46. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/basic_function_output.py +0 -0
  47. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/class_global_dependency_input.py +0 -0
  48. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/class_global_dependency_output.py +0 -0
  49. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/class_inheritance_input.py +0 -0
  50. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/class_inheritance_output.py +0 -0
  51. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/class_method_input.py +0 -0
  52. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/class_method_output.py +0 -0
  53. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/classmethod_input.py +0 -0
  54. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/classmethod_output.py +0 -0
  55. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/comprehension_dependency_input.py +0 -0
  56. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/comprehension_dependency_output.py +0 -0
  57. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/comprehensive_input.py +0 -0
  58. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/comprehensive_output.py +0 -0
  59. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/constant_ordering_input.py +0 -0
  60. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/constant_ordering_output.py +0 -0
  61. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/custom_decorators_input.py +0 -0
  62. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/custom_decorators_output.py +0 -0
  63. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/keyword_arguments_input.py +0 -0
  64. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/keyword_arguments_output.py +0 -0
  65. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/lazy_annotation_cycle_input.py +0 -0
  66. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/lazy_annotation_cycle_output.py +0 -0
  67. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/mixed_decorators_input.py +0 -0
  68. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/mixed_decorators_output.py +0 -0
  69. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/name_rebinding_input.py +0 -0
  70. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/name_rebinding_output.py +0 -0
  71. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/order_sensitive_input.py +0 -0
  72. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/order_sensitive_output.py +0 -0
  73. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/property_input.py +0 -0
  74. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/property_output.py +0 -0
  75. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/pytest_fixtures_input.py +0 -0
  76. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/pytest_fixtures_output.py +0 -0
  77. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/staticmethod_input.py +0 -0
  78. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/staticmethod_output.py +0 -0
  79. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/underscore_ordering_input.py +0 -0
  80. {codesorter-0.2.3 → codesorter-0.2.4}/tests/test_files/underscore_ordering_output.py +0 -0
  81. {codesorter-0.2.3 → codesorter-0.2.4}/uv.lock +0 -0
@@ -5,18 +5,19 @@
5
5
  codesorter follows `semantic versioning <https://semver.org/>`_.
6
6
 
7
7
  ********************
8
- 0.2.3 (2026/06/14)
8
+ 0.2.4 (2026/06/14)
9
9
  ********************
10
10
 
11
- ********************
12
- 0.2.2 (2026/06/14)
13
- ********************
11
+ **Fixed**
14
12
 
15
- **Changed**
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``).
16
17
 
17
- - Format reordered files with ``ruff`` by default instead of ``black``, so a downstream
18
- project no longer needs a ``.libcst.codemod.yaml`` to use the ``codesorter`` CLI or
19
- pre-commit hook. ``ruff`` must be importable on the ``PATH`` when sorting.
18
+ ********************
19
+ 0.2.3 (2026/06/14)
20
+ ********************
20
21
 
21
22
  **Fixed**
22
23
 
@@ -27,6 +28,16 @@ codesorter follows `semantic versioning <https://semver.org/>`_.
27
28
  getter/setter/deleter groups, which carry no assignment, are still ordered by their
28
29
  sort key.
29
30
 
31
+ ********************
32
+ 0.2.2 (2026/06/14)
33
+ ********************
34
+
35
+ **Changed**
36
+
37
+ - Format reordered files with ``ruff`` by default instead of ``black``, so a downstream
38
+ project no longer needs a ``.libcst.codemod.yaml`` to use the ``codesorter`` CLI or
39
+ pre-commit hook. ``ruff`` must be importable on the ``PATH`` when sorting.
40
+
30
41
  ********************
31
42
  0.2.1 (2026/06/14)
32
43
  ********************
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codesorter
3
- Version: 0.2.3
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.3"
42
+ __version__ = "0.2.4"
@@ -488,6 +488,44 @@ class SortCodeCommand(VisitorBasedCodemodCommand, m.MatcherDecoratableTransforme
488
488
  property_type,
489
489
  )
490
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
+
491
529
  def _resolve_dependents(self, node: _Sortable) -> None:
492
530
  dependencies, _ = self._get_dependencies(node)
493
531
  name = _sortable_name(node)
@@ -513,40 +551,27 @@ class SortCodeCommand(VisitorBasedCodemodCommand, m.MatcherDecoratableTransforme
513
551
  Blank-line spacing stays with each slot while comment lines travel with their
514
552
  statement, so reordering never shifts a blank line onto a different statement.
515
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
+
516
559
  """
517
560
  anchors = self._anchor_trailers(body, sort_constants=sort_constants)
518
561
  trailers_of: defaultdict[int, list[int]] = defaultdict(list)
519
562
  for trailer_index, anchor_index in anchors.items():
520
563
  trailers_of[anchor_index].append(trailer_index)
521
- positions: list[int] = []
522
- anchor_nodes: list[_Sortable] = []
523
- trailers_by_anchor: dict[int, list[cst.BaseStatement]] = {}
524
- for index, member in enumerate(body):
525
- if index in anchors or not self._is_sortable(member, sort_constants=sort_constants):
526
- continue
527
- trailer_indexes = sorted(trailers_of.get(index, []))
528
- positions.append(index)
529
- positions.extend(trailer_indexes)
530
- anchor = cast("_Sortable", member)
531
- anchor_nodes.append(anchor)
532
- trailers_by_anchor[id(anchor)] = [body[trailer] for trailer in trailer_indexes]
533
- # Blank-line separators are positional: the n-th anchor in the body keeps the
534
- # n-th separator. Trailers are excluded so they stay tight to their anchor.
535
- anchor_separators = [_split_leading_lines(node)[0] for node in anchor_nodes]
536
- trailer_ids = {id(trailer) for trailers in trailers_by_anchor.values() for trailer in trailers}
537
- flattened: list[cst.BaseStatement] = []
538
- for anchor in self._sorted_items(anchor_nodes):
539
- flattened.append(anchor)
540
- flattened.extend(trailers_by_anchor[id(anchor)])
541
564
  new_body = list(body)
542
- anchor_index = 0
543
- for position, member in zip(sorted(positions), flattened, strict=True):
544
- if id(member) in trailer_ids:
545
- 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)
546
571
  continue
547
- _, attached = _split_leading_lines(member)
548
- new_body[position] = member.with_changes(leading_lines=[*anchor_separators[anchor_index], *attached])
549
- 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)
550
575
  return new_body
551
576
 
552
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
@@ -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
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