undersort 0.1.0__tar.gz → 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: undersort
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: A tool to sort class methods by visibility (public, protected, private)
5
5
  Project-URL: Homepage, https://github.com/kivicode/undersort
6
6
  Project-URL: Repository, https://github.com/kivicode/undersort
@@ -61,8 +61,8 @@ order = ["public", "protected", "private"]
61
61
 
62
62
  # Method type ordering within each visibility level (secondary sort, optional)
63
63
  # Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
64
- # Default: ["class", "static", "instance"]
65
- method_type_order = ["class", "static", "instance"]
64
+ # Default: ["instance", "class", "static"]
65
+ method_type_order = ["instance", "class", "static"]
66
66
  ```
67
67
 
68
68
  ### Method Visibility Rules
@@ -82,19 +82,24 @@ method_type_order = ["class", "static", "instance"]
82
82
  Methods are sorted in two levels:
83
83
 
84
84
  1. **Primary**: By visibility (public → protected → private)
85
- 2. **Secondary**: Within each visibility level, by method type (class → static → instance by default)
85
+ 2. **Secondary**: Within each visibility level, by method type (instance → class → static by default)
86
86
 
87
- Example: With default configuration, the order would be:
87
+ The sorting algorithm **minimizes movement** to preserve the original order as much as possible:
88
+ - Methods that need to move DOWN (to a later section) are placed at the **beginning** of their target section
89
+ - Methods that need to move UP (to an earlier section) are placed at the **end** of their target section
90
+ - Methods already in the correct section maintain their relative order
88
91
 
89
- 1. Public class methods
90
- 2. Public static methods
91
- 3. Public instance methods
92
- 4. Protected class methods
93
- 5. Protected static methods
94
- 6. Protected instance methods
95
- 7. Private class methods
96
- 8. Private static methods
97
- 9. Private instance methods
92
+ Example order with default configuration:
93
+
94
+ 1. Public instance methods
95
+ 2. Public class methods
96
+ 3. Public static methods
97
+ 4. Protected instance methods
98
+ 5. Protected class methods
99
+ 6. Protected static methods
100
+ 7. Private instance methods
101
+ 8. Private class methods
102
+ 9. Private static methods
98
103
 
99
104
  ## Usage
100
105
 
@@ -131,7 +136,7 @@ undersort --diff example.py
131
136
  undersort --check --diff src/
132
137
  ```
133
138
 
134
- **Note**: By default, undersort excludes common directories like `.venv`, `.git`, `__pycache__`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, and `node_modules` when scanning directories.
139
+ **Note**: By default, undersort excludes all dot-prefixed directories (e.g., `.venv`, `.git`, `.pytest_cache`) and common build directories (`venv`, `__pycache__`, `node_modules`) when scanning directories recursively.
135
140
 
136
141
  ### Pre-commit Integration
137
142
 
@@ -190,6 +195,12 @@ class Example:
190
195
 
191
196
  ```python
192
197
  class Example:
198
+ def __init__(self):
199
+ pass
200
+
201
+ def public_instance(self):
202
+ pass
203
+
193
204
  @classmethod
194
205
  def public_class(cls):
195
206
  pass
@@ -198,19 +209,13 @@ class Example:
198
209
  def public_static():
199
210
  pass
200
211
 
201
- def __init__(self):
202
- pass
203
-
204
- def public_instance(self):
212
+ def _protected_instance(self):
205
213
  pass
206
214
 
207
215
  @classmethod
208
216
  def _protected_class(cls):
209
217
  pass
210
218
 
211
- def _protected_instance(self):
212
- pass
213
-
214
219
  def __private_method(self):
215
220
  pass
216
221
  ```
@@ -218,7 +223,7 @@ class Example:
218
223
  The methods are now organized by:
219
224
 
220
225
  1. **Visibility**: public (including `__init__`) → protected → private
221
- 2. **Type** (within each visibility): class → static → instance
226
+ 2. **Type** (within each visibility): instance → class → static
222
227
 
223
228
  ## Development
224
229
 
@@ -34,8 +34,8 @@ order = ["public", "protected", "private"]
34
34
 
35
35
  # Method type ordering within each visibility level (secondary sort, optional)
36
36
  # Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
