undersort 0.1.4__tar.gz → 0.1.6__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.4 → undersort-0.1.6}/PKG-INFO +134 -2
- {undersort-0.1.4 → undersort-0.1.6}/README.md +133 -1
- {undersort-0.1.4 → undersort-0.1.6}/pyproject.toml +5 -1
- undersort-0.1.6/tests/test_config.py +271 -0
- {undersort-0.1.4 → undersort-0.1.6}/tests/test_main.py +72 -0
- undersort-0.1.6/tests/test_module_sorter.py +375 -0
- undersort-0.1.6/undersort/config.py +144 -0
- undersort-0.1.6/undersort/deps.py +253 -0
- undersort-0.1.6/undersort/main.py +230 -0
- undersort-0.1.6/undersort/sorter.py +602 -0
- {undersort-0.1.4 → undersort-0.1.6}/uv.lock +1 -1
- undersort-0.1.4/tests/test_config.py +0 -115
- undersort-0.1.4/undersort/config.py +0 -67
- undersort-0.1.4/undersort/main.py +0 -128
- undersort-0.1.4/undersort/sorter.py +0 -281
- {undersort-0.1.4 → undersort-0.1.6}/.github/workflows/ci.yml +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/.gitignore +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/.pre-commit-config.yaml +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/.pre-commit-hooks.yaml +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/.python-version +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/.vscode/settings.json +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/LICENSE +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/Makefile +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/tests/__init__.py +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/tests/test_nosort.py +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/tests/test_sorter.py +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/undersort/__init__.py +0 -0
- {undersort-0.1.4 → undersort-0.1.6}/undersort/logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: undersort
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
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
|
|
@@ -33,6 +33,8 @@ A Python tool that automatically sorts class methods by visibility (public, prot
|
|
|
33
33
|
|
|
34
34
|
- Automatically reorders class methods based on visibility and method type
|
|
35
35
|
- Two-level sorting: primary by visibility, secondary by method type
|
|
36
|
+
- Optional module-level mode that sorts top-level functions and classes, with
|
|
37
|
+
dependency analysis so decorators, base classes, and annotations keep working
|
|
36
38
|
- Fully configurable ordering via `pyproject.toml`
|
|
37
39
|
- Pre-commit hook integration
|
|
38
40
|
- Colored output for better readability
|
|
@@ -68,6 +70,21 @@ order = ["public", "protected", "private"]
|
|
|
68
70
|
# Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
|
|
69
71
|
# Default: ["instance", "class", "static"]
|
|
70
72
|
method_type_order = ["instance", "class", "static"]
|
|
73
|
+
|
|
74
|
+
# Exclude files/directories matching these patterns (optional)
|
|
75
|
+
# Patterns support glob syntax (e.g., "tests/*", "migrations/*.py", "**/generated/*")
|
|
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"
|
|
71
88
|
```
|
|
72
89
|
|
|
73
90
|
### Method Visibility Rules
|
|
@@ -107,6 +124,103 @@ Example order with default configuration:
|
|
|
107
124
|
8. Private class methods
|
|
108
125
|
9. Private static methods
|
|
109
126
|
|
|
127
|
+
## Module-Level Sorting (optional)
|
|
128
|
+
|
|
129
|
+
By default undersort only reorders methods inside classes. Enable `sort_module_level`
|
|
130
|
+
to apply the same visibility ordering to top-level functions and classes:
|
|
131
|
+
|
|
132
|
+
```toml
|
|
133
|
+
[tool.undersort]
|
|
134
|
+
sort_module_level = true
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Or from the command line: `undersort --sort-module-level src/` (use
|
|
138
|
+
`--no-sort-module-level` to override the config file for one run).
|
|
139
|
+
|
|
140
|
+
Classes and functions are ordered in a single stream by the same naming rules
|
|
141
|
+
(`public` → `protected` → `private`).
|
|
142
|
+
|
|
143
|
+
### Safety Rules
|
|
144
|
+
|
|
145
|
+
Unlike methods in a class body, module-level definitions execute in order, so
|
|
146
|
+
reordering them can break a module. undersort only moves a definition when it is
|
|
147
|
+
provably safe:
|
|
148
|
+
|
|
149
|
+
**Non-definition statements are barriers.** Definitions are only reordered within
|
|
150
|
+
runs of consecutive `def`/`class` statements. An import, assignment, or `if` block
|
|
151
|
+
between them splits the file into independent runs, so nothing moves across it:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
def _helper(): ...
|
|
155
|
+
def public_a(): ... # these two are sorted together
|
|
156
|
+
|
|
157
|
+
CONSTANT = compute() # barrier -- nothing crosses this line
|
|
158
|
+
|
|
159
|
+
def _other(): ...
|
|
160
|
+
def public_b(): ... # these two are sorted together
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Definition-time references are respected.** A definition never moves above
|
|
164
|
+
something it needs when it is defined. That includes decorators, base classes and
|
|
165
|
+
`metaclass=` keywords, default argument values, and class bodies (which run
|
|
166
|
+
eagerly, though method bodies inside them do not):
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
class _Base: ...
|
|
170
|
+
|
|
171
|
+
class Public(_Base): # stays below _Base despite being public
|
|
172
|
+
...
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
References made inside a function or method body impose no constraint, since they
|
|
176
|
+
resolve when the function is called rather than when it is defined.
|
|
177
|
+
|
|
178
|
+
**Annotations are handled according to the effective evaluation semantics.**
|
|
179
|
+
Annotations are eagerly evaluated -- and therefore constrain ordering -- unless
|
|
180
|
+
either of the following applies, in which case annotated types are free to move:
|
|
181
|
+
|
|
182
|
+
- the module starts with `from __future__ import annotations` (PEP 563)
|
|
183
|
+
- the target Python version is 3.14 or newer, where annotations are lazy by
|
|
184
|
+
default (PEP 649)
|
|
185
|
+
|
|
186
|
+
The target version comes from `python_version`, or the project's
|
|
187
|
+
`requires-python` lower bound, or the running interpreter, in that order.
|
|
188
|
+
|
|
189
|
+
**Redefined names never move.** If a name is defined more than once at the top
|
|
190
|
+
level (`@overload` blocks, `@singledispatch` registrations, conditional
|
|
191
|
+
redefinitions), all of its definitions stay put.
|
|
192
|
+
|
|
193
|
+
**Decorated definitions are pinned by default.** A decorator runs at import time
|
|
194
|
+
and can have side effects whose *order* matters — `@app.route`, `@cli.command`,
|
|
195
|
+
`@register` and friends append to a registry as the module loads. No static
|
|
196
|
+
analysis can tell a registering decorator from a pure one, so decorated
|
|
197
|
+
definitions keep their position unless you opt in:
|
|
198
|
+
|
|
199
|
+
```toml
|
|
200
|
+
[tool.undersort]
|
|
201
|
+
sort_decorated = true # only if you know your decorators are order-independent
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**`# nosort` still applies**, both file-level and on individual definitions.
|
|
205
|
+
Note that at module level a pinned definition acts as a hard anchor: nothing is
|
|
206
|
+
reordered across it. This is stricter than the class-method behaviour, because
|
|
207
|
+
top-level statements execute in order.
|
|
208
|
+
|
|
209
|
+
### Known Limitation
|
|
210
|
+
|
|
211
|
+
undersort guarantees that reordering never breaks *name resolution* — nothing
|
|
212
|
+
moves above a name it needs at definition time. It cannot fully guarantee
|
|
213
|
+
*side-effect order*. Pinning decorated definitions covers the common registry
|
|
214
|
+
pattern, but two cases remain outside static reach:
|
|
215
|
+
|
|
216
|
+
- classes whose creation has side effects through a base class defined in another
|
|
217
|
+
module (`__init_subclass__` hooks, registering metaclasses)
|
|
218
|
+
- any definition whose mere position is load-bearing for reasons not visible in
|
|
219
|
+
the file
|
|
220
|
+
|
|
221
|
+
If your module has import-time ordering semantics like these, mark the affected
|
|
222
|
+
definitions with `# nosort`, or leave `sort_module_level` off for that file.
|
|
223
|
+
|
|
110
224
|
### Skipping Sorting with `# nosort`
|
|
111
225
|
|
|
112
226
|
You can prevent sorting at different levels using `# nosort` comments (case-insensitive):
|
|
@@ -185,9 +299,27 @@ undersort --diff example.py
|
|
|
185
299
|
|
|
186
300
|
# Combine flags
|
|
187
301
|
undersort --check --diff src/
|
|
302
|
+
|
|
303
|
+
# Exclude specific files or directories
|
|
304
|
+
undersort --exclude "tests/*" --exclude "migrations/*.py" src/
|
|
305
|
+
|
|
306
|
+
# Multiple exclude patterns (can be combined with config file patterns)
|
|
307
|
+
undersort --exclude "test_*.py" --exclude "*/legacy/*" .
|
|
308
|
+
|
|
309
|
+
# Also sort module-level functions and classes
|
|
310
|
+
undersort --sort-module-level src/
|
|
311
|
+
|
|
312
|
+
# Override the config file for a single run
|
|
313
|
+
undersort --no-sort-module-level src/
|
|
314
|
+
|
|
315
|
+
# Tell undersort which Python version to assume for annotation semantics
|
|
316
|
+
undersort --sort-module-level --python-version 3.14 src/
|
|
317
|
+
|
|
318
|
+
# Also reorder decorated definitions (off by default, see Known Limitation)
|
|
319
|
+
undersort --sort-module-level --sort-decorated src/
|
|
188
320
|
```
|
|
189
321
|
|
|
190
|
-
**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.
|
|
322
|
+
**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.
|
|
191
323
|
|
|
192
324
|
### Pre-commit Integration
|
|
193
325
|
|
|
@@ -6,6 +6,8 @@ A Python tool that automatically sorts class methods by visibility (public, prot
|
|
|
6
6
|
|
|
7
7
|
- Automatically reorders class methods based on visibility and method type
|
|
8
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
|
|
9
11
|
- Fully configurable ordering via `pyproject.toml`
|
|
10
12
|
- Pre-commit hook integration
|
|
11
13
|
- Colored output for better readability
|
|
@@ -41,6 +43,21 @@ order = ["public", "protected", "private"]
|
|
|
41
43
|
# Options: "class" (classmethod), "static" (staticmethod), "instance" (regular methods)
|
|
42
44
|
# Default: ["instance", "class", "static"]
|
|
43
45
|
method_type_order = ["instance", "class", "static"]
|
|
46
|
+
|
|
47
|
+
# Exclude files/directories matching these patterns (optional)
|
|
48
|
+
# Patterns support glob syntax (e.g., "tests/*", "migrations/*.py", "**/generated/*")
|
|
49
|
+
# exclude = ["tests/*", "migrations/*.py"]
|
|
50
|
+
|
|
51
|
+
# Also sort module-level functions and classes (optional, default: false)
|
|
52
|
+
# sort_module_level = true
|
|
53
|
+
|
|
54
|
+
# Allow decorated module-level definitions to move (optional, default: false)
|
|
55
|
+
# Leave off unless your decorators have no order-dependent import-time side effects
|
|
56
|
+
# sort_decorated = true
|
|
57
|
+
|
|
58
|
+
# Target Python version, used to decide whether annotations are evaluated eagerly
|
|
59
|
+
# (optional; inferred from [project] requires-python when not set)
|
|
60
|
+
# python_version = "3.12"
|
|
44
61
|
```
|
|
45
62
|
|
|
46
63
|
### Method Visibility Rules
|
|
@@ -80,6 +97,103 @@ Example order with default configuration:
|
|
|
80
97
|
8. Private class methods
|
|
81
98
|
9. Private static methods
|
|
82
99
|
|
|
100
|
+
## Module-Level Sorting (optional)
|
|
101
|
+
|
|
102
|
+
By default undersort only reorders methods inside classes. Enable `sort_module_level`
|
|
103
|
+
to apply the same visibility ordering to top-level functions and classes:
|
|
104
|
+
|
|
105
|
+
```toml
|
|
106
|
+
[tool.undersort]
|
|
107
|
+
sort_module_level = true
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or from the command line: `undersort --sort-module-level src/` (use
|
|
111
|
+
`--no-sort-module-level` to override the config file for one run).
|
|
112
|
+
|
|
113
|
+
Classes and functions are ordered in a single stream by the same naming rules
|
|
114
|
+
(`public` → `protected` → `private`).
|
|
115
|
+
|
|
116
|
+
### Safety Rules
|
|
117
|
+
|
|
118
|
+
Unlike methods in a class body, module-level definitions execute in order, so
|
|
119
|
+
reordering them can break a module. undersort only moves a definition when it is
|
|
120
|
+
provably safe:
|
|
121
|
+
|
|
122
|
+
**Non-definition statements are barriers.** Definitions are only reordered within
|
|
123
|
+
runs of consecutive `def`/`class` statements. An import, assignment, or `if` block
|
|
124
|
+
between them splits the file into independent runs, so nothing moves across it:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
def _helper(): ...
|
|
128
|
+
def public_a(): ... # these two are sorted together
|
|
129
|
+
|
|
130
|
+
CONSTANT = compute() # barrier -- nothing crosses this line
|
|
131
|
+
|
|
132
|
+
def _other(): ...
|
|
133
|
+
def public_b(): ... # these two are sorted together
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Definition-time references are respected.** A definition never moves above
|
|
137
|
+
something it needs when it is defined. That includes decorators, base classes and
|
|
138
|
+
`metaclass=` keywords, default argument values, and class bodies (which run
|
|
139
|
+
eagerly, though method bodies inside them do not):
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
class _Base: ...
|
|
143
|
+
|
|
144
|
+
class Public(_Base): # stays below _Base despite being public
|
|
145
|
+
...
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
References made inside a function or method body impose no constraint, since they
|
|
149
|
+
resolve when the function is called rather than when it is defined.
|
|
150
|
+
|
|
151
|
+
**Annotations are handled according to the effective evaluation semantics.**
|
|
152
|
+
Annotations are eagerly evaluated -- and therefore constrain ordering -- unless
|
|
153
|
+
either of the following applies, in which case annotated types are free to move:
|
|
154
|
+
|
|
155
|
+
- the module starts with `from __future__ import annotations` (PEP 563)
|
|
156
|
+
- the target Python version is 3.14 or newer, where annotations are lazy by
|
|
157
|
+
default (PEP 649)
|
|
158
|
+
|
|
159
|
+
The target version comes from `python_version`, or the project's
|
|
160
|
+
`requires-python` lower bound, or the running interpreter, in that order.
|
|
161
|
+
|
|
162
|
+
**Redefined names never move.** If a name is defined more than once at the top
|
|
163
|
+
level (`@overload` blocks, `@singledispatch` registrations, conditional
|
|
164
|
+
redefinitions), all of its definitions stay put.
|
|
165
|
+
|
|
166
|
+
**Decorated definitions are pinned by default.** A decorator runs at import time
|
|
167
|
+
and can have side effects whose *order* matters — `@app.route`, `@cli.command`,
|
|
168
|
+
`@register` and friends append to a registry as the module loads. No static
|
|
169
|
+
analysis can tell a registering decorator from a pure one, so decorated
|
|
170
|
+
definitions keep their position unless you opt in:
|
|
171
|
+
|
|
172
|
+
```toml
|
|
173
|
+
[tool.undersort]
|
|
174
|
+
sort_decorated = true # only if you know your decorators are order-independent
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**`# nosort` still applies**, both file-level and on individual definitions.
|
|
178
|
+
Note that at module level a pinned definition acts as a hard anchor: nothing is
|
|
179
|
+
reordered across it. This is stricter than the class-method behaviour, because
|
|
180
|
+
top-level statements execute in order.
|
|
181
|
+
|
|
182
|
+
### Known Limitation
|
|
183
|
+
|
|
184
|
+
undersort guarantees that reordering never breaks *name resolution* — nothing
|
|
185
|
+
moves above a name it needs at definition time. It cannot fully guarantee
|
|
186
|
+
*side-effect order*. Pinning decorated definitions covers the common registry
|
|
187
|
+
pattern, but two cases remain outside static reach:
|
|
188
|
+
|
|
189
|
+
- classes whose creation has side effects through a base class defined in another
|
|
190
|
+
module (`__init_subclass__` hooks, registering metaclasses)
|
|
191
|
+
- any definition whose mere position is load-bearing for reasons not visible in
|
|
192
|
+
the file
|
|
193
|
+
|
|
194
|
+
If your module has import-time ordering semantics like these, mark the affected
|
|
195
|
+
definitions with `# nosort`, or leave `sort_module_level` off for that file.
|
|
196
|
+
|
|
83
197
|
### Skipping Sorting with `# nosort`
|
|
84
198
|
|
|
85
199
|
You can prevent sorting at different levels using `# nosort` comments (case-insensitive):
|
|
@@ -158,9 +272,27 @@ undersort --diff example.py
|
|
|
158
272
|
|
|
159
273
|
# Combine flags
|
|
160
274
|
undersort --check --diff src/
|
|
275
|
+
|
|
276
|
+
# Exclude specific files or directories
|
|
277
|
+
undersort --exclude "tests/*" --exclude "migrations/*.py" src/
|
|
278
|
+
|
|
279
|
+
# Multiple exclude patterns (can be combined with config file patterns)
|
|
280
|
+
undersort --exclude "test_*.py" --exclude "*/legacy/*" .
|
|
281
|
+
|
|
282
|
+
# Also sort module-level functions and classes
|
|
283
|
+
undersort --sort-module-level src/
|
|
284
|
+
|
|
285
|
+
# Override the config file for a single run
|
|
286
|
+
undersort --no-sort-module-level src/
|
|
287
|
+
|
|
288
|
+
# Tell undersort which Python version to assume for annotation semantics
|
|
289
|
+
undersort --sort-module-level --python-version 3.14 src/
|
|
290
|
+
|
|
291
|
+
# Also reorder decorated definitions (off by default, see Known Limitation)
|
|
292
|
+
undersort --sort-module-level --sort-decorated src/
|
|
161
293
|
```
|
|
162
294
|
|
|
163
|
-
**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.
|
|
295
|
+
**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.
|
|
164
296
|
|
|
165
297
|
### Pre-commit Integration
|
|
166
298
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "undersort"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.6"
|
|
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"
|
|
@@ -50,6 +50,10 @@ order = ["public", "protected", "private"]
|
|
|
50
50
|
# Default order if not specified: ["instance", "class", "static"]
|
|
51
51
|
method_type_order = ["instance", "class", "static"]
|
|
52
52
|
|
|
53
|
+
# Exclude files/directories matching these patterns (optional)
|
|
54
|
+
# Patterns support glob syntax (e.g., "tests/*", "migrations/*.py", "**/generated/*")
|
|
55
|
+
# exclude = ["tests/*", "migrations/*.py"]
|
|
56
|
+
|
|
53
57
|
[tool.ruff]
|
|
54
58
|
unsafe-fixes = true
|
|
55
59
|
exclude = [".venv"]
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Tests for configuration loading."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from undersort.config import _find_pyproject_toml, load_config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestConfigLoading:
|
|
11
|
+
"""Tests for configuration loading from pyproject.toml."""
|
|
12
|
+
|
|
13
|
+
def test_default_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
14
|
+
"""Test that default config is returned when no pyproject.toml exists."""
|
|
15
|
+
monkeypatch.chdir(tmp_path)
|
|
16
|
+
config = load_config()
|
|
17
|
+
assert config == {
|
|
18
|
+
"order": ["public", "protected", "private"],
|
|
19
|
+
"method_type_order": None,
|
|
20
|
+
"exclude": None,
|
|
21
|
+
"sort_module_level": False,
|
|
22
|
+
"sort_decorated": False,
|
|
23
|
+
"python_version": None,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def test_load_custom_order(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
27
|
+
"""Test loading custom order from pyproject.toml."""
|
|
28
|
+
pyproject_content = """
|
|
29
|
+
[tool.undersort]
|
|
30
|
+
order = ["private", "protected", "public"]
|
|
31
|
+
"""
|
|
32
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
33
|
+
pyproject_path.write_text(pyproject_content)
|
|
34
|
+
|
|
35
|
+
monkeypatch.chdir(tmp_path)
|
|
36
|
+
config = load_config()
|
|
37
|
+
assert config == {
|
|
38
|
+
"order": ["private", "protected", "public"],
|
|
39
|
+
"method_type_order": None,
|
|
40
|
+
"exclude": None,
|
|
41
|
+
"sort_module_level": False,
|
|
42
|
+
"sort_decorated": False,
|
|
43
|
+
"python_version": None,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def test_invalid_order_values(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
47
|
+
"""Test that invalid order values fall back to default."""
|
|
48
|
+
pyproject_content = """
|
|
49
|
+
[tool.undersort]
|
|
50
|
+
order = ["public", "invalid", "private"]
|
|
51
|
+
"""
|
|
52
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
53
|
+
pyproject_path.write_text(pyproject_content)
|
|
54
|
+
|
|
55
|
+
monkeypatch.chdir(tmp_path)
|
|
56
|
+
config = load_config()
|
|
57
|
+
assert config == {
|
|
58
|
+
"order": ["public", "protected", "private"],
|
|
59
|
+
"method_type_order": None,
|
|
60
|
+
"exclude": None,
|
|
61
|
+
"sort_module_level": False,
|
|
62
|
+
"sort_decorated": False,
|
|
63
|
+
"python_version": None,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def test_missing_order_key(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
67
|
+
"""Test that missing order key returns default."""
|
|
68
|
+
pyproject_content = """
|
|
69
|
+
[tool.undersort]
|
|
70
|
+
some_other_key = "value"
|
|
71
|
+
"""
|
|
72
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
73
|
+
pyproject_path.write_text(pyproject_content)
|
|
74
|
+
|
|
75
|
+
monkeypatch.chdir(tmp_path)
|
|
76
|
+
config = load_config()
|
|
77
|
+
assert config == {
|
|
78
|
+
"order": ["public", "protected", "private"],
|
|
79
|
+
"method_type_order": None,
|
|
80
|
+
"exclude": None,
|
|
81
|
+
"sort_module_level": False,
|
|
82
|
+
"sort_decorated": False,
|
|
83
|
+
"python_version": None,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def test_missing_tool_section(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
87
|
+
"""Test that missing tool.undersort section returns default."""
|
|
88
|
+
pyproject_content = """
|
|
89
|
+
[project]
|
|
90
|
+
name = "test"
|
|
91
|
+
"""
|
|
92
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
93
|
+
pyproject_path.write_text(pyproject_content)
|
|
94
|
+
|
|
95
|
+
monkeypatch.chdir(tmp_path)
|
|
96
|
+
config = load_config()
|
|
97
|
+
assert config == {
|
|
98
|
+
"order": ["public", "protected", "private"],
|
|
99
|
+
"method_type_order": None,
|
|
100
|
+
"exclude": None,
|
|
101
|
+
"sort_module_level": False,
|
|
102
|
+
"sort_decorated": False,
|
|
103
|
+
"python_version": None,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def test_find_pyproject_in_parent(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
107
|
+
"""Test that pyproject.toml is found in parent directories."""
|
|
108
|
+
# Create pyproject.toml in parent
|
|
109
|
+
pyproject_content = """
|
|
110
|
+
[tool.undersort]
|
|
111
|
+
order = ["private", "public", "protected"]
|
|
112
|
+
"""
|
|
113
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
114
|
+
pyproject_path.write_text(pyproject_content)
|
|
115
|
+
|
|
116
|
+
# Change to subdirectory
|
|
117
|
+
subdir = tmp_path / "subdir" / "nested"
|
|
118
|
+
subdir.mkdir(parents=True)
|
|
119
|
+
monkeypatch.chdir(subdir)
|
|
120
|
+
|
|
121
|
+
config = load_config()
|
|
122
|
+
assert config == {
|
|
123
|
+
"order": ["private", "public", "protected"],
|
|
124
|
+
"method_type_order": None,
|
|
125
|
+
"exclude": None,
|
|
126
|
+
"sort_module_level": False,
|
|
127
|
+
"sort_decorated": False,
|
|
128
|
+
"python_version": None,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
def test_corrupted_toml(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
132
|
+
"""Test that corrupted TOML file falls back to default."""
|
|
133
|
+
pyproject_content = """
|
|
134
|
+
[tool.undersort
|
|
135
|
+
order = ["public" # Invalid TOML
|
|
136
|
+
"""
|
|
137
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
138
|
+
pyproject_path.write_text(pyproject_content)
|
|
139
|
+
|
|
140
|
+
monkeypatch.chdir(tmp_path)
|
|
141
|
+
config = load_config()
|
|
142
|
+
assert config == {
|
|
143
|
+
"order": ["public", "protected", "private"],
|
|
144
|
+
"method_type_order": None,
|
|
145
|
+
"exclude": None,
|
|
146
|
+
"sort_module_level": False,
|
|
147
|
+
"sort_decorated": False,
|
|
148
|
+
"python_version": None,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
def test_find_pyproject_toml_not_found(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
152
|
+
"""Test _find_pyproject_toml returns None when not found."""
|
|
153
|
+
monkeypatch.chdir(tmp_path)
|
|
154
|
+
result = _find_pyproject_toml()
|
|
155
|
+
assert result is None
|
|
156
|
+
|
|
157
|
+
def test_find_pyproject_toml_found(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
158
|
+
"""Test _find_pyproject_toml returns path when found."""
|
|
159
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
160
|
+
pyproject_path.write_text("[project]\nname = 'test'")
|
|
161
|
+
|
|
162
|
+
monkeypatch.chdir(tmp_path)
|
|
163
|
+
result = _find_pyproject_toml()
|
|
164
|
+
assert result == pyproject_path
|
|
165
|
+
|
|
166
|
+
def test_load_exclude_patterns(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
167
|
+
"""Test loading exclude patterns from pyproject.toml."""
|
|
168
|
+
pyproject_content = """
|
|
169
|
+
[tool.undersort]
|
|
170
|
+
order = ["public", "protected", "private"]
|
|
171
|
+
exclude = ["tests/*", "migrations/*.py"]
|
|
172
|
+
"""
|
|
173
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
174
|
+
pyproject_path.write_text(pyproject_content)
|
|
175
|
+
|
|
176
|
+
monkeypatch.chdir(tmp_path)
|
|
177
|
+
config = load_config()
|
|
178
|
+
assert config == {
|
|
179
|
+
"order": ["public", "protected", "private"],
|
|
180
|
+
"method_type_order": None,
|
|
181
|
+
"exclude": ["tests/*", "migrations/*.py"],
|
|
182
|
+
"sort_module_level": False,
|
|
183
|
+
"sort_decorated": False,
|
|
184
|
+
"python_version": None,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
def test_invalid_exclude_type(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
188
|
+
"""Test that invalid exclude type falls back to None."""
|
|
189
|
+
pyproject_content = """
|
|
190
|
+
[tool.undersort]
|
|
191
|
+
order = ["public", "protected", "private"]
|
|
192
|
+
exclude = "invalid"
|
|
193
|
+
"""
|
|
194
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
195
|
+
pyproject_path.write_text(pyproject_content)
|
|
196
|
+
|
|
197
|
+
monkeypatch.chdir(tmp_path)
|
|
198
|
+
config = load_config()
|
|
199
|
+
assert config == {
|
|
200
|
+
"order": ["public", "protected", "private"],
|
|
201
|
+
"method_type_order": None,
|
|
202
|
+
"exclude": None,
|
|
203
|
+
"sort_module_level": False,
|
|
204
|
+
"sort_decorated": False,
|
|
205
|
+
"python_version": None,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
def test_sort_module_level_enabled(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
209
|
+
"""Test that sort_module_level is read from pyproject.toml."""
|
|
210
|
+
pyproject_content = """
|
|
211
|
+
[tool.undersort]
|
|
212
|
+
sort_module_level = true
|
|
213
|
+
"""
|
|
214
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
215
|
+
pyproject_path.write_text(pyproject_content)
|
|
216
|
+
|
|
217
|
+
monkeypatch.chdir(tmp_path)
|
|
218
|
+
assert load_config()["sort_module_level"] is True
|
|
219
|
+
|
|
220
|
+
def test_invalid_sort_module_level(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
221
|
+
"""Test that a non-boolean sort_module_level falls back to the default."""
|
|
222
|
+
pyproject_content = """
|
|
223
|
+
[tool.undersort]
|
|
224
|
+
sort_module_level = "yes"
|
|
225
|
+
"""
|
|
226
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
227
|
+
pyproject_path.write_text(pyproject_content)
|
|
228
|
+
|
|
229
|
+
monkeypatch.chdir(tmp_path)
|
|
230
|
+
assert load_config()["sort_module_level"] is False
|
|
231
|
+
|
|
232
|
+
def test_python_version_from_requires_python(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
233
|
+
"""Test that the target version is inferred from the project's requires-python."""
|
|
234
|
+
pyproject_content = """
|
|
235
|
+
[project]
|
|
236
|
+
name = "test"
|
|
237
|
+
requires-python = ">=3.10,<4.0"
|
|
238
|
+
"""
|
|
239
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
240
|
+
pyproject_path.write_text(pyproject_content)
|
|
241
|
+
|
|
242
|
+
monkeypatch.chdir(tmp_path)
|
|
243
|
+
assert load_config()["python_version"] == (3, 10)
|
|
244
|
+
|
|
245
|
+
def test_explicit_python_version_wins(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
246
|
+
"""Test that an explicit python_version overrides requires-python."""
|
|
247
|
+
pyproject_content = """
|
|
248
|
+
[project]
|
|
249
|
+
name = "test"
|
|
250
|
+
requires-python = ">=3.10"
|
|
251
|
+
|
|
252
|
+
[tool.undersort]
|
|
253
|
+
python_version = "3.14"
|
|
254
|
+
"""
|
|
255
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
256
|
+
pyproject_path.write_text(pyproject_content)
|
|
257
|
+
|
|
258
|
+
monkeypatch.chdir(tmp_path)
|
|
259
|
+
assert load_config()["python_version"] == (3, 14)
|
|
260
|
+
|
|
261
|
+
def test_invalid_python_version_ignored(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
262
|
+
"""Test that an unparseable python_version is ignored."""
|
|
263
|
+
pyproject_content = """
|
|
264
|
+
[tool.undersort]
|
|
265
|
+
python_version = "nonsense"
|
|
266
|
+
"""
|
|
267
|
+
pyproject_path = tmp_path / "pyproject.toml"
|
|
268
|
+
pyproject_path.write_text(pyproject_content)
|
|
269
|
+
|
|
270
|
+
monkeypatch.chdir(tmp_path)
|
|
271
|
+
assert load_config()["python_version"] is None
|