docwrap 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.
- docwrap-0.1.0/.gitignore +46 -0
- docwrap-0.1.0/LICENSE +21 -0
- docwrap-0.1.0/PKG-INFO +126 -0
- docwrap-0.1.0/README.md +107 -0
- docwrap-0.1.0/_build.py +30 -0
- docwrap-0.1.0/plugins/agy/_docwrap/__init__.py +1 -0
- docwrap-0.1.0/plugins/agy/_docwrap/_adapter.py +122 -0
- docwrap-0.1.0/plugins/agy/adapter.toml +2 -0
- docwrap-0.1.0/plugins/agy/hooks.json +15 -0
- docwrap-0.1.0/plugins/agy/plugin.json +5 -0
- docwrap-0.1.0/plugins/agy/tests/test_agy_adapter.py +135 -0
- docwrap-0.1.0/plugins/claude/.claude-plugin/plugin.json +10 -0
- docwrap-0.1.0/plugins/claude/_docwrap/__init__.py +1 -0
- docwrap-0.1.0/plugins/claude/_docwrap/_adapter.py +106 -0
- docwrap-0.1.0/plugins/claude/adapter.toml +2 -0
- docwrap-0.1.0/plugins/claude/hooks/hooks.json +15 -0
- docwrap-0.1.0/plugins/claude/tests/test_claude_adapter.py +209 -0
- docwrap-0.1.0/plugins/codex/.codex-plugin/plugin.json +20 -0
- docwrap-0.1.0/plugins/codex/_docwrap/__init__.py +1 -0
- docwrap-0.1.0/plugins/codex/_docwrap/_adapter.py +117 -0
- docwrap-0.1.0/plugins/codex/adapter.toml +2 -0
- docwrap-0.1.0/plugins/codex/hooks/hooks.json +15 -0
- docwrap-0.1.0/plugins/codex/tests/test_codex_adapter.py +125 -0
- docwrap-0.1.0/pyproject.toml +161 -0
- docwrap-0.1.0/src/docwrap/__init__.py +59 -0
- docwrap-0.1.0/src/docwrap/__main__.py +8 -0
- docwrap-0.1.0/src/docwrap/_canonical.py +66 -0
- docwrap-0.1.0/src/docwrap/_classify.py +56 -0
- docwrap-0.1.0/src/docwrap/_cli.py +1284 -0
- docwrap-0.1.0/src/docwrap/_color.py +135 -0
- docwrap-0.1.0/src/docwrap/_config.py +615 -0
- docwrap-0.1.0/src/docwrap/_convert.py +1324 -0
- docwrap-0.1.0/src/docwrap/_core/__init__.py +27 -0
- docwrap-0.1.0/src/docwrap/_core/_codeblock.py +214 -0
- docwrap-0.1.0/src/docwrap/_core/_discover.py +466 -0
- docwrap-0.1.0/src/docwrap/_core/_escape.py +204 -0
- docwrap-0.1.0/src/docwrap/_core/_incremental.py +402 -0
- docwrap-0.1.0/src/docwrap/_core/_normalize.py +306 -0
- docwrap-0.1.0/src/docwrap/_core/_parse.py +594 -0
- docwrap-0.1.0/src/docwrap/_core/_process.py +826 -0
- docwrap-0.1.0/src/docwrap/_core/_wrap.py +1714 -0
- docwrap-0.1.0/src/docwrap/_detect.py +98 -0
- docwrap-0.1.0/src/docwrap/_detector.py +166 -0
- docwrap-0.1.0/src/docwrap/_display/__init__.py +17 -0
- docwrap-0.1.0/src/docwrap/_display/_diff.py +62 -0
- docwrap-0.1.0/src/docwrap/_display/_json.py +168 -0
- docwrap-0.1.0/src/docwrap/_display/_print.py +93 -0
- docwrap-0.1.0/src/docwrap/_display/_recommend.py +165 -0
- docwrap-0.1.0/src/docwrap/_display/_skips.py +17 -0
- docwrap-0.1.0/src/docwrap/_display/_stats.py +1075 -0
- docwrap-0.1.0/src/docwrap/_display/_summary.py +213 -0
- docwrap-0.1.0/src/docwrap/_display/_text.py +17 -0
- docwrap-0.1.0/src/docwrap/_downstream.py +181 -0
- docwrap-0.1.0/src/docwrap/_exceptions.py +52 -0
- docwrap-0.1.0/src/docwrap/_files.py +304 -0
- docwrap-0.1.0/src/docwrap/_git.py +764 -0
- docwrap-0.1.0/src/docwrap/_git_utils.py +76 -0
- docwrap-0.1.0/src/docwrap/_grammar.py +800 -0
- docwrap-0.1.0/src/docwrap/_hook.py +1001 -0
- docwrap-0.1.0/src/docwrap/_hosts/__init__.py +50 -0
- docwrap-0.1.0/src/docwrap/_shell.py +73 -0
- docwrap-0.1.0/src/docwrap/_shellparse.py +643 -0
- docwrap-0.1.0/src/docwrap/_stats.py +529 -0
- docwrap-0.1.0/src/docwrap/_styles/__init__.py +335 -0
- docwrap-0.1.0/src/docwrap/_styles/_google.py +423 -0
- docwrap-0.1.0/src/docwrap/_styles/_numpy.py +292 -0
- docwrap-0.1.0/src/docwrap/_styles/_sphinx.py +324 -0
- docwrap-0.1.0/src/docwrap/_types.py +423 -0
- docwrap-0.1.0/src/docwrap/_validate.py +62 -0
- docwrap-0.1.0/src/docwrap/hooks/__init__.py +245 -0
- docwrap-0.1.0/src/docwrap/py.typed +0 -0
docwrap-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
|
docwrap-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the docwrap 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.
|
docwrap-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: docwrap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Wrap Python docstrings with support for Google, NumPy, and Sphinx styles
|
|
5
|
+
Project-URL: Documentation, https://github.com/dactylo/docwrap/tree/main/docs
|
|
6
|
+
Project-URL: Homepage, https://github.com/dactylo/docwrap
|
|
7
|
+
Project-URL: Issues, https://github.com/dactylo/docwrap/issues
|
|
8
|
+
Project-URL: Repository, https://github.com/dactylo/docwrap
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: docstring,formatter,google,numpy,sphinx,wrap
|
|
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
|
+
# docwrap
|
|
21
|
+
|
|
22
|
+
[](https://github.com/dactylo/docwrap/actions/workflows/ci.yml)
|
|
23
|
+
|
|
24
|
+
Docwrap applies configurable formatting rules to Python docstrings. It makes wrapping and layout
|
|
25
|
+
deterministic, with controls for line length, paragraph reflow, and summary placement. It supports
|
|
26
|
+
Google, NumPy, and Sphinx/reST conventions.
|
|
27
|
+
|
|
28
|
+
Run `docwrap` to see which docstrings would change across your project:
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
Then use `docwrap --preview` to compare the original and proposed formatting. For example, using
|
|
33
|
+
the default 79-column limit:
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Docwrap requires CPython 3.13 or later. From a checkout, install the command with
|
|
40
|
+
[uv](https://docs.astral.sh/uv/):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv tool install .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
To update an existing installation after pulling changes:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv tool install --reinstall .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Git is needed for agent hooks and incremental mode; uv is needed for these installation commands,
|
|
53
|
+
not at runtime.
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
Start with a summary, preview individual docstrings, inspect the diff, then apply the changes:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
docwrap
|
|
61
|
+
docwrap --preview
|
|
62
|
+
docwrap --diff
|
|
63
|
+
docwrap --write
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Paths default to the current directory. Pass a file or directory to limit the scope.
|
|
67
|
+
|
|
68
|
+
Use `docwrap --stats` to inspect line-length distributions, compare wrapping targets, and see
|
|
69
|
+
recommendations.
|
|
70
|
+
|
|
71
|
+
Without `--write`, docwrap leaves files alone. It exits 0 when no changes are pending or a write
|
|
72
|
+
succeeds, 1 when changes are pending, and 2 on an error. Start with the defaults: 79 columns,
|
|
73
|
+
`smart` filling, cautious reflow, and no summary wrapping. To change the width for a project, add:
|
|
74
|
+
|
|
75
|
+
```toml
|
|
76
|
+
[tool.docwrap]
|
|
77
|
+
line-length = 88
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Configuration is discovered from each file's `pyproject.toml`. The
|
|
81
|
+
[wrapping policy guide](docs/design.md#choosing-a-wrapping-policy) explains the tradeoffs between
|
|
82
|
+
reflowing prose and preserving authored layout. See the [CLI reference](docs/cli.md) for output
|
|
83
|
+
modes, configuration precedence, and advanced options.
|
|
84
|
+
|
|
85
|
+
## Integrations and library use
|
|
86
|
+
|
|
87
|
+
A local [pre-commit](https://pre-commit.com/) hook can run docwrap when you commit. See the
|
|
88
|
+
[pre-commit setup](docs/hook.md#pre-commit) for a writing hook and its report-only variant. Agent
|
|
89
|
+
hooks for Claude Code, Codex, and Antigravity can run before commits or after edits. Their
|
|
90
|
+
installation, host settings, commit behavior, and composition limits are in the
|
|
91
|
+
[agent hook guide](docs/hook.md).
|
|
92
|
+
|
|
93
|
+
For Python callers, `wrap_source()` returns rewritten source, `check()` reports whether it would
|
|
94
|
+
change, and `wrap_source_report()` supplies structured results:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import docwrap
|
|
98
|
+
|
|
99
|
+
source = 'def greet():\n """Return a greeting."""\n'
|
|
100
|
+
result = docwrap.check(source)
|
|
101
|
+
wrapped = docwrap.wrap_source(source)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
See the [library reference](docs/library.md) for file operations, configuration, result types, and
|
|
105
|
+
safety skips.
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
|
|
109
|
+
- [CLI reference](docs/cli.md): commands, output, configuration, and exit codes.
|
|
110
|
+
- [Agent hook guide](docs/hook.md): setup, commit handling, and host responses.
|
|
111
|
+
- [Incremental mode](docs/incremental.md): wrap only changed files or definitions.
|
|
112
|
+
- [Library reference](docs/library.md): supported Python API and result contracts.
|
|
113
|
+
- [Concepts and safety](docs/design.md): styles, wrapping choices, and preservation rules.
|
|
114
|
+
- [Platform compatibility](docs/platform.md): supported environments and file behavior.
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
See the [development guide](docs/development.md) for pinned CI dependencies and lock updates.
|
|
119
|
+
|
|
120
|
+
From the checkout, run `make check` for repository checks. Run `make format` to apply the
|
|
121
|
+
repository's formatters. The Makefile also provides `make install`, `make reinstall`, and
|
|
122
|
+
`make test`; install targets use uv.
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
Docwrap is available under the [MIT License](LICENSE).
|
docwrap-0.1.0/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# docwrap
|
|
2
|
+
|
|
3
|
+
[](https://github.com/dactylo/docwrap/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Docwrap applies configurable formatting rules to Python docstrings. It makes wrapping and layout
|
|
6
|
+
deterministic, with controls for line length, paragraph reflow, and summary placement. It supports
|
|
7
|
+
Google, NumPy, and Sphinx/reST conventions.
|
|
8
|
+
|
|
9
|
+
Run `docwrap` to see which docstrings would change across your project:
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
Then use `docwrap --preview` to compare the original and proposed formatting. For example, using
|
|
14
|
+
the default 79-column limit:
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
Docwrap requires CPython 3.13 or later. From a checkout, install the command with
|
|
21
|
+
[uv](https://docs.astral.sh/uv/):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
To update an existing installation after pulling changes:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv tool install --reinstall .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Git is needed for agent hooks and incremental mode; uv is needed for these installation commands,
|
|
34
|
+
not at runtime.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
Start with a summary, preview individual docstrings, inspect the diff, then apply the changes:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
docwrap
|
|
42
|
+
docwrap --preview
|
|
43
|
+
docwrap --diff
|
|
44
|
+
docwrap --write
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Paths default to the current directory. Pass a file or directory to limit the scope.
|
|
48
|
+
|
|
49
|
+
Use `docwrap --stats` to inspect line-length distributions, compare wrapping targets, and see
|
|
50
|
+
recommendations.
|
|
51
|
+
|
|
52
|
+
Without `--write`, docwrap leaves files alone. It exits 0 when no changes are pending or a write
|
|
53
|
+
succeeds, 1 when changes are pending, and 2 on an error. Start with the defaults: 79 columns,
|
|
54
|
+
`smart` filling, cautious reflow, and no summary wrapping. To change the width for a project, add:
|
|
55
|
+
|
|
56
|
+
```toml
|
|
57
|
+
[tool.docwrap]
|
|
58
|
+
line-length = 88
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Configuration is discovered from each file's `pyproject.toml`. The
|
|
62
|
+
[wrapping policy guide](docs/design.md#choosing-a-wrapping-policy) explains the tradeoffs between
|
|
63
|
+
reflowing prose and preserving authored layout. See the [CLI reference](docs/cli.md) for output
|
|
64
|
+
modes, configuration precedence, and advanced options.
|
|
65
|
+
|
|
66
|
+
## Integrations and library use
|
|
67
|
+
|
|
68
|
+
A local [pre-commit](https://pre-commit.com/) hook can run docwrap when you commit. See the
|
|
69
|
+
[pre-commit setup](docs/hook.md#pre-commit) for a writing hook and its report-only variant. Agent
|
|
70
|
+
hooks for Claude Code, Codex, and Antigravity can run before commits or after edits. Their
|
|
71
|
+
installation, host settings, commit behavior, and composition limits are in the
|
|
72
|
+
[agent hook guide](docs/hook.md).
|
|
73
|
+
|
|
74
|
+
For Python callers, `wrap_source()` returns rewritten source, `check()` reports whether it would
|
|
75
|
+
change, and `wrap_source_report()` supplies structured results:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import docwrap
|
|
79
|
+
|
|
80
|
+
source = 'def greet():\n """Return a greeting."""\n'
|
|
81
|
+
result = docwrap.check(source)
|
|
82
|
+
wrapped = docwrap.wrap_source(source)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
See the [library reference](docs/library.md) for file operations, configuration, result types, and
|
|
86
|
+
safety skips.
|
|
87
|
+
|
|
88
|
+
## Documentation
|
|
89
|
+
|
|
90
|
+
- [CLI reference](docs/cli.md): commands, output, configuration, and exit codes.
|
|
91
|
+
- [Agent hook guide](docs/hook.md): setup, commit handling, and host responses.
|
|
92
|
+
- [Incremental mode](docs/incremental.md): wrap only changed files or definitions.
|
|
93
|
+
- [Library reference](docs/library.md): supported Python API and result contracts.
|
|
94
|
+
- [Concepts and safety](docs/design.md): styles, wrapping choices, and preservation rules.
|
|
95
|
+
- [Platform compatibility](docs/platform.md): supported environments and file behavior.
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
See the [development guide](docs/development.md) for pinned CI dependencies and lock updates.
|
|
100
|
+
|
|
101
|
+
From the checkout, run `make check` for repository checks. Run `make format` to apply the
|
|
102
|
+
repository's formatters. The Makefile also provides `make install`, `make reinstall`, and
|
|
103
|
+
`make test`; install targets use uv.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
Docwrap is available under the [MIT License](LICENSE).
|
docwrap-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
|
+
"""Provide the private Antigravity adapter implementation."""
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Translate Antigravity hook events for docwrap."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from docwrap.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`, `workspacePaths`, `cwd`, 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 `toolCall` 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 `toolCall.name` and `toolCall.args` 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 `--event`.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Neutral input; a tool other than `run_command` under `PreToolUse`
|
|
68
|
+
or other than the file writers under `PostToolUse` yields input
|
|
69
|
+
with nothing to act on.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: If `event` is not an Antigravity hook event.
|
|
73
|
+
"""
|
|
74
|
+
neutral_event = _EVENTS.get(event or '')
|
|
75
|
+
if neutral_event is None:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f'unknown Antigravity event {event!r}; expected PreToolUse or PostToolUse'
|
|
78
|
+
)
|
|
79
|
+
tool_call = object_field(payload, 'toolCall')
|
|
80
|
+
args = object_field(tool_call, 'args')
|
|
81
|
+
cwd = _base_cwd(payload, args)
|
|
82
|
+
name = tool_call.get('name')
|
|
83
|
+
if neutral_event == 'pre_tool':
|
|
84
|
+
command = args.get('CommandLine') if name == _COMMAND_TOOL else None
|
|
85
|
+
command_cwd = absolute_directory(args, 'Cwd') if name == _COMMAND_TOOL else None
|
|
86
|
+
return HostInput(
|
|
87
|
+
'pre_tool',
|
|
88
|
+
command if isinstance(command, str) else None,
|
|
89
|
+
cwd,
|
|
90
|
+
command_cwd,
|
|
91
|
+
)
|
|
92
|
+
editing = isinstance(name, str) and name in _EDIT_TOOLS
|
|
93
|
+
target = args.get('TargetFile') if editing else None
|
|
94
|
+
paths = (target,) if isinstance(target, str) and target else ()
|
|
95
|
+
return HostInput('post_tool', None, cwd, None, paths)
|
|
96
|
+
|
|
97
|
+
def render(self, result: HookResult) -> HostResponse:
|
|
98
|
+
"""Render allow feedback or a denial.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
result: Decision, reason, and notes from docwrap.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Empty stdout for a silent allow, or one system message for allow
|
|
105
|
+
feedback; notes on stderr and exit 0. For deny, the deny JSON on
|
|
106
|
+
stdout, the reason on stderr, and exit 0.
|
|
107
|
+
"""
|
|
108
|
+
notes = notes_text(result)
|
|
109
|
+
if result.decision == 'deny':
|
|
110
|
+
return _deny(result.reason or 'docwrap denied the operation', notes)
|
|
111
|
+
return _allow_response(result)
|
|
112
|
+
|
|
113
|
+
def render_failure(self, reason: str) -> HostResponse:
|
|
114
|
+
"""Render the top-level deny object.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
reason: Reason for the failure.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The deny object on stdout, the reason on stderr, and exit 0.
|
|
121
|
+
"""
|
|
122
|
+
return _deny(reason)
|
|
@@ -0,0 +1,135 @@
|
|
|
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 docwrap._cli import _build_hook_parser
|
|
12
|
+
from docwrap._hosts import load_adapter
|
|
13
|
+
from docwrap.hooks import API_VERSION, HookAdapter, HookResult, HostInput, HostResponse
|
|
14
|
+
from plugins.agy._docwrap._adapter import AntigravityAdapter
|
|
15
|
+
|
|
16
|
+
ROOT = Path(__file__).resolve().parents[3]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class _Result:
|
|
21
|
+
decision: Literal['allow', 'deny']
|
|
22
|
+
reason: str | None = None
|
|
23
|
+
notes: tuple[str, ...] = ()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _payload(tool: str, args: dict[str, object]) -> dict[str, object]:
|
|
27
|
+
return {'toolCall': {'name': tool, 'args': args}}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _result(decision: str, reason: str | None = None, notes: tuple[str, ...] = ()) -> HookResult:
|
|
31
|
+
return _Result('deny' if decision == 'deny' else 'allow', reason, notes)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestAntigravityAdapter:
|
|
35
|
+
def test_contract(self) -> None:
|
|
36
|
+
adapter: HookAdapter = AntigravityAdapter()
|
|
37
|
+
assert (adapter.name, adapter.api_version, adapter.requires_event) == (
|
|
38
|
+
'agy',
|
|
39
|
+
API_VERSION,
|
|
40
|
+
True,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize('tool', ['write_to_file', 'replace_file_content'])
|
|
44
|
+
def test_parse_post_tool_file_writers(self, tool: str) -> None:
|
|
45
|
+
payload = _payload(tool, {'TargetFile': '/t/a.py', 'Cwd': '/t'})
|
|
46
|
+
assert AntigravityAdapter().parse(payload, 'PostToolUse') == HostInput(
|
|
47
|
+
'post_tool', None, '/t', None, ('/t/a.py',)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def test_parse_pre_tool_other_tool_has_no_command(self) -> None:
|
|
51
|
+
payload = _payload('write_to_file', {'TargetFile': 'a.py', 'Cwd': '/t'})
|
|
52
|
+
assert AntigravityAdapter().parse(payload, 'PreToolUse') == HostInput(
|
|
53
|
+
'pre_tool', None, '/t', None
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def test_parse_rejects_unknown_event(self) -> None:
|
|
57
|
+
with pytest.raises(ValueError, match='unknown Antigravity event'):
|
|
58
|
+
AntigravityAdapter().parse({}, 'Stop')
|
|
59
|
+
|
|
60
|
+
@pytest.mark.parametrize('cwd', ['rel', '', 7])
|
|
61
|
+
def test_parse_run_command_non_absolute_cwd_is_unknown(self, cwd: object) -> None:
|
|
62
|
+
payload = _payload('run_command', {'CommandLine': 'git commit -m x', 'Cwd': cwd})
|
|
63
|
+
payload['workspacePaths'] = ['/ws']
|
|
64
|
+
parsed = AntigravityAdapter().parse(payload, 'PreToolUse')
|
|
65
|
+
assert parsed is not None
|
|
66
|
+
assert parsed.command_cwd is None
|
|
67
|
+
|
|
68
|
+
def test_parse_run_command_prefers_cwd_over_workspace(self, tmp_path: Path) -> None:
|
|
69
|
+
cwd = str(tmp_path / 'cwd')
|
|
70
|
+
payload = _payload('run_command', {'CommandLine': 'git commit -m x', 'Cwd': cwd})
|
|
71
|
+
payload['workspacePaths'] = [str(tmp_path / 'workspace')]
|
|
72
|
+
assert AntigravityAdapter().parse(payload, 'PreToolUse') == HostInput(
|
|
73
|
+
'pre_tool', 'git commit -m x', cwd, cwd
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def test_parse_run_command_without_cwd_is_unknown(self) -> None:
|
|
77
|
+
payload = _payload('run_command', {'CommandLine': 'git commit -m x'})
|
|
78
|
+
payload['workspacePaths'] = ['/ws']
|
|
79
|
+
assert AntigravityAdapter().parse(payload, 'PreToolUse') == HostInput(
|
|
80
|
+
'pre_tool', 'git commit -m x', '/ws', None
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def test_render_allow_and_failure(self) -> None:
|
|
84
|
+
assert AntigravityAdapter().render(_result('allow')) == HostResponse('', '', 0)
|
|
85
|
+
failure = AntigravityAdapter().render_failure('docwrap: reason')
|
|
86
|
+
assert json.loads(failure.stdout) == {'decision': 'deny', 'reason': 'docwrap: reason'}
|
|
87
|
+
assert (failure.stderr, failure.exit_code) == ('docwrap: reason\n', 0)
|
|
88
|
+
|
|
89
|
+
def test_render_allow_feedback_is_one_system_message(self) -> None:
|
|
90
|
+
response = AntigravityAdapter().render(_result('allow', 'first\nsecond', ('diagnostic',)))
|
|
91
|
+
assert json.loads(response.stdout) == {'systemMessage': 'first\nsecond'}
|
|
92
|
+
assert (response.stderr, response.exit_code) == ('diagnostic\n', 0)
|
|
93
|
+
|
|
94
|
+
def test_render_deny_is_top_level_json(self) -> None:
|
|
95
|
+
response = AntigravityAdapter().render(_result('deny', 'docwrap: reason', ('note',)))
|
|
96
|
+
assert json.loads(response.stdout) == {'decision': 'deny', 'reason': 'docwrap: reason'}
|
|
97
|
+
assert (response.stderr, response.exit_code) == ('note\ndocwrap: reason\n', 0)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.usefixtures('registered_hosts')
|
|
101
|
+
def test_native_manifest_command_loads_this_adapter() -> None:
|
|
102
|
+
argv = shlex.split('docwrap hook --agent agy --event PreToolUse')
|
|
103
|
+
args = _build_hook_parser().parse_args(argv[2:])
|
|
104
|
+
adapter = load_adapter(args.agent)
|
|
105
|
+
assert adapter.name == 'agy'
|
|
106
|
+
assert args.event == 'PreToolUse'
|
|
107
|
+
assert adapter.requires_event
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_native_manifest_is_unchanged() -> None:
|
|
111
|
+
assert json.loads((ROOT / 'plugins' / 'agy' / 'hooks.json').read_text(encoding='utf-8')) == {
|
|
112
|
+
'docwrap': {
|
|
113
|
+
'PreToolUse': [
|
|
114
|
+
{
|
|
115
|
+
'matcher': 'run_command',
|
|
116
|
+
'hooks': [
|
|
117
|
+
{
|
|
118
|
+
'type': 'command',
|
|
119
|
+
'command': 'docwrap hook --agent agy --event PreToolUse',
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_native_plugin_identity_and_layout() -> None:
|
|
129
|
+
plugin = json.loads((ROOT / 'plugins' / 'agy' / 'plugin.json').read_text(encoding='utf-8'))
|
|
130
|
+
assert set(plugin) == {'$schema', 'name', 'description'}
|
|
131
|
+
assert plugin['$schema'] == 'https://antigravity.google/schemas/v1/plugin.json'
|
|
132
|
+
assert plugin['name'] == 'docwrap-agy'
|
|
133
|
+
assert isinstance(plugin['description'], str)
|
|
134
|
+
assert plugin['description'].strip()
|
|
135
|
+
assert not (ROOT / 'plugins' / 'agy' / 'hooks').exists()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "docwrap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install the docwrap commit-time docstring wrapping hook for Claude Code.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "docwrap contributors"
|
|
7
|
+
},
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"keywords": ["docstrings", "formatting", "hooks", "python"]
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Provide the private Claude Code adapter implementation."""
|