37
- # Default: ["class", "static", "instance"]
38
- method_type_order = ["class", "static", "instance"]
37
+ # Default: ["instance", "class", "static"]
38
+ method_type_order = ["instance", "class", "static"]
39
39
  ```
40
40
 
41
41
  ### Method Visibility Rules
@@ -55,19 +55,24 @@ method_type_order = ["class", "static", "instance"]
55
55
  Methods are sorted in two levels:
56
56
 
57
57
  1. **Primary**: By visibility (public → protected → private)
58
- 2. **Secondary**: Within each visibility level, by method type (class → static → instance by default)
58
+ 2. **Secondary**: Within each visibility level, by method type (instance → class → static by default)
59
59
 
60
- Example: With default configuration, the order would be:
60
+ The sorting algorithm **minimizes movement** to preserve the original order as much as possible:
61
+ - Methods that need to move DOWN (to a later section) are placed at the **beginning** of their target section
62
+ - Methods that need to move UP (to an earlier section) are placed at the **end** of their target section
63
+ - Methods already in the correct section maintain their relative order
61
64
 
62
- 1. Public class methods
63
- 2. Public static methods
64
- 3. Public instance methods
65
- 4. Protected class methods
66
- 5. Protected static methods
67
- 6. Protected instance methods
68
- 7. Private class methods
69
- 8. Private static methods
70
- 9. Private instance methods
65
+ Example order with default configuration:
66
+
67
+ 1. Public instance methods
68
+ 2. Public class methods
69
+ 3. Public static methods
70
+ 4. Protected instance methods
71
+ 5. Protected class methods
72
+ 6. Protected static methods
73
+ 7. Private instance methods
74
+ 8. Private class methods
75
+ 9. Private static methods
71
76
 
72
77
  ## Usage
73
78
 
@@ -104,7 +109,7 @@ undersort --diff example.py
104
109
  undersort --check --diff src/
105
110
  ```
106
111
 
107
- **Note**: By default, undersort excludes common directories like `.venv`, `.git`, `__pycache__`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, and `node_modules` when scanning directories.
112
+ **Note**: By default, undersort excludes all dot-prefixed directories (e.g., `.venv`, `.git`, `.pytest_cache`) and common build directories (`venv`, `__pycache__`, `node_modules`) when scanning directories recursively.
108
113
 
109
114
  ### Pre-commit Integration
110
115
 
@@ -163,6 +168,12 @@ class Example:
163
168
 
164
169
  ```python
165
170
  class Example:
171
+ def __init__(self):
172
+ pass
173
+
174
+ def public_instance(self):
175
+ pass
176
+
166
177
  @classmethod
167
178
  def public_class(cls):
168
179
  pass
@@ -171,19 +182,13 @@ class Example:
171
182
  def public_static():
172
183
  pass
173
184
 
174
- def __init__(self):
175
- pass
176
-
177
- def public_instance(self):
185
+ def _protected_instance(self):
178
186
  pass
179
187
 
180
188
  @classmethod
181
189
  def _protected_class(cls):
182
190
  pass
183
191
 
184
- def _protected_instance(self):
185
- pass
186
-
187
192
  def __private_method(self):
188
193
  pass
189
194
  ```
@@ -191,7 +196,7 @@ class Example:
191
196
  The methods are now organized by:
192
197
 
193
198
  1. **Visibility**: public (including `__init__`) → protected → private
194
- 2. **Type** (within each visibility): class → static → instance
199
+ 2. **Type** (within each visibility): instance → class → static
195
200
 
196
201
  ## Development
197
202
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "undersort"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "A tool to sort class methods by visibility (public, protected, private)"
9
9
  authors = [{ name = "KiviCode", email = "kivicode.dev@gmail.com" }]
10
10
  readme = "README.md"
@@ -47,8 +47,8 @@ order = ["public", "protected", "private"]
47
47
 
48
48
  # Method type ordering within each visibility level (optional)
49
49
  # Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
50
- # Default order if not specified: ["class", "static", "instance"]
51
- method_type_order = ["class", "static", "instance"]
50
+ # Default order if not specified: ["instance", "class", "static"]
51
+ method_type_order = ["instance", "class", "static"]
52
52
 
53
53
  [tool.ruff]
54
54
  unsafe-fixes = true
@@ -382,7 +382,7 @@ class Example:
382
382
 
383
383
  try:
384
384
  order = ["public", "protected", "private"]
385
- method_type_order = ["class", "static", "instance"]
385
+ method_type_order = ["instance", "class", "static"]
386
386
  was_modified = sort_file(temp_path, order, method_type_order)
387
387
 
388
388
  assert was_modified is True
@@ -390,19 +390,21 @@ class Example:
390
390
  with open(temp_path) as f:
391
391
  result = f.read()
392
392
 
393
- # Check order: class -> static -> instance
393
+ instance_idx = result.find("def instance_method")
394
394
  class_idx = result.find("def class_method")
