defsort 0.1.0__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.
- defsort-0.1.0/.gitignore +46 -0
- defsort-0.1.0/LICENSE +21 -0
- defsort-0.1.0/PKG-INFO +150 -0
- defsort-0.1.0/README.md +131 -0
- defsort-0.1.0/_build.py +30 -0
- defsort-0.1.0/plugins/agy/_defsort/__init__.py +1 -0
- defsort-0.1.0/plugins/agy/_defsort/_adapter.py +121 -0
- defsort-0.1.0/plugins/agy/adapter.toml +2 -0
- defsort-0.1.0/plugins/agy/hooks.json +15 -0
- defsort-0.1.0/plugins/agy/plugin.json +5 -0
- defsort-0.1.0/plugins/agy/tests/test_agy_adapter.py +140 -0
- defsort-0.1.0/plugins/claude/.claude-plugin/plugin.json +10 -0
- defsort-0.1.0/plugins/claude/_defsort/__init__.py +1 -0
- defsort-0.1.0/plugins/claude/_defsort/_adapter.py +131 -0
- defsort-0.1.0/plugins/claude/adapter.toml +2 -0
- defsort-0.1.0/plugins/claude/hooks/hooks.json +15 -0
- defsort-0.1.0/plugins/claude/tests/test_claude_adapter.py +210 -0
- defsort-0.1.0/plugins/codex/.codex-plugin/plugin.json +20 -0
- defsort-0.1.0/plugins/codex/_defsort/__init__.py +1 -0
- defsort-0.1.0/plugins/codex/_defsort/_adapter.py +142 -0
- defsort-0.1.0/plugins/codex/adapter.toml +2 -0
- defsort-0.1.0/plugins/codex/hooks/hooks.json +15 -0
- defsort-0.1.0/plugins/codex/tests/test_codex_adapter.py +166 -0
- defsort-0.1.0/pyproject.toml +167 -0
- defsort-0.1.0/src/defsort/__init__.py +332 -0
- defsort-0.1.0/src/defsort/__main__.py +8 -0
- defsort-0.1.0/src/defsort/_classify.py +56 -0
- defsort-0.1.0/src/defsort/_cli.py +1186 -0
- defsort-0.1.0/src/defsort/_color.py +86 -0
- defsort-0.1.0/src/defsort/_config.py +576 -0
- defsort-0.1.0/src/defsort/_core/__init__.py +5 -0
- defsort-0.1.0/src/defsort/_core/_analysis.py +1237 -0
- defsort-0.1.0/src/defsort/_core/_api.py +373 -0
- defsort-0.1.0/src/defsort/_core/_banners.py +563 -0
- defsort-0.1.0/src/defsort/_core/_full.py +407 -0
- defsort-0.1.0/src/defsort/_core/_future.py +233 -0
- defsort-0.1.0/src/defsort/_core/_guard.py +151 -0
- defsort-0.1.0/src/defsort/_core/_hoist.py +238 -0
- defsort-0.1.0/src/defsort/_core/_incremental.py +741 -0
- defsort-0.1.0/src/defsort/_core/_layout.py +496 -0
- defsort-0.1.0/src/defsort/_core/_merge.py +123 -0
- defsort-0.1.0/src/defsort/_core/_methods.py +1283 -0
- defsort-0.1.0/src/defsort/_core/_ordering.py +394 -0
- defsort-0.1.0/src/defsort/_core/_parse.py +763 -0
- defsort-0.1.0/src/defsort/_core/_reassemble.py +42 -0
- defsort-0.1.0/src/defsort/_core/_safety.py +411 -0
- defsort-0.1.0/src/defsort/_detector.py +152 -0
- defsort-0.1.0/src/defsort/_display/__init__.py +16 -0
- defsort-0.1.0/src/defsort/_display/_constraints.py +60 -0
- defsort-0.1.0/src/defsort/_display/_detect.py +62 -0
- defsort-0.1.0/src/defsort/_display/_diff.py +105 -0
- defsort-0.1.0/src/defsort/_display/_json.py +138 -0
- defsort-0.1.0/src/defsort/_display/_movement.py +282 -0
- defsort-0.1.0/src/defsort/_display/_skipped.py +130 -0
- defsort-0.1.0/src/defsort/_display/_summary.py +211 -0
- defsort-0.1.0/src/defsort/_display/_terminal.py +24 -0
- defsort-0.1.0/src/defsort/_display/_types.py +166 -0
- defsort-0.1.0/src/defsort/_display/_width.py +34 -0
- defsort-0.1.0/src/defsort/_downstream.py +181 -0
- defsort-0.1.0/src/defsort/_files.py +272 -0
- defsort-0.1.0/src/defsort/_git.py +1021 -0
- defsort-0.1.0/src/defsort/_git_executable.py +51 -0
- defsort-0.1.0/src/defsort/_grammar.py +785 -0
- defsort-0.1.0/src/defsort/_hook.py +1564 -0
- defsort-0.1.0/src/defsort/_hosts/__init__.py +50 -0
- defsort-0.1.0/src/defsort/_shell.py +66 -0
- defsort-0.1.0/src/defsort/_shellparse.py +595 -0
- defsort-0.1.0/src/defsort/_source.py +140 -0
- defsort-0.1.0/src/defsort/_types.py +840 -0
- defsort-0.1.0/src/defsort/hooks/__init__.py +242 -0
- defsort-0.1.0/src/defsort/py.typed +0 -0
defsort-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
.eggs/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
*.egg
|
|
16
|
+
|
|
17
|
+
# Virtual environments
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
ENV/
|
|
21
|
+
|
|
22
|
+
# PyInstaller
|
|
23
|
+
*.manifest
|
|
24
|
+
*.spec
|
|
25
|
+
|
|
26
|
+
# Unit test / coverage reports
|
|
27
|
+
htmlcov/
|
|
28
|
+
.tox/
|
|
29
|
+
.nox/
|
|
30
|
+
.coverage*
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.cache/
|
|
33
|
+
|
|
34
|
+
# Type checker / linter caches
|
|
35
|
+
.pyright/
|
|
36
|
+
.mypy_cache/
|
|
37
|
+
.ruff_cache/
|
|
38
|
+
pyrightconfig.json
|
|
39
|
+
|
|
40
|
+
# Editors
|
|
41
|
+
.vscode/
|
|
42
|
+
.idea/
|
|
43
|
+
|
|
44
|
+
# OS
|
|
45
|
+
.DS_Store
|
|
46
|
+
Thumbs.db
|
defsort-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the defsort authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
defsort-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: defsort
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sort Python definitions by dependency or alphabetically
|
|
5
|
+
Project-URL: Documentation, https://github.com/dactylo/defsort/tree/main/docs
|
|
6
|
+
Project-URL: Homepage, https://github.com/dactylo/defsort
|
|
7
|
+
Project-URL: Issues, https://github.com/dactylo/defsort/issues
|
|
8
|
+
Project-URL: Repository, https://github.com/dactylo/defsort
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ast,definitions,dependency,formatter,sort
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.13
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# defsort
|
|
21
|
+
|
|
22
|
+
[](https://github.com/dactylo/defsort/actions/workflows/ci.yml)
|
|
23
|
+
|
|
24
|
+
Defsort applies a configurable ordering policy to Python functions, classes, and methods. It makes
|
|
25
|
+
definition order deterministic, so placement follows a project convention rather than accumulating
|
|
26
|
+
from successive edits.
|
|
27
|
+
|
|
28
|
+
Run `defsort` to see how much would change across your project:
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
Then use `defsort --preview` to see where definitions move. Colors track definitions between the
|
|
33
|
+
Current and Proposed columns. For example:
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
Use the CLI to preview and apply changes, an agent hook to sort before commits, or the Python
|
|
38
|
+
library to integrate sorting into your own tools.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
defsort requires CPython 3.13 or later. Install the current checkout from the repository root with
|
|
43
|
+
[uv](https://docs.astral.sh/uv/):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv tool install .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
To update an existing installation after pulling changes:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv tool install --reinstall .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Git 2.42 or newer is required for agent hooks, incremental mode, and a configured reorder guard.
|
|
56
|
+
Ordinary CLI sorting and the library do not require Git. See
|
|
57
|
+
[platform compatibility](docs/platform.md) for runtime requirements and file-handling details.
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
Start with a summary, preview movement, inspect the diff, then apply the changes:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
defsort
|
|
65
|
+
defsort --preview
|
|
66
|
+
defsort --diff
|
|
67
|
+
defsort --write
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The first three commands leave files unchanged. They exit 1 when changes or skipped files need
|
|
71
|
+
attention; exit 2 indicates an error. A successful write exits 0 unless files were skipped.
|
|
72
|
+
|
|
73
|
+
Choose dependency or alphabetical ordering, with separate policies for top-level definitions and
|
|
74
|
+
class methods. Automatic sorting trades control over individual placement for consistency.
|
|
75
|
+
Dependency changes can produce large reorders; alphabetical order can separate related code. Use
|
|
76
|
+
comment banners to preserve sections. See
|
|
77
|
+
[ordering policies](docs/design.md#choose-an-ordering-policy) for the choices and tradeoffs.
|
|
78
|
+
|
|
79
|
+
The defaults use dependency ordering, alphabetical ties, and lifecycle methods first. No
|
|
80
|
+
configuration is required. To keep class methods in their authored order while sorting top-level
|
|
81
|
+
definitions, add:
|
|
82
|
+
|
|
83
|
+
```toml
|
|
84
|
+
[tool.defsort]
|
|
85
|
+
method-order = "none"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Defsort checks for changes that could break definition-time references and preserves decorators,
|
|
89
|
+
comments, and source layout. Its checks are bounded: classes whose behavior depends on method order
|
|
90
|
+
should use `method-order = "none"`. Read the [safety model](docs/design.md#safety-model) before
|
|
91
|
+
adopting it in order-sensitive code.
|
|
92
|
+
|
|
93
|
+
For an existing codebase, the [incremental guide](docs/incremental.md) explains how to establish
|
|
94
|
+
sorted baselines file by file and limit later reordering.
|
|
95
|
+
|
|
96
|
+
## Integrations
|
|
97
|
+
|
|
98
|
+
- **Coding agents:** the recommended PreToolUse hook sorts staged Python content before supported
|
|
99
|
+
commits and preserves partial staging. Follow the [hook setup guide](docs/hook.md#configuration)
|
|
100
|
+
for Claude Code, Codex, or Antigravity. Choose either plugin installation or manual registration.
|
|
101
|
+
- **Multiple formatting hooks:** use the
|
|
102
|
+
[composition guide](docs/hook.md#composing-with-other-hooks) to run modifying hooks in order. It
|
|
103
|
+
includes the current Antigravity limitation.
|
|
104
|
+
- **pre-commit:** use a local registration with an installed CLI as described in the
|
|
105
|
+
[pre-commit guide](docs/hook.md#pre-commit).
|
|
106
|
+
|
|
107
|
+
## Library API
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
import defsort
|
|
111
|
+
|
|
112
|
+
source = 'def zebra(): pass\n\ndef alpha(): pass\n'
|
|
113
|
+
result = defsort.check(source)
|
|
114
|
+
if result.status == 'changed':
|
|
115
|
+
source = defsort.sort(source)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Use `check()` to inspect skipped or invalid source before deciding what to do. The
|
|
119
|
+
[library reference](docs/library.md) describes configuration, results, and the public adapter API.
|
|
120
|
+
|
|
121
|
+
## Documentation
|
|
122
|
+
|
|
123
|
+
| If you want to... | Read |
|
|
124
|
+
| --------------------------------------------------- | ------------------------------------------ |
|
|
125
|
+
| Choose an ordering policy and understand its limits | [Concepts and safety](docs/design.md) |
|
|
126
|
+
| Preview, apply, or automate a sort | [CLI reference](docs/cli.md) |
|
|
127
|
+
| Configure agent hooks or pre-commit | [Hook guide](docs/hook.md) |
|
|
128
|
+
| Control reordering during adoption | [Incremental mode](docs/incremental.md) |
|
|
129
|
+
| Call defsort from Python | [Library reference](docs/library.md) |
|
|
130
|
+
| Check runtime, encoding, or path behavior | [Platform compatibility](docs/platform.md) |
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
See the [development guide](docs/development.md) for pinned CI dependencies and lock updates.
|
|
135
|
+
|
|
136
|
+
Provide the tools in `[dependency-groups].dev` through your environment or package manager.
|
|
137
|
+
`make install` and `make reinstall` install only the CLI through uv.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
make quality # read-only quality checks
|
|
141
|
+
make check # quality checks and one full test run
|
|
142
|
+
make format # apply repository formatters
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
To contribute an agent integration, see
|
|
146
|
+
[adding a bundled agent](docs/hook.md#adding-a-bundled-agent).
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
defsort is distributed under the [MIT License](LICENSE).
|
defsort-0.1.0/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# defsort
|
|
2
|
+
|
|
3
|
+
[](https://github.com/dactylo/defsort/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Defsort applies a configurable ordering policy to Python functions, classes, and methods. It makes
|
|
6
|
+
definition order deterministic, so placement follows a project convention rather than accumulating
|
|
7
|
+
from successive edits.
|
|
8
|
+
|
|
9
|
+
Run `defsort` to see how much would change across your project:
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
Then use `defsort --preview` to see where definitions move. Colors track definitions between the
|
|
14
|
+
Current and Proposed columns. For example:
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
Use the CLI to preview and apply changes, an agent hook to sort before commits, or the Python
|
|
19
|
+
library to integrate sorting into your own tools.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
defsort requires CPython 3.13 or later. Install the current checkout from the repository root with
|
|
24
|
+
[uv](https://docs.astral.sh/uv/):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv tool install .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To update an existing installation after pulling changes:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv tool install --reinstall .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Git 2.42 or newer is required for agent hooks, incremental mode, and a configured reorder guard.
|
|
37
|
+
Ordinary CLI sorting and the library do not require Git. See
|
|
38
|
+
[platform compatibility](docs/platform.md) for runtime requirements and file-handling details.
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
Start with a summary, preview movement, inspect the diff, then apply the changes:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
defsort
|
|
46
|
+
defsort --preview
|
|
47
|
+
defsort --diff
|
|
48
|
+
defsort --write
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The first three commands leave files unchanged. They exit 1 when changes or skipped files need
|
|
52
|
+
attention; exit 2 indicates an error. A successful write exits 0 unless files were skipped.
|
|
53
|
+
|
|
54
|
+
Choose dependency or alphabetical ordering, with separate policies for top-level definitions and
|
|
55
|
+
class methods. Automatic sorting trades control over individual placement for consistency.
|
|
56
|
+
Dependency changes can produce large reorders; alphabetical order can separate related code. Use
|
|
57
|
+
comment banners to preserve sections. See
|
|
58
|
+
[ordering policies](docs/design.md#choose-an-ordering-policy) for the choices and tradeoffs.
|
|
59
|
+
|
|
60
|
+
The defaults use dependency ordering, alphabetical ties, and lifecycle methods first. No
|
|
61
|
+
configuration is required. To keep class methods in their authored order while sorting top-level
|
|
62
|
+
definitions, add:
|
|
63
|
+
|
|
64
|
+
```toml
|
|
65
|
+
[tool.defsort]
|
|
66
|
+
method-order = "none"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Defsort checks for changes that could break definition-time references and preserves decorators,
|
|
70
|
+
comments, and source layout. Its checks are bounded: classes whose behavior depends on method order
|
|
71
|
+
should use `method-order = "none"`. Read the [safety model](docs/design.md#safety-model) before
|
|
72
|
+
adopting it in order-sensitive code.
|
|
73
|
+
|
|
74
|
+
For an existing codebase, the [incremental guide](docs/incremental.md) explains how to establish
|
|
75
|
+
sorted baselines file by file and limit later reordering.
|
|
76
|
+
|
|
77
|
+
## Integrations
|
|
78
|
+
|
|
79
|
+
- **Coding agents:** the recommended PreToolUse hook sorts staged Python content before supported
|
|
80
|
+
commits and preserves partial staging. Follow the [hook setup guide](docs/hook.md#configuration)
|
|
81
|
+
for Claude Code, Codex, or Antigravity. Choose either plugin installation or manual registration.
|
|
82
|
+
- **Multiple formatting hooks:** use the
|
|
83
|
+
[composition guide](docs/hook.md#composing-with-other-hooks) to run modifying hooks in order. It
|
|
84
|
+
includes the current Antigravity limitation.
|
|
85
|
+
- **pre-commit:** use a local registration with an installed CLI as described in the
|
|
86
|
+
[pre-commit guide](docs/hook.md#pre-commit).
|
|
87
|
+
|
|
88
|
+
## Library API
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import defsort
|
|
92
|
+
|
|
93
|
+
source = 'def zebra(): pass\n\ndef alpha(): pass\n'
|
|
94
|
+
result = defsort.check(source)
|
|
95
|
+
if result.status == 'changed':
|
|
96
|
+
source = defsort.sort(source)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Use `check()` to inspect skipped or invalid source before deciding what to do. The
|
|
100
|
+
[library reference](docs/library.md) describes configuration, results, and the public adapter API.
|
|
101
|
+
|
|
102
|
+
## Documentation
|
|
103
|
+
|
|
104
|
+
| If you want to... | Read |
|
|
105
|
+
| --------------------------------------------------- | ------------------------------------------ |
|
|
106
|
+
| Choose an ordering policy and understand its limits | [Concepts and safety](docs/design.md) |
|
|
107
|
+
| Preview, apply, or automate a sort | [CLI reference](docs/cli.md) |
|
|
108
|
+
| Configure agent hooks or pre-commit | [Hook guide](docs/hook.md) |
|
|
109
|
+
| Control reordering during adoption | [Incremental mode](docs/incremental.md) |
|
|
110
|
+
| Call defsort from Python | [Library reference](docs/library.md) |
|
|
111
|
+
| Check runtime, encoding, or path behavior | [Platform compatibility](docs/platform.md) |
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
See the [development guide](docs/development.md) for pinned CI dependencies and lock updates.
|
|
116
|
+
|
|
117
|
+
Provide the tools in `[dependency-groups].dev` through your environment or package manager.
|
|
118
|
+
`make install` and `make reinstall` install only the CLI through uv.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
make quality # read-only quality checks
|
|
122
|
+
make check # quality checks and one full test run
|
|
123
|
+
make format # apply repository formatters
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
To contribute an agent integration, see
|
|
127
|
+
[adding a bundled agent](docs/hook.md#adding-a-bundled-agent).
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
defsort is distributed under the [MIT License](LICENSE).
|
defsort-0.1.0/_build.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Collect bundled agent entry points for distribution metadata."""
|
|
2
|
+
|
|
3
|
+
import tomllib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import override
|
|
6
|
+
|
|
7
|
+
from hatchling.metadata.plugin.interface import MetadataHookInterface
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PluginMetadataHook(MetadataHookInterface):
|
|
11
|
+
"""Include each bundled plugin's registration in the containing distribution."""
|
|
12
|
+
|
|
13
|
+
@override
|
|
14
|
+
def update(self, metadata: dict[str, object]) -> None:
|
|
15
|
+
metadata['entry-points'] = _plugin_entry_points(Path(self.root))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _plugin_entry_points(root: Path) -> dict[str, dict[str, str]]:
|
|
19
|
+
"""Read entry points declared by the root's agent plugin directories."""
|
|
20
|
+
groups: dict[str, dict[str, str]] = {}
|
|
21
|
+
for manifest in sorted((root / 'plugins').glob('*/adapter.toml')):
|
|
22
|
+
with manifest.open('rb') as stream:
|
|
23
|
+
entries = tomllib.load(stream)['entry-points']
|
|
24
|
+
for group, targets in entries.items():
|
|
25
|
+
registered = groups.setdefault(group, {})
|
|
26
|
+
for name, target in targets.items():
|
|
27
|
+
if name in registered:
|
|
28
|
+
raise ValueError(f'Duplicate entry point {group}:{name} in {manifest}')
|
|
29
|
+
registered[name] = target
|
|
30
|
+
return groups
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Antigravity adapter package for defsort."""
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Antigravity hook adapter."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from defsort.hooks import (
|
|
8
|
+
API_VERSION,
|
|
9
|
+
HookResult,
|
|
10
|
+
HostInput,
|
|
11
|
+
HostResponse,
|
|
12
|
+
absolute_directory,
|
|
13
|
+
notes_text,
|
|
14
|
+
object_field,
|
|
15
|
+
payload_cwd,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_COMMAND_TOOL = 'run_command'
|
|
19
|
+
_EDIT_TOOLS = frozenset({'replace_file_content', 'write_to_file'})
|
|
20
|
+
_EVENTS = {'PreToolUse': 'pre_tool', 'PostToolUse': 'post_tool'}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _allow_response(result: HookResult) -> HostResponse:
|
|
24
|
+
"""Render Antigravity allow feedback."""
|
|
25
|
+
stdout = f'{json.dumps({"systemMessage": result.reason})}\n' if result.reason else ''
|
|
26
|
+
return HostResponse(stdout, notes_text(result), 0)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _base_cwd(payload: Mapping[str, object], args: Mapping[str, object]) -> str:
|
|
30
|
+
"""Resolve the target directory from Cwd, workspace paths, or the process."""
|
|
31
|
+
cwd = args.get('Cwd')
|
|
32
|
+
if isinstance(cwd, str) and cwd:
|
|
33
|
+
return cwd
|
|
34
|
+
workspaces = payload.get('workspacePaths')
|
|
35
|
+
if isinstance(workspaces, list):
|
|
36
|
+
first = cast('list[object]', workspaces)[:1]
|
|
37
|
+
if first and isinstance(first[0], str) and first[0]:
|
|
38
|
+
return first[0]
|
|
39
|
+
return payload_cwd(payload)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _deny(reason: str, stderr_prefix: str = '') -> HostResponse:
|
|
43
|
+
"""Render Antigravity's top-level deny object."""
|
|
44
|
+
stdout = json.dumps({'decision': 'deny', 'reason': reason})
|
|
45
|
+
return HostResponse(f'{stdout}\n', f'{stderr_prefix}{reason}\n', 0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AntigravityAdapter:
|
|
49
|
+
"""Translate Antigravity tool-call payloads, which carry no event name."""
|
|
50
|
+
|
|
51
|
+
name = 'agy'
|
|
52
|
+
api_version = API_VERSION
|
|
53
|
+
requires_event = True
|
|
54
|
+
|
|
55
|
+
def parse(self, payload: Mapping[str, object], event: str | None) -> HostInput | None:
|
|
56
|
+
"""Read the tool-call name and arguments under the given event.
|
|
57
|
+
|
|
58
|
+
For `run_command`, pre-tool `command_cwd` is `toolCall.args.Cwd` when
|
|
59
|
+
it is an absolute path; `workspacePaths` and the process directory are
|
|
60
|
+
never invocation evidence. Other tools report `None`.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
payload: Decoded Antigravity hook payload.
|
|
64
|
+
event: PreToolUse or PostToolUse from the command line.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Neutral input. An unrelated tool yields input with nothing to act
|
|
68
|
+
on.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
ValueError: If event is not an Antigravity hook event.
|
|
72
|
+
"""
|
|
73
|
+
neutral_event = _EVENTS.get(event or '')
|
|
74
|
+
if neutral_event is None:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f'unknown Antigravity event {event!r}; expected PreToolUse or PostToolUse'
|
|
77
|
+
)
|
|
78
|
+
tool_call = object_field(payload, 'toolCall')
|
|
79
|
+
args = object_field(tool_call, 'args')
|
|
80
|
+
cwd = _base_cwd(payload, args)
|
|
81
|
+
name = tool_call.get('name')
|
|
82
|
+
if neutral_event == 'pre_tool':
|
|
83
|
+
command = args.get('CommandLine') if name == _COMMAND_TOOL else None
|
|
84
|
+
command_cwd = absolute_directory(args, 'Cwd') if name == _COMMAND_TOOL else None
|
|
85
|
+
return HostInput(
|
|
86
|
+
'pre_tool',
|
|
87
|
+
command if isinstance(command, str) else None,
|
|
88
|
+
cwd,
|
|
89
|
+
command_cwd,
|
|
90
|
+
)
|
|
91
|
+
editing = isinstance(name, str) and name in _EDIT_TOOLS
|
|
92
|
+
target = args.get('TargetFile') if editing else None
|
|
93
|
+
paths = (target,) if isinstance(target, str) and target else ()
|
|
94
|
+
return HostInput('post_tool', None, cwd, None, paths)
|
|
95
|
+
|
|
96
|
+
def render(self, result: HookResult) -> HostResponse:
|
|
97
|
+
"""Render allow feedback or the top-level deny object.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
result: Decision, reason, and notes from defsort.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Empty stdout for a silent allow, or one system message for allow
|
|
104
|
+
feedback; notes on stderr and exit 0. For deny, the deny object on
|
|
105
|
+
stdout, the reason on stderr, and exit 0.
|
|
106
|
+
"""
|
|
107
|
+
notes = notes_text(result)
|
|
108
|
+
if result.decision == 'deny':
|
|
109
|
+
return _deny(result.reason or 'defsort denied the operation', notes)
|
|
110
|
+
return _allow_response(result)
|
|
111
|
+
|
|
112
|
+
def render_failure(self, reason: str) -> HostResponse:
|
|
113
|
+
"""Render the top-level deny object.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
reason: Reason for the failure.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The deny object on stdout, the reason on stderr, and exit 0.
|
|
120
|
+
"""
|
|
121
|
+
return _deny(reason)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# pyright: reportPrivateUsage=false
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shlex
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from defsort._cli import _build_hook_parser
|
|
12
|
+
from defsort.hooks import API_VERSION, HookAdapter, HookResult, HostInput, HostResponse
|
|
13
|
+
from plugins.agy._defsort._adapter import AntigravityAdapter
|
|
14
|
+
|
|
15
|
+
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class _Result:
|
|
20
|
+
decision: Literal['allow', 'deny']
|
|
21
|
+
reason: str | None = None
|
|
22
|
+
notes: tuple[str, ...] = ()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _payload(tool: str, args: dict[str, object]) -> dict[str, object]:
|
|
26
|
+
return {'toolCall': {'name': tool, 'args': args}}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _result(
|
|
30
|
+
decision: Literal['allow', 'deny'],
|
|
31
|
+
reason: str | None = None,
|
|
32
|
+
notes: tuple[str, ...] = (),
|
|
33
|
+
) -> HookResult:
|
|
34
|
+
return _Result(decision, reason, notes)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_contract() -> None:
|
|
38
|
+
adapter: HookAdapter = AntigravityAdapter()
|
|
39
|
+
assert (adapter.name, adapter.api_version, adapter.requires_event) == (
|
|
40
|
+
'agy',
|
|
41
|
+
API_VERSION,
|
|
42
|
+
True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_manifest_registers_only_pre_tool_use() -> None:
|
|
47
|
+
manifest = PLUGIN_ROOT / 'hooks.json'
|
|
48
|
+
assert json.loads(manifest.read_text(encoding='utf-8')) == {
|
|
49
|
+
'defsort': {
|
|
50
|
+
'PreToolUse': [
|
|
51
|
+
{
|
|
52
|
+
'matcher': 'run_command',
|
|
53
|
+
'hooks': [
|
|
54
|
+
{
|
|
55
|
+
'type': 'command',
|
|
56
|
+
'command': 'defsort hook --agent agy --event PreToolUse',
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
argv = shlex.split('defsort hook --agent agy --event PreToolUse')
|
|
64
|
+
args = _build_hook_parser().parse_args(argv[2:])
|
|
65
|
+
assert args.agent == 'agy'
|
|
66
|
+
assert (args.event is not None) == AntigravityAdapter.requires_event
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.parametrize('tool', ['write_to_file', 'replace_file_content'])
|
|
70
|
+
def test_parse_post_tool_file_writers(tool: str) -> None:
|
|
71
|
+
payload = _payload(tool, {'TargetFile': '/t/a.py', 'Cwd': '/t'})
|
|
72
|
+
assert AntigravityAdapter().parse(payload, 'PostToolUse') == HostInput(
|
|
73
|
+
'post_tool', None, '/t', None, ('/t/a.py',)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_parse_pre_tool_other_tool_has_no_command() -> None:
|
|
78
|
+
payload = _payload('write_to_file', {'TargetFile': 'a.py', 'Cwd': '/t'})
|
|
79
|
+
assert AntigravityAdapter().parse(payload, 'PreToolUse') == HostInput(
|
|
80
|
+
'pre_tool', None, '/t', None
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_parse_rejects_unknown_event() -> None:
|
|
85
|
+
with pytest.raises(ValueError, match='unknown Antigravity event'):
|
|
86
|
+
AntigravityAdapter().parse({}, 'Stop')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize('cwd', ['rel', '', 7])
|
|
90
|
+
def test_parse_run_command_non_absolute_cwd_is_unknown(cwd: object) -> None:
|
|
91
|
+
payload = _payload('run_command', {'CommandLine': 'git commit -m x', 'Cwd': cwd})
|
|
92
|
+
payload['workspacePaths'] = ['/ws']
|
|
93
|
+
parsed = AntigravityAdapter().parse(payload, 'PreToolUse')
|
|
94
|
+
assert parsed is not None
|
|
95
|
+
assert parsed.command_cwd is None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_parse_run_command_prefers_cwd_over_workspace(tmp_path: Path) -> None:
|
|
99
|
+
cwd = str(tmp_path / 'command')
|
|
100
|
+
payload = _payload('run_command', {'CommandLine': 'git commit -m x', 'Cwd': cwd})
|
|
101
|
+
payload['workspacePaths'] = [str(tmp_path / 'workspace')]
|
|
102
|
+
assert AntigravityAdapter().parse(payload, 'PreToolUse') == HostInput(
|
|
103
|
+
'pre_tool', 'git commit -m x', cwd, cwd
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_parse_run_command_without_cwd_is_unknown() -> None:
|
|
108
|
+
payload = _payload('run_command', {'CommandLine': 'git commit -m x'})
|
|
109
|
+
payload['workspacePaths'] = ['/ws']
|
|
110
|
+
assert AntigravityAdapter().parse(payload, 'PreToolUse') == HostInput(
|
|
111
|
+
'pre_tool', 'git commit -m x', '/ws', None
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_plugin_manifest_uses_defsort_identity() -> None:
|
|
116
|
+
manifest = json.loads((PLUGIN_ROOT / 'plugin.json').read_text(encoding='utf-8'))
|
|
117
|
+
assert manifest == {
|
|
118
|
+
'$schema': 'https://antigravity.google/schemas/v1/plugin.json',
|
|
119
|
+
'name': 'defsort-agy',
|
|
120
|
+
'description': 'Install the defsort commit-time definition sorting hook for Antigravity.',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_render_allow_and_failure() -> None:
|
|
125
|
+
assert AntigravityAdapter().render(_result('allow')) == HostResponse('', '', 0)
|
|
126
|
+
failure = AntigravityAdapter().render_failure('defsort: reason')
|
|
127
|
+
assert json.loads(failure.stdout) == {'decision': 'deny', 'reason': 'defsort: reason'}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_render_allow_feedback_is_one_system_message() -> None:
|
|
131
|
+
response = AntigravityAdapter().render(_result('allow', 'first\nsecond', ('diagnostic',)))
|
|
132
|
+
assert json.loads(response.stdout) == {'systemMessage': 'first\nsecond'}
|
|
133
|
+
assert response.stderr == 'diagnostic\n'
|
|
134
|
+
assert response.exit_code == 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_render_deny_is_top_level_json() -> None:
|
|
138
|
+
response = AntigravityAdapter().render(_result('deny', 'defsort: reason', ('note',)))
|
|
139
|
+
assert json.loads(response.stdout) == {'decision': 'deny', 'reason': 'defsort: reason'}
|
|
140
|
+
assert (response.stderr, response.exit_code) == ('note\ndefsort: reason\n', 0)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "defsort",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install the defsort commit-time definition sorting hook for Claude Code.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "defsort contributors"
|
|
7
|
+
},
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"keywords": ["definitions", "dependency", "hooks", "python", "sorting"]
|
|
10
|
+
}
|