undersort 0.1.1__tar.gz → 0.1.3__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.1
3
+ Version: 0.1.3
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
@@ -43,10 +43,15 @@ A Python tool that automatically sorts class methods by visibility (public, prot
43
43
 
44
44
  ```bash
45
45
  # Using uv (recommended)
46
- uv pip install -e .
46
+ uv add undersort
47
47
 
48
48
  # Using pip
49
- pip install -e .
49
+ pip install undersort
50
+
51
+ # For development
52
+ git clone https://github.com/kivicode/undersort
53
+ cd undersort
54
+ uv sync
50
55
  ```
51
56
 
52
57
  ## Configuration
@@ -61,8 +66,8 @@ order = ["public", "protected", "private"]
61
66
 
62
67
  # Method type ordering within each visibility level (secondary sort, optional)
63
68
  # Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
64
- # Default: ["class", "static", "instance"]
65
- method_type_order = ["class", "static", "instance"]
69
+ # Default: ["instance", "class", "static"]
70
+ method_type_order = ["instance", "class", "static"]
66
71
  ```
67
72
 
68
73
  ### Method Visibility Rules
@@ -82,19 +87,25 @@ method_type_order = ["class", "static", "instance"]
82
87
  Methods are sorted in two levels:
83
88
 
84
89
  1. **Primary**: By visibility (public → protected → private)
85
- 2. **Secondary**: Within each visibility level, by method type (class → static → instance by default)
90
+ 2. **Secondary**: Within each visibility level, by method type (instance → class → static by default)
91
+
92
+ The sorting algorithm **minimizes movement** to preserve the original order as much as possible:
86
93
 
87
- Example: With default configuration, the order would be:
94
+ - Methods that need to move DOWN (to a later section) are placed at the **beginning** of their target section
95
+ - Methods that need to move UP (to an earlier section) are placed at the **end** of their target section
96
+ - Methods already in the correct section maintain their relative order
88
97
 
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
98
+ Example order with default configuration:
99
+
100
+ 1. Public instance methods
101
+ 2. Public class methods
102
+ 3. Public static methods
103
+ 4. Protected instance methods
104
+ 5. Protected class methods
105
+ 6. Protected static methods
106
+ 7. Private instance methods
107
+ 8. Private class methods
108
+ 9. Private static methods
98
109
 
99
110
  ## Usage
100
111
 
@@ -143,9 +154,10 @@ repos:
143
154
  hooks:
144
155
  - id: undersort
145
156
  name: undersort
146
- entry: uv run undersort
147
- language: system
157
+ entry: undersort
158
+ language: python
148
159
  types: [python]
160
+ additional_dependencies: ["undersort"]
149
161
  ```
150
162
 
151
163
  Then install the hook:
@@ -190,6 +202,12 @@ class Example:
190
202
 
191
203
  ```python
192
204
  class Example:
205
+ def __init__(self):
206
+ pass
207
+
208
+ def public_instance(self):
209
+ pass
210
+
193
211
  @classmethod
194
212
  def public_class(cls):
195
213
  pass
@@ -198,19 +216,13 @@ class Example:
198
216
  def public_static():
199
217
  pass
200
218
 
201
- def __init__(self):
202
- pass
203
-
204
- def public_instance(self):
219
+ def _protected_instance(self):
205
220
  pass
206
221
 
207
222
  @classmethod
208
223
  def _protected_class(cls):
209
224
  pass
210
225
 
211
- def _protected_instance(self):
212
- pass
213
-
214
226
  def __private_method(self):
215
227
  pass
216
228
  ```
@@ -218,7 +230,7 @@ class Example:
218
230
  The methods are now organized by:
219
231
 
220
232
  1. **Visibility**: public (including `__init__`) → protected → private
221
- 2. **Type** (within each visibility): class → static → instance
233
+ 2. **Type** (within each visibility): instance → class → static
222
234
 
223
235
  ## Development
224
236
 
@@ -16,10 +16,15 @@ A Python tool that automatically sorts class methods by visibility (public, prot
16
16
 
17
17
  ```bash
18
18
  # Using uv (recommended)
19
- uv pip install -e .
19
+ uv add undersort
20
20
 
21
21
  # Using pip
22
- pip install -e .
22
+ pip install undersort
23
+
24
+ # For development
25
+ git clone https://github.com/kivicode/undersort
26
+ cd undersort
27
+ uv sync
23
28
  ```
24
29
 
25
30
  ## Configuration
@@ -34,8 +39,8 @@ order = ["public", "protected", "private"]
34
39
 
35
40
  # Method type ordering within each visibility level (secondary sort, optional)
36
41
  # Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
37
- # Default: ["class", "static", "instance"]
38
- method_type_order = ["class", "static", "instance"]
42
+ # Default: ["instance", "class", "static"]
43
+ method_type_order = ["instance", "class", "static"]
39
44
  ```
40
45
 
41
46
  ### Method Visibility Rules
@@ -55,19 +60,25 @@ method_type_order = ["class", "static", "instance"]
55
60
  Methods are sorted in two levels:
56
61
 
57
62
  1. **Primary**: By visibility (public → protected → private)
58
- 2. **Secondary**: Within each visibility level, by method type (class → static → instance by default)
63
+ 2. **Secondary**: Within each visibility level, by method type (instance → class → static by default)
64
+
65
+ The sorting algorithm **minimizes movement** to preserve the original order as much as possible:
59
66
 
60
- Example: With default configuration, the order would be:
67
+ - Methods that need to move DOWN (to a later section) are placed at the **beginning** of their target section
68
+ - Methods that need to move UP (to an earlier section) are placed at the **end** of their target section
69
+ - Methods already in the correct section maintain their relative order
61
70
 
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
71
+ Example order with default configuration:
72
+
73
+ 1. Public instance methods
74
+ 2. Public class methods
75
+ 3. Public static methods
76
+ 4. Protected instance methods
77
+ 5. Protected class methods
78
+ 6. Protected static methods
79
+ 7. Private instance methods
80
+ 8. Private class methods
81
+ 9. Private static methods
71
82
 
72
83
  ## Usage
73
84
 
@@ -116,9 +127,10 @@ repos:
116
127
  hooks:
117
128
  - id: undersort
118
129
  name: undersort
119
- entry: uv run undersort
120
- language: system
130
+ entry: undersort
131
+ language: python
121
132
  types: [python]
133
+ additional_dependencies: ["undersort"]
122
134
  ```
123
135
 
124
136
  Then install the hook:
@@ -163,6 +175,12 @@ class Example:
163
175
 
164
176
  ```python
165
177
  class Example:
178
+ def __init__(self):
179
+ pass
180
+
181
+ def public_instance(self):
182
+ pass
183
+
166
184
  @classmethod
167
185
  def public_class(cls):
168
186
  pass
@@ -171,19 +189,13 @@ class Example:
171
189
  def public_static():
172
190
  pass
173
191
 
174
- def __init__(self):
175
- pass
176
-
177
- def public_instance(self):
192
+ def _protected_instance(self):
178
193
  pass
179
194
 
180
195
  @classmethod
181
196
  def _protected_class(cls):
182
197
  pass
183
198
 
184
- def _protected_instance(self):
185
- pass
186
-
187
199
  def __private_method(self):
188
200
  pass
189
201
  ```
@@ -191,7 +203,7 @@ class Example:
191
203
  The methods are now organized by:
192
204
 
193
205
  1. **Visibility**: public (including `__init__`) → protected → private
194
- 2. **Type** (within each visibility): class → static → instance
206
+ 2. **Type** (within each visibility): instance → class → static
195
207
 
196
208
  ## Development
197
209
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "undersort"
7
- version = "0.1.1"
7
+ version = "0.1.3"
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.3"
@@ -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.1"
908
+ version = "0.1.3"
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
File without changes