undersort 0.1.5__tar.gz → 0.1.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 (29) hide show
  1. {undersort-0.1.5 → undersort-0.1.7}/.github/workflows/ci.yml +4 -6
  2. {undersort-0.1.5 → undersort-0.1.7}/PKG-INFO +169 -7
  3. undersort-0.1.7/README.md +445 -0
  4. {undersort-0.1.5 → undersort-0.1.7}/pyproject.toml +3 -5
  5. {undersort-0.1.5 → undersort-0.1.7}/tests/test_config.py +127 -7
  6. undersort-0.1.7/tests/test_dunder_order.py +201 -0
  7. undersort-0.1.7/tests/test_module_sorter.py +375 -0
  8. {undersort-0.1.5 → undersort-0.1.7}/tests/test_sorter.py +21 -0
  9. {undersort-0.1.5 → undersort-0.1.7}/undersort/__init__.py +1 -1
  10. undersort-0.1.7/undersort/config.py +177 -0
  11. undersort-0.1.7/undersort/deps.py +272 -0
  12. {undersort-0.1.5 → undersort-0.1.7}/undersort/main.py +57 -4
  13. undersort-0.1.7/undersort/sorter.py +692 -0
  14. undersort-0.1.7/uv.lock +451 -0
  15. undersort-0.1.5/README.md +0 -281
  16. undersort-0.1.5/undersort/config.py +0 -75
  17. undersort-0.1.5/undersort/sorter.py +0 -281
  18. undersort-0.1.5/uv.lock +0 -936
  19. {undersort-0.1.5 → undersort-0.1.7}/.gitignore +0 -0
  20. {undersort-0.1.5 → undersort-0.1.7}/.pre-commit-config.yaml +0 -0
  21. {undersort-0.1.5 → undersort-0.1.7}/.pre-commit-hooks.yaml +0 -0
  22. {undersort-0.1.5 → undersort-0.1.7}/.python-version +0 -0
  23. {undersort-0.1.5 → undersort-0.1.7}/.vscode/settings.json +0 -0
  24. {undersort-0.1.5 → undersort-0.1.7}/LICENSE +0 -0
  25. {undersort-0.1.5 → undersort-0.1.7}/Makefile +0 -0
  26. {undersort-0.1.5 → undersort-0.1.7}/tests/__init__.py +0 -0
  27. {undersort-0.1.5 → undersort-0.1.7}/tests/test_main.py +0 -0
  28. {undersort-0.1.5 → undersort-0.1.7}/tests/test_nosort.py +0 -0
  29. {undersort-0.1.5 → undersort-0.1.7}/undersort/logger.py +0 -0
@@ -39,7 +39,7 @@ jobs:
39
39
  runs-on: ubuntu-latest
40
40
  strategy:
41
41
  matrix:
42
- python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
42
+ python-version: ["3.11", "3.12", "3.13"]
43
43
  steps:
44
44
  - uses: actions/checkout@v4
45
45
 
@@ -49,15 +49,13 @@ jobs:
49
49
  enable-cache: true
50
50
 
51
51
  - name: Set up Python ${{ matrix.python-version }}
52
- uses: actions/setup-python@v5
53
- with:
54
- python-version: ${{ matrix.python-version }}
52
+ run: uv python install ${{ matrix.python-version }}
55
53
 
56
54
  - name: Install dependencies
57
- run: uv sync
55
+ run: uv sync --python ${{ matrix.python-version }}
58
56
 
59
57
  - name: Run tests
60
- run: uv run pytest -v --cov=undersort --cov-report=term-missing
58
+ run: uv run --python ${{ matrix.python-version }} pytest -v --cov=undersort --cov-report=term-missing
61
59
 
62
60
  publish:
63
61
  name: Publish to PyPI
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: undersort
3
- Version: 0.1.5
3
+ Version: 0.1.7
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
@@ -13,14 +13,12 @@ Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3.8
17
- Classifier: Programming Language :: Python :: 3.9
18
- Classifier: Programming Language :: Python :: 3.10
19
16
  Classifier: Programming Language :: Python :: 3.11
20
17
  Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
21
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
20
  Classifier: Topic :: Software Development :: Quality Assurance
23
- Requires-Python: >=3.8
21
+ Requires-Python: >=3.11
24
22
  Requires-Dist: libcst>=1.1.0
25
23
  Requires-Dist: rich>=13.0.0
26
24
  Description-Content-Type: text/markdown
@@ -33,6 +31,8 @@ A Python tool that automatically sorts class methods by visibility (public, prot
33
31
 
34
32
  - Automatically reorders class methods based on visibility and method type
35
33
  - Two-level sorting: primary by visibility, secondary by method type
34
+ - Optional module-level mode that sorts top-level functions and classes, with
35
+ dependency analysis so decorators, base classes, and annotations keep working
36
36
  - Fully configurable ordering via `pyproject.toml`
37
37
  - Pre-commit hook integration
38
38
  - Colored output for better readability
@@ -61,7 +61,9 @@ Configure the method ordering in your `pyproject.toml`:
61
61
  ```toml
62
62
  [tool.undersort]
63
63
  # Method visibility ordering (primary sort)
64
- # Options: "public", "protected", "private"
64
+ # Required groups: "public", "protected", "private"
65
+ # Optional groups: "init" (creational dunders), "dunder" (all other magic methods)
66
+ # Omit the optional groups to keep magic methods inside "public" (the default).
65
67
  order = ["public", "protected", "private"]
66
68
 
67
69
  # Method type ordering within each visibility level (secondary sort, optional)
@@ -72,6 +74,17 @@ method_type_order = ["instance", "class", "static"]
72
74
  # Exclude files/directories matching these patterns (optional)
73
75
  # Patterns support glob syntax (e.g., "tests/*", "migrations/*.py", "**/generated/*")
74
76
  # exclude = ["tests/*", "migrations/*.py"]
77
+
78
+ # Also sort module-level functions and classes (optional, default: false)
79
+ # sort_module_level = true
80
+
81
+ # Allow decorated module-level definitions to move (optional, default: false)
82
+ # Leave off unless your decorators have no order-dependent import-time side effects
83
+ # sort_decorated = true
84
+
85
+ # Target Python version, used to decide whether annotations are evaluated eagerly
86
+ # (optional; inferred from [project] requires-python when not set)
87
+ # python_version = "3.12"
75
88
  ```
76
89
 
77
90
  ### Method Visibility Rules
@@ -80,6 +93,46 @@ method_type_order = ["instance", "class", "static"]
80
93
  - **Protected methods**: Single underscore prefix (e.g., `def _method()`)
81
94
  - **Private methods**: Double underscore prefix, not magic (e.g., `def __method()`)
82
95
 
96
+ #### Separating dunder methods
97
+
98
+ By default magic methods count as **public**, which leaves `__init__` mixed in with
99
+ ordinary public methods. Add either of the two optional groups to `order` to pull
100
+ them out:
101
+
102
+ - **`init`** — creational dunders: `__new__`, `__init__`, `__init_subclass__`, `__post_init__`
103
+ - **`dunder`** — every other magic method: `__str__`, `__get__`, `__eq__`, ...
104
+
105
+ ```toml
106
+ [tool.undersort]
107
+ order = ["init", "dunder", "public", "protected", "private"]
108
+ method_type_order = ["static", "class", "instance"]
109
+ ```
110
+
111
+ ```python
112
+ # before # after
113
+ class C: class C:
114
+ @classmethod def __init__(self) -> None: ...
115
+ def default(cls): ...
116
+ def __str__(self) -> str: ...
117
+ def __init__(self) -> None: ...
118
+ @classmethod
119
+ def random_public(self): ... def default(cls): ...
120
+
121
+ def __str__(self) -> str: ... def random_public(self): ...
122
+ ```
123
+
124
+ The groups are positional, so `order = ["public", "protected", "private", "init", "dunder"]`
125
+ puts the magic methods last instead.
126
+
127
+ Notes:
128
+
129
+ - `public`, `protected` and `private` must always be present, so no method can be
130
+ silently dropped. `init` and `dunder` are optional.
131
+ - Using `init` without `dunder` leaves non-creational magic methods in `public`.
132
+ - Name-mangled methods (`__method`, no trailing underscores) remain **private** —
133
+ they are not dunders.
134
+ - An existing `order` that does not mention the new groups behaves exactly as before.
135
+
83
136
  ### Method Type Rules
84
137
 
85
138
  - **Class methods**: Decorated with `@classmethod`
@@ -111,6 +164,103 @@ Example order with default configuration:
111
164
  8. Private class methods
112
165
  9. Private static methods
113
166
 
167
+ ## Module-Level Sorting (optional)
168
+
169
+ By default undersort only reorders methods inside classes. Enable `sort_module_level`
170
+ to apply the same visibility ordering to top-level functions and classes:
171
+
172
+ ```toml
173
+ [tool.undersort]
174
+ sort_module_level = true
175
+ ```
176
+
177
+ Or from the command line: `undersort --sort-module-level src/` (use
178
+ `--no-sort-module-level` to override the config file for one run).
179
+
180
+ Classes and functions are ordered in a single stream by the same naming rules
181
+ (`public` → `protected` → `private`).
182
+
183
+ ### Safety Rules
184
+
185
+ Unlike methods in a class body, module-level definitions execute in order, so
186
+ reordering them can break a module. undersort only moves a definition when it is
187
+ provably safe:
188
+
189
+ **Non-definition statements are barriers.** Definitions are only reordered within
190
+ runs of consecutive `def`/`class` statements. An import, assignment, or `if` block
191
+ between them splits the file into independent runs, so nothing moves across it:
192
+
193
+ ```python
194
+ def _helper(): ...
195
+ def public_a(): ... # these two are sorted together
196
+
197
+ CONSTANT = compute() # barrier -- nothing crosses this line
198
+
199
+ def _other(): ...
200
+ def public_b(): ... # these two are sorted together
201
+ ```
202
+
203
+ **Definition-time references are respected.** A definition never moves above
204
+ something it needs when it is defined. That includes decorators, base classes and
205
+ `metaclass=` keywords, default argument values, and class bodies (which run
206
+ eagerly, though method bodies inside them do not):
207
+
208
+ ```python
209
+ class _Base: ...
210
+
211
+ class Public(_Base): # stays below _Base despite being public
212
+ ...
213
+ ```
214
+
215
+ References made inside a function or method body impose no constraint, since they
216
+ resolve when the function is called rather than when it is defined.
217
+
218
+ **Annotations are handled according to the effective evaluation semantics.**
219
+ Annotations are eagerly evaluated -- and therefore constrain ordering -- unless
220
+ either of the following applies, in which case annotated types are free to move:
221
+
222
+ - the module starts with `from __future__ import annotations` (PEP 563)
223
+ - the target Python version is 3.14 or newer, where annotations are lazy by
224
+ default (PEP 649)
225
+
226
+ The target version comes from `python_version`, or the project's
227
+ `requires-python` lower bound, or the running interpreter, in that order.
228
+
229
+ **Redefined names never move.** If a name is defined more than once at the top
230
+ level (`@overload` blocks, `@singledispatch` registrations, conditional
231
+ redefinitions), all of its definitions stay put.
232
+
233
+ **Decorated definitions are pinned by default.** A decorator runs at import time
234
+ and can have side effects whose *order* matters — `@app.route`, `@cli.command`,
235
+ `@register` and friends append to a registry as the module loads. No static
236
+ analysis can tell a registering decorator from a pure one, so decorated
237
+ definitions keep their position unless you opt in:
238
+
239
+ ```toml
240
+ [tool.undersort]
241
+ sort_decorated = true # only if you know your decorators are order-independent
242
+ ```
243
+
244
+ **`# nosort` still applies**, both file-level and on individual definitions.
245
+ Note that at module level a pinned definition acts as a hard anchor: nothing is
246
+ reordered across it. This is stricter than the class-method behaviour, because
247
+ top-level statements execute in order.
248
+
249
+ ### Known Limitation
250
+
251
+ undersort guarantees that reordering never breaks *name resolution* — nothing
252
+ moves above a name it needs at definition time. It cannot fully guarantee
253
+ *side-effect order*. Pinning decorated definitions covers the common registry
254
+ pattern, but two cases remain outside static reach:
255
+
256
+ - classes whose creation has side effects through a base class defined in another
257
+ module (`__init_subclass__` hooks, registering metaclasses)
258
+ - any definition whose mere position is load-bearing for reasons not visible in
259
+ the file
260
+
261
+ If your module has import-time ordering semantics like these, mark the affected
262
+ definitions with `# nosort`, or leave `sort_module_level` off for that file.
263
+
114
264
  ### Skipping Sorting with `# nosort`
115
265
 
116
266
  You can prevent sorting at different levels using `# nosort` comments (case-insensitive):
@@ -195,6 +345,18 @@ undersort --exclude "tests/*" --exclude "migrations/*.py" src/
195
345
 
196
346
  # Multiple exclude patterns (can be combined with config file patterns)
197
347
  undersort --exclude "test_*.py" --exclude "*/legacy/*" .
348
+
349
+ # Also sort module-level functions and classes
350
+ undersort --sort-module-level src/
351
+
352
+ # Override the config file for a single run
353
+ undersort --no-sort-module-level src/
354
+
355
+ # Tell undersort which Python version to assume for annotation semantics
356
+ undersort --sort-module-level --python-version 3.14 src/
357
+
358
+ # Also reorder decorated definitions (off by default, see Known Limitation)
359
+ undersort --sort-module-level --sort-decorated src/
198
360
  ```
199
361
 
200
362
  **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. You can add custom exclusions via CLI flags or the config file.
@@ -0,0 +1,445 @@
1
+ # undersort
2
+
3
+ A Python tool that automatically sorts class methods by visibility (public, protected, private) and type (class, static, instance).
4
+
5
+ ## Features
6
+
7
+ - Automatically reorders class methods based on visibility and method type
8
+ - Two-level sorting: primary by visibility, secondary by method type
9
+ - Optional module-level mode that sorts top-level functions and classes, with
10
+ dependency analysis so decorators, base classes, and annotations keep working
11
+ - Fully configurable ordering via `pyproject.toml`
12
+ - Pre-commit hook integration
13
+ - Colored output for better readability
14
+ - Check mode for CI/CD validation
15
+ - Diff mode to preview changes
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ # Using uv (recommended)
21
+ uv add undersort
22
+
23
+ # Using pip
24
+ pip install undersort
25
+
26
+ # For development
27
+ git clone https://github.com/kivicode/undersort
28
+ cd undersort
29
+ uv sync
30
+ ```
31
+
32
+ ## Configuration
33
+
34
+ Configure the method ordering in your `pyproject.toml`:
35
+
36
+ ```toml
37
+ [tool.undersort]
38
+ # Method visibility ordering (primary sort)
39
+ # Required groups: "public", "protected", "private"
40
+ # Optional groups: "init" (creational dunders), "dunder" (all other magic methods)
41
+ # Omit the optional groups to keep magic methods inside "public" (the default).
42
+ order = ["public", "protected", "private"]
43
+
44
+ # Method type ordering within each visibility level (secondary sort, optional)
45
+ # Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
46
+ # Default: ["instance", "class", "static"]
47
+ method_type_order = ["instance", "class", "static"]
48
+
49
+ # Exclude files/directories matching these patterns (optional)
50
+ # Patterns support glob syntax (e.g., "tests/*", "migrations/*.py", "**/generated/*")
51
+ # exclude = ["tests/*", "migrations/*.py"]
52
+
53
+ # Also sort module-level functions and classes (optional, default: false)
54
+ # sort_module_level = true
55
+
56
+ # Allow decorated module-level definitions to move (optional, default: false)
57
+ # Leave off unless your decorators have no order-dependent import-time side effects
58
+ # sort_decorated = true
59
+
60
+ # Target Python version, used to decide whether annotations are evaluated eagerly
61
+ # (optional; inferred from [project] requires-python when not set)
62
+ # python_version = "3.12"
63
+ ```
64
+
65
+ ### Method Visibility Rules
66
+
67
+ - **Public methods**: No underscore prefix (e.g., `def method()`) or magic methods (e.g., `__init__`, `__str__`)
68
+ - **Protected methods**: Single underscore prefix (e.g., `def _method()`)
69
+ - **Private methods**: Double underscore prefix, not magic (e.g., `def __method()`)
70
+
71
+ #### Separating dunder methods
72
+
73
+ By default magic methods count as **public**, which leaves `__init__` mixed in with
74
+ ordinary public methods. Add either of the two optional groups to `order` to pull
75
+ them out:
76
+
77
+ - **`init`** — creational dunders: `__new__`, `__init__`, `__init_subclass__`, `__post_init__`
78
+ - **`dunder`** — every other magic method: `__str__`, `__get__`, `__eq__`, ...
79
+
80
+ ```toml
81
+ [tool.undersort]
82
+ order = ["init", "dunder", "public", "protected", "private"]
83
+ method_type_order = ["static", "class", "instance"]
84
+ ```
85
+
86
+ ```python
87
+ # before # after
88
+ class C: class C:
89
+ @classmethod def __init__(self) -> None: ...
90
+ def default(cls): ...
91
+ def __str__(self) -> str: ...
92
+ def __init__(self) -> None: ...
93
+ @classmethod
94
+ def random_public(self): ... def default(cls): ...
95
+
96
+ def __str__(self) -> str: ... def random_public(self): ...
97
+ ```
98
+
99
+ The groups are positional, so `order = ["public", "protected", "private", "init", "dunder"]`
100
+ puts the magic methods last instead.
101
+
102
+ Notes:
103
+
104
+ - `public`, `protected` and `private` must always be present, so no method can be
105
+ silently dropped. `init` and `dunder` are optional.
106
+ - Using `init` without `dunder` leaves non-creational magic methods in `public`.
107
+ - Name-mangled methods (`__method`, no trailing underscores) remain **private** —
108
+ they are not dunders.
109
+ - An existing `order` that does not mention the new groups behaves exactly as before.
110
+
111
+ ### Method Type Rules
112
+
113
+ - **Class methods**: Decorated with `@classmethod`
114
+ - **Static methods**: Decorated with `@staticmethod`
115
+ - **Instance methods**: Regular methods (no special decorator)
116
+
117
+ ### Sorting Behavior
118
+
119
+ Methods are sorted in two levels:
120
+
121
+ 1. **Primary**: By visibility (public → protected → private)
122
+ 2. **Secondary**: Within each visibility level, by method type (instance → class → static by default)
123
+
124
+ The sorting algorithm **minimizes movement** to preserve the original order as much as possible:
125
+
126
+ - Methods that need to move DOWN (to a later section) are placed at the **beginning** of their target section
127
+ - Methods that need to move UP (to an earlier section) are placed at the **end** of their target section
128
+ - Methods already in the correct section maintain their relative order
129
+
130
+ Example order with default configuration:
131
+
132
+ 1. Public instance methods
133
+ 2. Public class methods
134
+ 3. Public static methods
135
+ 4. Protected instance methods
136
+ 5. Protected class methods
137
+ 6. Protected static methods
138
+ 7. Private instance methods
139
+ 8. Private class methods
140
+ 9. Private static methods
141
+
142
+ ## Module-Level Sorting (optional)
143
+
144
+ By default undersort only reorders methods inside classes. Enable `sort_module_level`
145
+ to apply the same visibility ordering to top-level functions and classes:
146
+
147
+ ```toml
148
+ [tool.undersort]
149
+ sort_module_level = true
150
+ ```
151
+
152
+ Or from the command line: `undersort --sort-module-level src/` (use
153
+ `--no-sort-module-level` to override the config file for one run).
154
+
155
+ Classes and functions are ordered in a single stream by the same naming rules
156
+ (`public` → `protected` → `private`).
157
+
158
+ ### Safety Rules
159
+
160
+ Unlike methods in a class body, module-level definitions execute in order, so
161
+ reordering them can break a module. undersort only moves a definition when it is
162
+ provably safe:
163
+
164
+ **Non-definition statements are barriers.** Definitions are only reordered within
165
+ runs of consecutive `def`/`class` statements. An import, assignment, or `if` block
166
+ between them splits the file into independent runs, so nothing moves across it:
167
+
168
+ ```python
169
+ def _helper(): ...
170
+ def public_a(): ... # these two are sorted together
171
+
172
+ CONSTANT = compute() # barrier -- nothing crosses this line
173
+
174
+ def _other(): ...
175
+ def public_b(): ... # these two are sorted together
176
+ ```
177
+
178
+ **Definition-time references are respected.** A definition never moves above
179
+ something it needs when it is defined. That includes decorators, base classes and
180
+ `metaclass=` keywords, default argument values, and class bodies (which run
181
+ eagerly, though method bodies inside them do not):
182
+
183
+ ```python
184
+ class _Base: ...
185
+
186
+ class Public(_Base): # stays below _Base despite being public
187
+ ...
188
+ ```
189
+
190
+ References made inside a function or method body impose no constraint, since they
191
+ resolve when the function is called rather than when it is defined.
192
+
193
+ **Annotations are handled according to the effective evaluation semantics.**
194
+ Annotations are eagerly evaluated -- and therefore constrain ordering -- unless
195
+ either of the following applies, in which case annotated types are free to move:
196
+
197
+ - the module starts with `from __future__ import annotations` (PEP 563)
198
+ - the target Python version is 3.14 or newer, where annotations are lazy by
199
+ default (PEP 649)
200
+
201
+ The target version comes from `python_version`, or the project's
202
+ `requires-python` lower bound, or the running interpreter, in that order.
203
+
204
+ **Redefined names never move.** If a name is defined more than once at the top
205
+ level (`@overload` blocks, `@singledispatch` registrations, conditional
206
+ redefinitions), all of its definitions stay put.
207
+
208
+ **Decorated definitions are pinned by default.** A decorator runs at import time
209
+ and can have side effects whose *order* matters — `@app.route`, `@cli.command`,
210
+ `@register` and friends append to a registry as the module loads. No static
211
+ analysis can tell a registering decorator from a pure one, so decorated
212
+ definitions keep their position unless you opt in:
213
+
214
+ ```toml
215
+ [tool.undersort]
216
+ sort_decorated = true # only if you know your decorators are order-independent
217
+ ```
218
+
219
+ **`# nosort` still applies**, both file-level and on individual definitions.
220
+ Note that at module level a pinned definition acts as a hard anchor: nothing is
221
+ reordered across it. This is stricter than the class-method behaviour, because
222
+ top-level statements execute in order.
223
+
224
+ ### Known Limitation
225
+
226
+ undersort guarantees that reordering never breaks *name resolution* — nothing
227
+ moves above a name it needs at definition time. It cannot fully guarantee
228
+ *side-effect order*. Pinning decorated definitions covers the common registry
229
+ pattern, but two cases remain outside static reach:
230
+
231
+ - classes whose creation has side effects through a base class defined in another
232
+ module (`__init_subclass__` hooks, registering metaclasses)
233
+ - any definition whose mere position is load-bearing for reasons not visible in
234
+ the file
235
+
236
+ If your module has import-time ordering semantics like these, mark the affected
237
+ definitions with `# nosort`, or leave `sort_module_level` off for that file.
238
+
239
+ ### Skipping Sorting with `# nosort`
240
+
241
+ You can prevent sorting at different levels using `# nosort` comments (case-insensitive):
242
+
243
+ **File-level**: Skip entire file
244
+
245
+ ```python
246
+ # nosort: file
247
+ class Example:
248
+ def _protected(self):
249
+ pass
250
+ def public(self):
251
+ pass # File won't be sorted
252
+ ```
253
+
254
+ **Class-level**: Skip specific class
255
+
256
+ ```python
257
+ class Example: # nosort
258
+ def _protected(self):
259
+ pass
260
+ def public(self):
261
+ pass # This class won't be sorted
262
+
263
+ class Other:
264
+ def _protected(self):
265
+ pass
266
+ def public(self):
267
+ pass # This class WILL be sorted
268
+ ```
269
+
270
+ **Method-level**: Keep method in its current position
271
+
272
+ ```python
273
+ class Example:
274
+ def public_a(self):
275
+ pass
276
+
277
+ def _protected(self): # nosort
278
+ pass # Stays here, between public methods
279
+
280
+ def public_b(self):
281
+ pass # Will move up, but _protected stays in place
282
+ ```
283
+
284
+ ## Usage
285
+
286
+ ### Command Line
287
+
288
+ ```bash
289
+ # Sort a single file
290
+ undersort example.py
291
+
292
+ # Sort multiple files
293
+ undersort file1.py file2.py file3.py
294
+
295
+ # Sort all Python files in a directory (recursive by default)
296
+ undersort src/
297
+
298
+ # Sort all Python files in current directory and subdirectories
299
+ undersort .
300
+
301
+ # Non-recursive directory sorting (only files in the directory, not subdirectories)
302
+ undersort src/ --no-recursive
303
+
304
+ # Wildcards work too (expanded by shell)
305
+ undersort *.py
306
+ undersort src/**/*.py
307
+
308
+ # Check if files need sorting (useful for CI)
309
+ undersort --check example.py
310
+ undersort --check src/
311
+
312
+ # Show diff of changes
313
+ undersort --diff example.py
314
+
315
+ # Combine flags
316
+ undersort --check --diff src/
317
+
318
+ # Exclude specific files or directories
319
+ undersort --exclude "tests/*" --exclude "migrations/*.py" src/
320
+
321
+ # Multiple exclude patterns (can be combined with config file patterns)
322
+ undersort --exclude "test_*.py" --exclude "*/legacy/*" .
323
+
324
+ # Also sort module-level functions and classes
325
+ undersort --sort-module-level src/
326
+
327
+ # Override the config file for a single run
328
+ undersort --no-sort-module-level src/
329
+
330
+ # Tell undersort which Python version to assume for annotation semantics
331
+ undersort --sort-module-level --python-version 3.14 src/
332
+
333
+ # Also reorder decorated definitions (off by default, see Known Limitation)
334
+ undersort --sort-module-level --sort-decorated src/
335
+ ```
336
+
337
+ **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. You can add custom exclusions via CLI flags or the config file.
338
+
339
+ ### Pre-commit Integration
340
+
341
+ Add to your `.pre-commit-config.yaml`:
342
+
343
+ ```yaml
344
+ repos:
345
+ - repo: local
346
+ hooks:
347
+ - id: undersort
348
+ name: undersort
349
+ entry: undersort
350
+ language: python
351
+ types: [python]
352
+ additional_dependencies: ["undersort"]
353
+ ```
354
+
355
+ Then install the hook:
356
+
357
+ ```bash
358
+ pip install pre-commit
359
+ pre-commit install
360
+ ```
361
+
362
+ ## Example
363
+
364
+ ### Before
365
+
366
+ ```python
367
+ class Example:
368
+ def _protected_instance(self):
369
+ pass
370
+
371
+ @staticmethod
372
+ def public_static():
373
+ pass
374
+
375
+ def __init__(self):
376
+ pass
377
+
378
+ @classmethod
379
+ def _protected_class(cls):
380
+ pass
381
+
382
+ def public_instance(self):
383
+ pass
384
+
385
+ def __private_method(self):
386
+ pass
387
+
388
+ @classmethod
389
+ def public_class(cls):
390
+ pass
391
+ ```
392
+
393
+ ### After (with default config)
394
+
395
+ ```python
396
+ class Example:
397
+ def __init__(self):
398
+ pass
399
+
400
+ def public_instance(self):
401
+ pass
402
+
403
+ @classmethod
404
+ def public_class(cls):
405
+ pass
406
+
407
+ @staticmethod
408
+ def public_static():
409
+ pass
410
+
411
+ def _protected_instance(self):
412
+ pass
413
+
414
+ @classmethod
415
+ def _protected_class(cls):
416
+ pass
417
+
418
+ def __private_method(self):
419
+ pass
420
+ ```
421
+
422
+ The methods are now organized by:
423
+
424
+ 1. **Visibility**: public (including `__init__`) → protected → private
425
+ 2. **Type** (within each visibility): instance → class → static
426
+
427
+ ## Development
428
+
429
+ ```bash
430
+ # Install dependencies
431
+ uv sync
432
+
433
+ # Run on example file
434
+ uv run undersort example.py
435
+
436
+ # Test with check mode
437
+ uv run undersort --check example.py
438
+
439
+ # View diff
440
+ uv run undersort --diff example.py
441
+ ```
442
+
443
+ ## License
444
+
445
+ MIT