395
395
  static_idx = result.find("def static_method")
396
- instance_idx = result.find("def instance_method")
397
396
 
398
- assert class_idx < static_idx < instance_idx
397
+ assert instance_idx < class_idx < static_idx
399
398
  finally:
400
399
  temp_path.unlink()
401
400
 
402
401
  def test_method_type_sorting_custom_order(self) -> None:
403
- """Test custom method type ordering (instance first)."""
402
+ """Test custom method type ordering (class first)."""
404
403
  source = """
405
404
  class Example:
405
+ def instance_method(self):
406
+ pass
407
+
406
408
  @classmethod
407
409
  def class_method(cls):
408
410
  pass
@@ -410,9 +412,6 @@ class Example:
410
412
  @staticmethod
411
413
  def static_method():
412
414
  pass
413
-
414
- def instance_method(self):
415
- pass
416
415
  """
417
416
  with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
418
417
  f.write(source)
@@ -420,7 +419,7 @@ class Example:
420
419
 
421
420
  try:
422
421
  order = ["public", "protected", "private"]
423
- method_type_order = ["instance", "static", "class"]
422
+ method_type_order = ["class", "static", "instance"]
424
423
  was_modified = sort_file(temp_path, order, method_type_order)
425
424
 
426
425
  assert was_modified is True
@@ -428,12 +427,11 @@ class Example:
428
427
  with open(temp_path) as f:
429
428
  result = f.read()
430
429
 
431
- # Check order: instance -> static -> class
432
- instance_idx = result.find("def instance_method")
433
- static_idx = result.find("def static_method")
434
430
  class_idx = result.find("def class_method")
431
+ static_idx = result.find("def static_method")
432
+ instance_idx = result.find("def instance_method")
435
433
 
436
- assert instance_idx < static_idx < class_idx
434
+ assert class_idx < static_idx < instance_idx
437
435
  finally:
438
436
  temp_path.unlink()
439
437
 
@@ -465,7 +463,7 @@ class Example:
465
463
 
466
464
  try:
467
465
  order = ["public", "protected", "private"]
468
- method_type_order = ["class", "static", "instance"]
466
+ method_type_order = ["instance", "class", "static"]
469
467
  was_modified = sort_file(temp_path, order, method_type_order)
470
468
 
471
469
  assert was_modified is True
@@ -473,23 +471,18 @@ class Example:
473
471
  with open(temp_path) as f:
474
472
  result = f.read()
475
473
 
476
- # Public methods should come first
477
- public_class_idx = result.find("def public_class")
478
474
  public_instance_idx = result.find("def public_instance")
475
+ public_class_idx = result.find("def public_class")
479
476
 
480
- # Protected methods should come after public
477
+ protected_instance_idx = result.find("def _protected_instance")
481
478
  protected_class_idx = result.find("def _protected_class")
482
479
  protected_static_idx = result.find("def _protected_static")
483
- protected_instance_idx = result.find("def _protected_instance")
484
480
 
485
- # Check visibility ordering
486
- assert public_class_idx < protected_class_idx
487
481
  assert public_instance_idx < protected_instance_idx
482
+ assert public_class_idx < protected_class_idx
488
483
 
489
- # Within public: class -> static -> instance
490
- assert public_class_idx < public_instance_idx
484
+ assert public_instance_idx < public_class_idx
491
485
 
492
- # Within protected: class -> static -> instance
493
- assert protected_class_idx < protected_static_idx < protected_instance_idx
486
+ assert protected_instance_idx < protected_class_idx < protected_static_idx
494
487
  finally:
495
488
  temp_path.unlink()
@@ -1,3 +1,3 @@
1
1
  """undersort - Sort class methods by visibility."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -19,15 +19,9 @@ def collect_python_files(path: Path, recursive: bool = True) -> list[Path]:
19
19
  Returns:
20
20
  List of Python file paths
21
21
  """
22
- # Common directories to exclude
23
22
  exclude_dirs = {
24
- ".venv",
25
23
  "venv",
26
- ".git",
27
24
  "__pycache__",
28
- ".pytest_cache",
29
- ".mypy_cache",
30
- ".ruff_cache",
31
25
  "node_modules",
32
26
  }
33
27
 
@@ -37,7 +31,11 @@ def collect_python_files(path: Path, recursive: bool = True) -> list[Path]:
37
31
  if path.is_dir():
38
32
  pattern = "**/*.py" if recursive else "*.py"
39
33
  all_files = path.glob(pattern)
