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.
Files changed (71) hide show
  1. docwrap-0.1.0/.gitignore +46 -0
  2. docwrap-0.1.0/LICENSE +21 -0
  3. docwrap-0.1.0/PKG-INFO +126 -0
  4. docwrap-0.1.0/README.md +107 -0
  5. docwrap-0.1.0/_build.py +30 -0
  6. docwrap-0.1.0/plugins/agy/_docwrap/__init__.py +1 -0
  7. docwrap-0.1.0/plugins/agy/_docwrap/_adapter.py +122 -0
  8. docwrap-0.1.0/plugins/agy/adapter.toml +2 -0
  9. docwrap-0.1.0/plugins/agy/hooks.json +15 -0
  10. docwrap-0.1.0/plugins/agy/plugin.json +5 -0
  11. docwrap-0.1.0/plugins/agy/tests/test_agy_adapter.py +135 -0
  12. docwrap-0.1.0/plugins/claude/.claude-plugin/plugin.json +10 -0
  13. docwrap-0.1.0/plugins/claude/_docwrap/__init__.py +1 -0
  14. docwrap-0.1.0/plugins/claude/_docwrap/_adapter.py +106 -0
  15. docwrap-0.1.0/plugins/claude/adapter.toml +2 -0
  16. docwrap-0.1.0/plugins/claude/hooks/hooks.json +15 -0
  17. docwrap-0.1.0/plugins/claude/tests/test_claude_adapter.py +209 -0
  18. docwrap-0.1.0/plugins/codex/.codex-plugin/plugin.json +20 -0
  19. docwrap-0.1.0/plugins/codex/_docwrap/__init__.py +1 -0
  20. docwrap-0.1.0/plugins/codex/_docwrap/_adapter.py +117 -0
  21. docwrap-0.1.0/plugins/codex/adapter.toml +2 -0
  22. docwrap-0.1.0/plugins/codex/hooks/hooks.json +15 -0
  23. docwrap-0.1.0/plugins/codex/tests/test_codex_adapter.py +125 -0
  24. docwrap-0.1.0/pyproject.toml +161 -0
  25. docwrap-0.1.0/src/docwrap/__init__.py +59 -0
  26. docwrap-0.1.0/src/docwrap/__main__.py +8 -0
  27. docwrap-0.1.0/src/docwrap/_canonical.py +66 -0
  28. docwrap-0.1.0/src/docwrap/_classify.py +56 -0
  29. docwrap-0.1.0/src/docwrap/_cli.py +1284 -0
  30. docwrap-0.1.0/src/docwrap/_color.py +135 -0
  31. docwrap-0.1.0/src/docwrap/_config.py +615 -0
  32. docwrap-0.1.0/src/docwrap/_convert.py +1324 -0
  33. docwrap-0.1.0/src/docwrap/_core/__init__.py +27 -0
  34. docwrap-0.1.0/src/docwrap/_core/_codeblock.py +214 -0
  35. docwrap-0.1.0/src/docwrap/_core/_discover.py +466 -0
  36. docwrap-0.1.0/src/docwrap/_core/_escape.py +204 -0
  37. docwrap-0.1.0/src/docwrap/_core/_incremental.py +402 -0
  38. docwrap-0.1.0/src/docwrap/_core/_normalize.py +306 -0
  39. docwrap-0.1.0/src/docwrap/_core/_parse.py +594 -0
  40. docwrap-0.1.0/src/docwrap/_core/_process.py +826 -0
  41. docwrap-0.1.0/src/docwrap/_core/_wrap.py +1714 -0
  42. docwrap-0.1.0/src/docwrap/_detect.py +98 -0
  43. docwrap-0.1.0/src/docwrap/_detector.py +166 -0
  44. docwrap-0.1.0/src/docwrap/_display/__init__.py +17 -0
  45. docwrap-0.1.0/src/docwrap/_display/_diff.py +62 -0
  46. docwrap-0.1.0/src/docwrap/_display/_json.py +168 -0
  47. docwrap-0.1.0/src/docwrap/_display/_print.py +93 -0
  48. docwrap-0.1.0/src/docwrap/_display/_recommend.py +165 -0
  49. docwrap-0.1.0/src/docwrap/_display/_skips.py +17 -0
  50. docwrap-0.1.0/src/docwrap/_display/_stats.py +1075 -0
  51. docwrap-0.1.0/src/docwrap/_display/_summary.py +213 -0
  52. docwrap-0.1.0/src/docwrap/_display/_text.py +17 -0
  53. docwrap-0.1.0/src/docwrap/_downstream.py +181 -0
  54. docwrap-0.1.0/src/docwrap/_exceptions.py +52 -0
  55. docwrap-0.1.0/src/docwrap/_files.py +304 -0
  56. docwrap-0.1.0/src/docwrap/_git.py +764 -0
  57. docwrap-0.1.0/src/docwrap/_git_utils.py +76 -0
  58. docwrap-0.1.0/src/docwrap/_grammar.py +800 -0
  59. docwrap-0.1.0/src/docwrap/_hook.py +1001 -0
  60. docwrap-0.1.0/src/docwrap/_hosts/__init__.py +50 -0
  61. docwrap-0.1.0/src/docwrap/_shell.py +73 -0
  62. docwrap-0.1.0/src/docwrap/_shellparse.py +643 -0
  63. docwrap-0.1.0/src/docwrap/_stats.py +529 -0
  64. docwrap-0.1.0/src/docwrap/_styles/__init__.py +335 -0
  65. docwrap-0.1.0/src/docwrap/_styles/_google.py +423 -0
  66. docwrap-0.1.0/src/docwrap/_styles/_numpy.py +292 -0
  67. docwrap-0.1.0/src/docwrap/_styles/_sphinx.py +324 -0
  68. docwrap-0.1.0/src/docwrap/_types.py +423 -0
  69. docwrap-0.1.0/src/docwrap/_validate.py +62 -0
  70. docwrap-0.1.0/src/docwrap/hooks/__init__.py +245 -0
  71. docwrap-0.1.0/src/docwrap/py.typed +0 -0
@@ -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
+ [![CI](https://github.com/dactylo/docwrap/actions/workflows/ci.yml/badge.svg)](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
+ ![docwrap summary of six Python files, with 10 of 32 docstrings requiring wrapping](images/docwrap-summary.png)
31
+
32
+ Then use `docwrap --preview` to compare the original and proposed formatting. For example, using
33
+ the default 79-column limit:
34
+
35
+ ![docwrap preview showing prose and an argument description before and after reflow](images/docwrap-preview.png)
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).
@@ -0,0 +1,107 @@
1
+ # docwrap
2
+
3
+ [![CI](https://github.com/dactylo/docwrap/actions/workflows/ci.yml/badge.svg)](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
+ ![docwrap summary of six Python files, with 10 of 32 docstrings requiring wrapping](images/docwrap-summary.png)
12
+
13
+ Then use `docwrap --preview` to compare the original and proposed formatting. For example, using
14
+ the default 79-column limit:
15
+
16
+ ![docwrap preview showing prose and an argument description before and after reflow](images/docwrap-preview.png)
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).
@@ -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,2 @@
1
+ [entry-points."docwrap.hook_adapters"]
2
+ agy = "plugins.agy._docwrap._adapter:AntigravityAdapter"
@@ -0,0 +1,15 @@
1
+ {
2
+ "docwrap": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "run_command",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "docwrap hook --agent agy --event PreToolUse"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
+ "name": "docwrap-agy",
4
+ "description": "Install the docwrap commit-time docstring wrapping hook for Antigravity."
5
+ }
@@ -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."""