pyintent 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.
- pyintent-0.1.0/LICENSE +21 -0
- pyintent-0.1.0/PKG-INFO +255 -0
- pyintent-0.1.0/README.md +218 -0
- pyintent-0.1.0/pyintent/__init__.py +56 -0
- pyintent-0.1.0/pyintent/_discovery.py +105 -0
- pyintent-0.1.0/pyintent/_effects.py +166 -0
- pyintent-0.1.0/pyintent/_errors.py +15 -0
- pyintent-0.1.0/pyintent/_loader.py +56 -0
- pyintent-0.1.0/pyintent/_module_spec.py +68 -0
- pyintent-0.1.0/pyintent/_parser.py +198 -0
- pyintent-0.1.0/pyintent/_perf.py +49 -0
- pyintent-0.1.0/pyintent/_spec.py +375 -0
- pyintent-0.1.0/pyintent/cli.py +240 -0
- pyintent-0.1.0/pyintent/plugin.py +155 -0
- pyintent-0.1.0/pyintent/prompt.py +142 -0
- pyintent-0.1.0/pyintent/py.typed +0 -0
- pyintent-0.1.0/pyintent/verifier/__init__.py +64 -0
- pyintent-0.1.0/pyintent/verifier/_result.py +42 -0
- pyintent-0.1.0/pyintent/verifier/effects.py +169 -0
- pyintent-0.1.0/pyintent/verifier/examples.py +142 -0
- pyintent-0.1.0/pyintent/verifier/properties.py +196 -0
- pyintent-0.1.0/pyintent/verifier/types.py +50 -0
- pyintent-0.1.0/pyintent.egg-info/PKG-INFO +255 -0
- pyintent-0.1.0/pyintent.egg-info/SOURCES.txt +31 -0
- pyintent-0.1.0/pyintent.egg-info/dependency_links.txt +1 -0
- pyintent-0.1.0/pyintent.egg-info/entry_points.txt +5 -0
- pyintent-0.1.0/pyintent.egg-info/requires.txt +16 -0
- pyintent-0.1.0/pyintent.egg-info/top_level.txt +1 -0
- pyintent-0.1.0/pyproject.toml +66 -0
- pyintent-0.1.0/setup.cfg +4 -0
- pyintent-0.1.0/tests/test_parser.py +103 -0
- pyintent-0.1.0/tests/test_spec.py +164 -0
- pyintent-0.1.0/tests/test_verifier.py +397 -0
pyintent-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 pyintent contributors
|
|
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.
|
pyintent-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyintent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Verify that implementations satisfy intent specifications. A pure verifier for AI-generated Python code — it checks, it never generates.
|
|
5
|
+
Author: pyintent contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cshaley/pyintent
|
|
8
|
+
Project-URL: Repository, https://github.com/cshaley/pyintent
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/cshaley/pyintent/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/cshaley/pyintent/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: verification,specification,ai,codegen,contracts,testing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: click>=8.0
|
|
25
|
+
Requires-Dist: hypothesis>=6.0
|
|
26
|
+
Requires-Dist: pytest>=7.0
|
|
27
|
+
Provides-Extra: types
|
|
28
|
+
Requires-Dist: mypy>=1.0; extra == "types"
|
|
29
|
+
Provides-Extra: async
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "async"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
34
|
+
Provides-Extra: evals
|
|
35
|
+
Requires-Dist: openai>=1.40; extra == "evals"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# pyintent
|
|
39
|
+
|
|
40
|
+
Write your intent as a specification. Let any AI coding tool write the implementation. **pyintent verifies the implementation actually satisfies the intent.**
|
|
41
|
+
|
|
42
|
+
pyintent is a *pure verifier*. It never calls an LLM. Like `mypy` checks types without generating code, pyintent checks that an implementation matches its declared intent — examples, pre/post-conditions, effects, and types.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from pyintent import spec, reads, throws
|
|
46
|
+
|
|
47
|
+
@spec(
|
|
48
|
+
intent = "Return the order with the given id from the database.",
|
|
49
|
+
where = ["order_id > 0"],
|
|
50
|
+
ensures = ["result.id == order_id"],
|
|
51
|
+
effects = [reads("db"), throws(NotFoundError)],
|
|
52
|
+
ex = [
|
|
53
|
+
"(42,) -> _",
|
|
54
|
+
"(999,) -> raises NotFoundError",
|
|
55
|
+
"(0,) -> raises ValueError",
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
def find_order(order_id: int) -> Order:
|
|
59
|
+
... # implemented by Claude Code / Copilot / Devin / you
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pyintent verify myapp/orders.py # run all verifiers, human-readable report
|
|
66
|
+
pytest --pyintent # specs become pytest items automatically
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Install
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install pyintent # core (examples, properties, effects)
|
|
73
|
+
pip install pyintent[types] # + mypy integration
|
|
74
|
+
pip install pyintent[dev] # + mypy + pytest-asyncio
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from pyintent import spec, pure
|
|
81
|
+
|
|
82
|
+
@spec(
|
|
83
|
+
intent = "Return the absolute value of x.",
|
|
84
|
+
effects = [pure],
|
|
85
|
+
ensures = ["result >= 0", "result == x or result == -x"],
|
|
86
|
+
ex = ["(3,) -> 3", "(-4,) -> 4", "(0,) -> 0"],
|
|
87
|
+
)
|
|
88
|
+
def my_abs(x: int) -> int:
|
|
89
|
+
return x if x >= 0 else -x
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
$ pyintent verify mymodule.py
|
|
94
|
+
[PASS] examples my_abs (3,) -> 3
|
|
95
|
+
[PASS] examples my_abs (-4,) -> 4
|
|
96
|
+
[PASS] examples my_abs (0,) -> 0
|
|
97
|
+
[PASS] properties my_abs
|
|
98
|
+
[PASS] effects my_abs pure
|
|
99
|
+
[PASS] types mymodule.py
|
|
100
|
+
|
|
101
|
+
3 passed 0 failed 0 errored 0 skipped
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## The `@spec` decorator
|
|
105
|
+
|
|
106
|
+
`@spec` accepts these fields:
|
|
107
|
+
|
|
108
|
+
| Field | Type | Description |
|
|
109
|
+
|--------------|---------------------|-------------|
|
|
110
|
+
| `intent` | `str` (required) | One-line description of what the function does and why. |
|
|
111
|
+
| `where` | `list[str]` | Preconditions — Python expressions that must hold over the inputs. |
|
|
112
|
+
| `ensures` | `list[str]` | Postconditions — Python expressions over inputs and `result`. |
|
|
113
|
+
| `effects` | `list[Effect]` | Declared side-effects (see below). |
|
|
114
|
+
| `ex` | `list[str]` | Runnable examples in `"(args) -> expected"` format. |
|
|
115
|
+
| `perf` | `Perf` | Advisory complexity, e.g. `Perf(time="O(n)")`. |
|
|
116
|
+
| `invariants` | `list[str]` | Class/module-level invariants (plain strings or expressions). |
|
|
117
|
+
|
|
118
|
+
`@spec` must be the **outermost** decorator and returns the target **unchanged** — it only attaches metadata, so there is zero runtime overhead.
|
|
119
|
+
|
|
120
|
+
## Verifiers
|
|
121
|
+
|
|
122
|
+
### `examples` — run concrete cases
|
|
123
|
+
|
|
124
|
+
Each `ex` string has the format `"(args) -> expected"`:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
ex = [
|
|
128
|
+
"(1, 2) -> 3", # must return 3
|
|
129
|
+
"(0,) -> raises ValueError", # must raise ValueError
|
|
130
|
+
"('hi',) -> _", # wildcard: any return without raising
|
|
131
|
+
]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- The left side is a tuple literal (single-arg tuples need a trailing comma: `(42,)`).
|
|
135
|
+
- `raises ExcType` matches if the call raises that type or a subclass.
|
|
136
|
+
- `_` matches any non-raising return.
|
|
137
|
+
- Values are evaluated in the module's global namespace, so domain objects and enums resolve correctly.
|
|
138
|
+
|
|
139
|
+
### `properties` — hypothesis-based postcondition testing
|
|
140
|
+
|
|
141
|
+
For functions with `ensures` and no impure effects, pyintent generates inputs from type hints using [Hypothesis](https://hypothesis.readthedocs.io/), filters them through `where`, and asserts every `ensures` expression:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
@spec(
|
|
145
|
+
intent = "Sort a list of integers in ascending order.",
|
|
146
|
+
effects = [pure],
|
|
147
|
+
where = ["len(xs) < 1000"],
|
|
148
|
+
ensures = [
|
|
149
|
+
"len(result) == len(xs)",
|
|
150
|
+
"all(result[i] <= result[i+1] for i in range(len(result)-1))",
|
|
151
|
+
],
|
|
152
|
+
)
|
|
153
|
+
def sort_ints(xs: list[int]) -> list[int]:
|
|
154
|
+
return sorted(xs)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`ensures` expressions may reference input parameters and `result` (the return value).
|
|
158
|
+
|
|
159
|
+
### `types` — mypy integration
|
|
160
|
+
|
|
161
|
+
Runs `mypy` over the target file. Skipped gracefully if mypy is not installed. Install it with `pip install pyintent[types]`.
|
|
162
|
+
|
|
163
|
+
### `effects` — AST-based effect checking
|
|
164
|
+
|
|
165
|
+
Three effects are actively verified in v0.1:
|
|
166
|
+
|
|
167
|
+
| Effect | What is checked |
|
|
168
|
+
|--------|----------------|
|
|
169
|
+
| `pure` | No calls to impure builtins (`print`, `open`, …) or modules (`os`, `sys`, `random`, `requests`, …), no `global`/`nonlocal` writes. |
|
|
170
|
+
| `async_` | The function must be defined with `async def`. |
|
|
171
|
+
| `throws(ExcA, ExcB)` | Every explicitly raised exception type is declared. |
|
|
172
|
+
|
|
173
|
+
These effects are **declaration-only** (recorded but not verified):
|
|
174
|
+
`reads("db")`, `writes("cache")`, `network("stripe")`, `io`
|
|
175
|
+
|
|
176
|
+
A function may combine multiple effects:
|
|
177
|
+
```python
|
|
178
|
+
effects = [reads("db"), throws(NotFoundError, ValueError)]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## CLI usage
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Write the spec-authoring guide into your AI tool's prompt files
|
|
185
|
+
# (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
186
|
+
pyintent init
|
|
187
|
+
|
|
188
|
+
# Print the spec-authoring guide to stdout
|
|
189
|
+
pyintent prompt
|
|
190
|
+
|
|
191
|
+
# Validate spec structure by importing files (no execution)
|
|
192
|
+
pyintent check myapp/
|
|
193
|
+
|
|
194
|
+
# Require every public function to have a @spec
|
|
195
|
+
pyintent check --require-specs myapp/
|
|
196
|
+
|
|
197
|
+
# Run all verifiers and report results
|
|
198
|
+
pyintent verify myapp/orders.py
|
|
199
|
+
pyintent verify myapp/
|
|
200
|
+
|
|
201
|
+
# Machine-readable JSON output
|
|
202
|
+
pyintent verify --json myapp/ > results.json
|
|
203
|
+
|
|
204
|
+
# Run only specific verifiers
|
|
205
|
+
pyintent verify --only examples --only properties myapp/
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Exit codes: `0` all good, `1` verification failures, `2` usage or load error.
|
|
209
|
+
|
|
210
|
+
## pytest plugin
|
|
211
|
+
|
|
212
|
+
The pytest plugin is opt-in — installing pyintent does not change how existing `pytest` runs behave.
|
|
213
|
+
|
|
214
|
+
Enable it on the command line:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pytest --pyintent
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Or permanently in `pyproject.toml`:
|
|
221
|
+
|
|
222
|
+
```toml
|
|
223
|
+
[tool.pytest.ini_options]
|
|
224
|
+
pyintent = true
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Each spec becomes one or more pytest items:
|
|
228
|
+
|
|
229
|
+
- One item per `ex` case
|
|
230
|
+
- One item for property testing (if `ensures` is set)
|
|
231
|
+
- One item for the type check per file
|
|
232
|
+
|
|
233
|
+
## pyproject.toml configuration
|
|
234
|
+
|
|
235
|
+
```toml
|
|
236
|
+
[tool.pyintent]
|
|
237
|
+
require_specs = true # or "all" to also require class/module specs
|
|
238
|
+
exclude = ["migrations", "tests"]
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Safety
|
|
242
|
+
|
|
243
|
+
pyintent's `examples` and `properties` verifiers **execute the code under test** in the current Python process. That is fine for your own code — but pyintent's whole premise is checking code written by an AI tool, so treat that code as untrusted: review it, or run `pyintent verify` in a sandbox (container, VM, or restricted user), before running it on your machine.
|
|
244
|
+
|
|
245
|
+
## Status
|
|
246
|
+
|
|
247
|
+
v0.1. The following are planned for v0.2: generator/async-generator specs, `@overload`, instance-method example execution, Liskov enforcement of abstract-method contracts, performance measurement.
|
|
248
|
+
|
|
249
|
+
## Contributing
|
|
250
|
+
|
|
251
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
[MIT](LICENSE)
|
pyintent-0.1.0/README.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# pyintent
|
|
2
|
+
|
|
3
|
+
Write your intent as a specification. Let any AI coding tool write the implementation. **pyintent verifies the implementation actually satisfies the intent.**
|
|
4
|
+
|
|
5
|
+
pyintent is a *pure verifier*. It never calls an LLM. Like `mypy` checks types without generating code, pyintent checks that an implementation matches its declared intent — examples, pre/post-conditions, effects, and types.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from pyintent import spec, reads, throws
|
|
9
|
+
|
|
10
|
+
@spec(
|
|
11
|
+
intent = "Return the order with the given id from the database.",
|
|
12
|
+
where = ["order_id > 0"],
|
|
13
|
+
ensures = ["result.id == order_id"],
|
|
14
|
+
effects = [reads("db"), throws(NotFoundError)],
|
|
15
|
+
ex = [
|
|
16
|
+
"(42,) -> _",
|
|
17
|
+
"(999,) -> raises NotFoundError",
|
|
18
|
+
"(0,) -> raises ValueError",
|
|
19
|
+
],
|
|
20
|
+
)
|
|
21
|
+
def find_order(order_id: int) -> Order:
|
|
22
|
+
... # implemented by Claude Code / Copilot / Devin / you
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pyintent verify myapp/orders.py # run all verifiers, human-readable report
|
|
29
|
+
pytest --pyintent # specs become pytest items automatically
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install pyintent # core (examples, properties, effects)
|
|
36
|
+
pip install pyintent[types] # + mypy integration
|
|
37
|
+
pip install pyintent[dev] # + mypy + pytest-asyncio
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from pyintent import spec, pure
|
|
44
|
+
|
|
45
|
+
@spec(
|
|
46
|
+
intent = "Return the absolute value of x.",
|
|
47
|
+
effects = [pure],
|
|
48
|
+
ensures = ["result >= 0", "result == x or result == -x"],
|
|
49
|
+
ex = ["(3,) -> 3", "(-4,) -> 4", "(0,) -> 0"],
|
|
50
|
+
)
|
|
51
|
+
def my_abs(x: int) -> int:
|
|
52
|
+
return x if x >= 0 else -x
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
$ pyintent verify mymodule.py
|
|
57
|
+
[PASS] examples my_abs (3,) -> 3
|
|
58
|
+
[PASS] examples my_abs (-4,) -> 4
|
|
59
|
+
[PASS] examples my_abs (0,) -> 0
|
|
60
|
+
[PASS] properties my_abs
|
|
61
|
+
[PASS] effects my_abs pure
|
|
62
|
+
[PASS] types mymodule.py
|
|
63
|
+
|
|
64
|
+
3 passed 0 failed 0 errored 0 skipped
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## The `@spec` decorator
|
|
68
|
+
|
|
69
|
+
`@spec` accepts these fields:
|
|
70
|
+
|
|
71
|
+
| Field | Type | Description |
|
|
72
|
+
|--------------|---------------------|-------------|
|
|
73
|
+
| `intent` | `str` (required) | One-line description of what the function does and why. |
|
|
74
|
+
| `where` | `list[str]` | Preconditions — Python expressions that must hold over the inputs. |
|
|
75
|
+
| `ensures` | `list[str]` | Postconditions — Python expressions over inputs and `result`. |
|
|
76
|
+
| `effects` | `list[Effect]` | Declared side-effects (see below). |
|
|
77
|
+
| `ex` | `list[str]` | Runnable examples in `"(args) -> expected"` format. |
|
|
78
|
+
| `perf` | `Perf` | Advisory complexity, e.g. `Perf(time="O(n)")`. |
|
|
79
|
+
| `invariants` | `list[str]` | Class/module-level invariants (plain strings or expressions). |
|
|
80
|
+
|
|
81
|
+
`@spec` must be the **outermost** decorator and returns the target **unchanged** — it only attaches metadata, so there is zero runtime overhead.
|
|
82
|
+
|
|
83
|
+
## Verifiers
|
|
84
|
+
|
|
85
|
+
### `examples` — run concrete cases
|
|
86
|
+
|
|
87
|
+
Each `ex` string has the format `"(args) -> expected"`:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
ex = [
|
|
91
|
+
"(1, 2) -> 3", # must return 3
|
|
92
|
+
"(0,) -> raises ValueError", # must raise ValueError
|
|
93
|
+
"('hi',) -> _", # wildcard: any return without raising
|
|
94
|
+
]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- The left side is a tuple literal (single-arg tuples need a trailing comma: `(42,)`).
|
|
98
|
+
- `raises ExcType` matches if the call raises that type or a subclass.
|
|
99
|
+
- `_` matches any non-raising return.
|
|
100
|
+
- Values are evaluated in the module's global namespace, so domain objects and enums resolve correctly.
|
|
101
|
+
|
|
102
|
+
### `properties` — hypothesis-based postcondition testing
|
|
103
|
+
|
|
104
|
+
For functions with `ensures` and no impure effects, pyintent generates inputs from type hints using [Hypothesis](https://hypothesis.readthedocs.io/), filters them through `where`, and asserts every `ensures` expression:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
@spec(
|
|
108
|
+
intent = "Sort a list of integers in ascending order.",
|
|
109
|
+
effects = [pure],
|
|
110
|
+
where = ["len(xs) < 1000"],
|
|
111
|
+
ensures = [
|
|
112
|
+
"len(result) == len(xs)",
|
|
113
|
+
"all(result[i] <= result[i+1] for i in range(len(result)-1))",
|
|
114
|
+
],
|
|
115
|
+
)
|
|
116
|
+
def sort_ints(xs: list[int]) -> list[int]:
|
|
117
|
+
return sorted(xs)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`ensures` expressions may reference input parameters and `result` (the return value).
|
|
121
|
+
|
|
122
|
+
### `types` — mypy integration
|
|
123
|
+
|
|
124
|
+
Runs `mypy` over the target file. Skipped gracefully if mypy is not installed. Install it with `pip install pyintent[types]`.
|
|
125
|
+
|
|
126
|
+
### `effects` — AST-based effect checking
|
|
127
|
+
|
|
128
|
+
Three effects are actively verified in v0.1:
|
|
129
|
+
|
|
130
|
+
| Effect | What is checked |
|
|
131
|
+
|--------|----------------|
|
|
132
|
+
| `pure` | No calls to impure builtins (`print`, `open`, …) or modules (`os`, `sys`, `random`, `requests`, …), no `global`/`nonlocal` writes. |
|
|
133
|
+
| `async_` | The function must be defined with `async def`. |
|
|
134
|
+
| `throws(ExcA, ExcB)` | Every explicitly raised exception type is declared. |
|
|
135
|
+
|
|
136
|
+
These effects are **declaration-only** (recorded but not verified):
|
|
137
|
+
`reads("db")`, `writes("cache")`, `network("stripe")`, `io`
|
|
138
|
+
|
|
139
|
+
A function may combine multiple effects:
|
|
140
|
+
```python
|
|
141
|
+
effects = [reads("db"), throws(NotFoundError, ValueError)]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## CLI usage
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Write the spec-authoring guide into your AI tool's prompt files
|
|
148
|
+
# (AGENTS.md, CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
149
|
+
pyintent init
|
|
150
|
+
|
|
151
|
+
# Print the spec-authoring guide to stdout
|
|
152
|
+
pyintent prompt
|
|
153
|
+
|
|
154
|
+
# Validate spec structure by importing files (no execution)
|
|
155
|
+
pyintent check myapp/
|
|
156
|
+
|
|
157
|
+
# Require every public function to have a @spec
|
|
158
|
+
pyintent check --require-specs myapp/
|
|
159
|
+
|
|
160
|
+
# Run all verifiers and report results
|
|
161
|
+
pyintent verify myapp/orders.py
|
|
162
|
+
pyintent verify myapp/
|
|
163
|
+
|
|
164
|
+
# Machine-readable JSON output
|
|
165
|
+
pyintent verify --json myapp/ > results.json
|
|
166
|
+
|
|
167
|
+
# Run only specific verifiers
|
|
168
|
+
pyintent verify --only examples --only properties myapp/
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Exit codes: `0` all good, `1` verification failures, `2` usage or load error.
|
|
172
|
+
|
|
173
|
+
## pytest plugin
|
|
174
|
+
|
|
175
|
+
The pytest plugin is opt-in — installing pyintent does not change how existing `pytest` runs behave.
|
|
176
|
+
|
|
177
|
+
Enable it on the command line:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pytest --pyintent
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Or permanently in `pyproject.toml`:
|
|
184
|
+
|
|
185
|
+
```toml
|
|
186
|
+
[tool.pytest.ini_options]
|
|
187
|
+
pyintent = true
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Each spec becomes one or more pytest items:
|
|
191
|
+
|
|
192
|
+
- One item per `ex` case
|
|
193
|
+
- One item for property testing (if `ensures` is set)
|
|
194
|
+
- One item for the type check per file
|
|
195
|
+
|
|
196
|
+
## pyproject.toml configuration
|
|
197
|
+
|
|
198
|
+
```toml
|
|
199
|
+
[tool.pyintent]
|
|
200
|
+
require_specs = true # or "all" to also require class/module specs
|
|
201
|
+
exclude = ["migrations", "tests"]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Safety
|
|
205
|
+
|
|
206
|
+
pyintent's `examples` and `properties` verifiers **execute the code under test** in the current Python process. That is fine for your own code — but pyintent's whole premise is checking code written by an AI tool, so treat that code as untrusted: review it, or run `pyintent verify` in a sandbox (container, VM, or restricted user), before running it on your machine.
|
|
207
|
+
|
|
208
|
+
## Status
|
|
209
|
+
|
|
210
|
+
v0.1. The following are planned for v0.2: generator/async-generator specs, `@overload`, instance-method example execution, Liskov enforcement of abstract-method contracts, performance measurement.
|
|
211
|
+
|
|
212
|
+
## Contributing
|
|
213
|
+
|
|
214
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""pyintent — verify that implementations satisfy intent specifications.
|
|
2
|
+
|
|
3
|
+
A pure verifier: it checks, it never generates. See ``pyintent prompt`` for the
|
|
4
|
+
full spec-authoring reference.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ._effects import (
|
|
10
|
+
Effect,
|
|
11
|
+
EffectKind,
|
|
12
|
+
async_,
|
|
13
|
+
io,
|
|
14
|
+
network,
|
|
15
|
+
pure,
|
|
16
|
+
reads,
|
|
17
|
+
throws,
|
|
18
|
+
writes,
|
|
19
|
+
)
|
|
20
|
+
from ._errors import PyIntentError, PyIntentSpecError
|
|
21
|
+
from ._module_spec import module_spec, package_spec
|
|
22
|
+
from ._parser import Example
|
|
23
|
+
from ._perf import Perf
|
|
24
|
+
from ._spec import (
|
|
25
|
+
Invariant,
|
|
26
|
+
PyIntentSpec,
|
|
27
|
+
SpecLevel,
|
|
28
|
+
get_spec,
|
|
29
|
+
spec,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"spec",
|
|
36
|
+
"get_spec",
|
|
37
|
+
"module_spec",
|
|
38
|
+
"package_spec",
|
|
39
|
+
"Perf",
|
|
40
|
+
"pure",
|
|
41
|
+
"reads",
|
|
42
|
+
"writes",
|
|
43
|
+
"network",
|
|
44
|
+
"io",
|
|
45
|
+
"async_",
|
|
46
|
+
"throws",
|
|
47
|
+
"Effect",
|
|
48
|
+
"EffectKind",
|
|
49
|
+
"PyIntentSpec",
|
|
50
|
+
"SpecLevel",
|
|
51
|
+
"Invariant",
|
|
52
|
+
"Example",
|
|
53
|
+
"PyIntentError",
|
|
54
|
+
"PyIntentSpecError",
|
|
55
|
+
"__version__",
|
|
56
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Find every spec attached to a module's functions, classes, and methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ._module_spec import MODULE_ATTR
|
|
11
|
+
from ._spec import PyIntentSpec, SpecLevel, get_spec
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SpecTarget:
|
|
16
|
+
"""A discovered spec plus everything a verifier needs to act on it."""
|
|
17
|
+
|
|
18
|
+
qualname: str
|
|
19
|
+
spec: PyIntentSpec
|
|
20
|
+
globalns: dict[str, Any]
|
|
21
|
+
module_name: str
|
|
22
|
+
filename: str | None = None
|
|
23
|
+
invoke: Any | None = None # the callable to execute, when runnable
|
|
24
|
+
owner: type | None = None # the owning class, for methods
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def discover_in_module(module: ModuleType) -> list[SpecTarget]:
|
|
28
|
+
targets: list[SpecTarget] = []
|
|
29
|
+
globalns = vars(module)
|
|
30
|
+
modname = getattr(module, "__name__", "<module>")
|
|
31
|
+
filename = getattr(module, "__file__", None)
|
|
32
|
+
|
|
33
|
+
mod_spec = globalns.get(MODULE_ATTR)
|
|
34
|
+
if isinstance(mod_spec, PyIntentSpec):
|
|
35
|
+
targets.append(
|
|
36
|
+
SpecTarget(
|
|
37
|
+
qualname=modname,
|
|
38
|
+
spec=mod_spec,
|
|
39
|
+
globalns=globalns,
|
|
40
|
+
module_name=modname,
|
|
41
|
+
filename=filename,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
for name, obj in list(globalns.items()):
|
|
46
|
+
if inspect.isfunction(obj):
|
|
47
|
+
sp = get_spec(obj)
|
|
48
|
+
if sp is not None and getattr(obj, "__module__", None) == modname:
|
|
49
|
+
targets.append(
|
|
50
|
+
SpecTarget(
|
|
51
|
+
qualname=obj.__qualname__,
|
|
52
|
+
spec=sp,
|
|
53
|
+
globalns=globalns,
|
|
54
|
+
module_name=modname,
|
|
55
|
+
filename=filename,
|
|
56
|
+
invoke=obj,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
elif inspect.isclass(obj) and getattr(obj, "__module__", None) == modname:
|
|
60
|
+
csp = get_spec(obj)
|
|
61
|
+
if csp is not None:
|
|
62
|
+
targets.append(
|
|
63
|
+
SpecTarget(
|
|
64
|
+
qualname=obj.__qualname__,
|
|
65
|
+
spec=csp,
|
|
66
|
+
globalns=globalns,
|
|
67
|
+
module_name=modname,
|
|
68
|
+
filename=filename,
|
|
69
|
+
owner=obj,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
targets.extend(_discover_in_class(obj, globalns, modname, filename))
|
|
73
|
+
|
|
74
|
+
return targets
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _discover_in_class(
|
|
78
|
+
cls: type, globalns: dict[str, Any], modname: str, filename: str | None
|
|
79
|
+
) -> list[SpecTarget]:
|
|
80
|
+
targets: list[SpecTarget] = []
|
|
81
|
+
for name, member in list(vars(cls).items()):
|
|
82
|
+
sp = get_spec(member)
|
|
83
|
+
if sp is None:
|
|
84
|
+
continue
|
|
85
|
+
invoke: Any | None = None
|
|
86
|
+
if sp.level is SpecLevel.CLASSMETHOD:
|
|
87
|
+
invoke = getattr(cls, name) # bound to cls
|
|
88
|
+
elif sp.level is SpecLevel.STATICMETHOD:
|
|
89
|
+
invoke = member.__func__
|
|
90
|
+
elif sp.level is SpecLevel.PROPERTY:
|
|
91
|
+
invoke = member.fget
|
|
92
|
+
elif sp.level in (SpecLevel.METHOD, SpecLevel.ABSTRACT):
|
|
93
|
+
invoke = member
|
|
94
|
+
targets.append(
|
|
95
|
+
SpecTarget(
|
|
96
|
+
qualname=sp.target_name,
|
|
97
|
+
spec=sp,
|
|
98
|
+
globalns=globalns,
|
|
99
|
+
module_name=modname,
|
|
100
|
+
filename=filename,
|
|
101
|
+
invoke=invoke,
|
|
102
|
+
owner=cls,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
return targets
|