40
- filtered_files = [f for f in all_files if not any(excluded in f.parts for excluded in exclude_dirs)]
34
+ filtered_files = [
35
+ f
36
+ for f in all_files
37
+ if not any(part in exclude_dirs or (part.startswith(".") and part != ".") for part in f.parts)
38
+ ]
41
39
 
42
40
  return sorted(filtered_files)
43
41
 
@@ -2,13 +2,14 @@
2
2
 
3
3
  import difflib
4
4
  from pathlib import Path
5
+ from typing import Literal
5
6
 
6
7
  import libcst as cst
7
8
 
8
9
  from undersort import logger
9
10
 
10
11
 
11
- def get_method_visibility(method_name: str) -> str:
12
+ def get_method_visibility(method_name: str) -> Literal["public", "private", "protected"]:
12
13
  """Determine method visibility based on naming convention.
13
14
 
14
15
  Args:
@@ -63,13 +64,15 @@ class MethodSorter(cst.CSTTransformer):
63
64
  (e.g., ["public", "protected", "private"])
64
65
  method_type_order: Optional list specifying the order of method types
65
66
  within each visibility level
66
- (e.g., ["class", "static", "instance"])
67
+ (e.g., ["instance", "class", "static"])
67
68
  """
68
69
  self.order = order
69
- self.method_type_order = method_type_order or ["class", "static", "instance"]
70
+ self.method_type_order = method_type_order or ["instance", "class", "static"]
70
71
  self.modified = False
71
72
 
72
- def leave_ClassDef(self, original_node: cst.ClassDef, updated_node: cst.ClassDef) -> cst.ClassDef:
73
+ def leave_ClassDef( # noqa: PLR0912, PLR0915
74
+ self, original_node: cst.ClassDef, updated_node: cst.ClassDef
75
+ ) -> cst.ClassDef:
73
76
  """Sort methods within a class definition.
74
77
 
75
78
  Args:
@@ -91,21 +94,56 @@ class MethodSorter(cst.CSTTransformer):
91
94
  if not methods:
92
95
  return updated_node
93
96
 
94
- method_groups: dict[str, dict[str, list[cst.FunctionDef]]] = {
97
+ method_with_index = [(i, method) for i, method in enumerate(methods)]
98
+
99
+ method_groups: dict[str, dict[str, list[tuple[int, cst.FunctionDef]]]] = {
95
100
  "public": {"class": [], "static": [], "instance": []},
96
101
  "protected": {"class": [], "static": [], "instance": []},
97
102
  "private": {"class": [], "static": [], "instance": []},
98
103
  }
99
104
 
100
- for method in methods:
105
+ for idx, method in method_with_index:
101
106
  visibility = get_method_visibility(method.name.value)
102
107
  method_type = get_method_type(method)
103
- method_groups[visibility][method_type].append(method)
108
+ method_groups[visibility][method_type].append((idx, method))
104
109
 
105
- sorted_methods = []
110
+ group_order = []
106
111
  for visibility in self.order:
107
112
  for method_type in self.method_type_order:
108
- sorted_methods.extend(method_groups[visibility][method_type])
113
+ group_order.append((visibility, method_type))
114
+
115
+ sorted_methods = []
116
+ current_position = 0
117
+
118
+ for visibility, method_type in group_order:
119
+ group = method_groups[visibility][method_type]
120
+ if not group:
121
+ continue
122
+
123
+ group_indices = [idx for idx, _ in group]
124
+ min_original_idx = min(group_indices)
125
+ max_original_idx = max(group_indices)
126
+
127
+ moved_down = []
128
+ in_place = []
129
+ moved_up = []
130
+
131
+ for idx, method in group:
132
+ if idx < min_original_idx or (idx < current_position and current_position > 0):
133
+ moved_down.append((idx, method))
134
+ elif idx > max_original_idx:
135
+ moved_up.append((idx, method))
136
+ else:
137
+ in_place.append((idx, method))
138
+
139
+ moved_down.sort(key=lambda x: x[0])
140
+ in_place.sort(key=lambda x: x[0])
141
+ moved_up.sort(key=lambda x: x[0])
142
+
143
+ group_sorted = moved_down + in_place + moved_up
144
+ sorted_methods.extend([method for _, method in group_sorted])
145
+
146
+ current_position += len(group)
109
147
 
110
148
  if methods != sorted_methods:
111
149
  self.modified = True
@@ -905,7 +905,7 @@ wheels = [
905
905
 
906
906
  [[package]]
907
907
  name = "undersort"
908
- version = "0.1.0"
908
+ version = "0.1.2"
909
909
  source = { editable = "." }
910
910
  dependencies = [
911
911
  { name = "libcst", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes