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.
- {undersort-0.1.5 → undersort-0.1.7}/.github/workflows/ci.yml +4 -6
- {undersort-0.1.5 → undersort-0.1.7}/PKG-INFO +169 -7
- undersort-0.1.7/README.md +445 -0
- {undersort-0.1.5 → undersort-0.1.7}/pyproject.toml +3 -5
- {undersort-0.1.5 → undersort-0.1.7}/tests/test_config.py +127 -7
- undersort-0.1.7/tests/test_dunder_order.py +201 -0
- undersort-0.1.7/tests/test_module_sorter.py +375 -0
- {undersort-0.1.5 → undersort-0.1.7}/tests/test_sorter.py +21 -0
- {undersort-0.1.5 → undersort-0.1.7}/undersort/__init__.py +1 -1
- undersort-0.1.7/undersort/config.py +177 -0
- undersort-0.1.7/undersort/deps.py +272 -0
- {undersort-0.1.5 → undersort-0.1.7}/undersort/main.py +57 -4
- undersort-0.1.7/undersort/sorter.py +692 -0
- undersort-0.1.7/uv.lock +451 -0
- undersort-0.1.5/README.md +0 -281
- undersort-0.1.5/undersort/config.py +0 -75
- undersort-0.1.5/undersort/sorter.py +0 -281
- undersort-0.1.5/uv.lock +0 -936
- {undersort-0.1.5 → undersort-0.1.7}/.gitignore +0 -0
- {undersort-0.1.5 → undersort-0.1.7}/.pre-commit-config.yaml +0 -0
- {undersort-0.1.5 → undersort-0.1.7}/.pre-commit-hooks.yaml +0 -0
- {undersort-0.1.5 → undersort-0.1.7}/.python-version +0 -0
- {undersort-0.1.5 → undersort-0.1.7}/.vscode/settings.json +0 -0
- {undersort-0.1.5 → undersort-0.1.7}/LICENSE +0 -0
- {undersort-0.1.5 → undersort-0.1.7}/Makefile +0 -0
- {undersort-0.1.5 → undersort-0.1.7}/tests/__init__.py +0 -0
- {undersort-0.1.5 → undersort-0.1.7}/tests/test_main.py +0 -0
- {undersort-0.1.5 → undersort-0.1.7}/tests/test_nosort.py +0 -0
- {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.
|
|
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
|
-
|
|
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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: undersort
|
|
3
|
-
Version: 0.1.
|
|
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.
|
|
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
|
-
#
|
|
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
|