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.
Files changed (33) hide show
  1. pyintent-0.1.0/LICENSE +21 -0
  2. pyintent-0.1.0/PKG-INFO +255 -0
  3. pyintent-0.1.0/README.md +218 -0
  4. pyintent-0.1.0/pyintent/__init__.py +56 -0
  5. pyintent-0.1.0/pyintent/_discovery.py +105 -0
  6. pyintent-0.1.0/pyintent/_effects.py +166 -0
  7. pyintent-0.1.0/pyintent/_errors.py +15 -0
  8. pyintent-0.1.0/pyintent/_loader.py +56 -0
  9. pyintent-0.1.0/pyintent/_module_spec.py +68 -0
  10. pyintent-0.1.0/pyintent/_parser.py +198 -0
  11. pyintent-0.1.0/pyintent/_perf.py +49 -0
  12. pyintent-0.1.0/pyintent/_spec.py +375 -0
  13. pyintent-0.1.0/pyintent/cli.py +240 -0
  14. pyintent-0.1.0/pyintent/plugin.py +155 -0
  15. pyintent-0.1.0/pyintent/prompt.py +142 -0
  16. pyintent-0.1.0/pyintent/py.typed +0 -0
  17. pyintent-0.1.0/pyintent/verifier/__init__.py +64 -0
  18. pyintent-0.1.0/pyintent/verifier/_result.py +42 -0
  19. pyintent-0.1.0/pyintent/verifier/effects.py +169 -0
  20. pyintent-0.1.0/pyintent/verifier/examples.py +142 -0
  21. pyintent-0.1.0/pyintent/verifier/properties.py +196 -0
  22. pyintent-0.1.0/pyintent/verifier/types.py +50 -0
  23. pyintent-0.1.0/pyintent.egg-info/PKG-INFO +255 -0
  24. pyintent-0.1.0/pyintent.egg-info/SOURCES.txt +31 -0
  25. pyintent-0.1.0/pyintent.egg-info/dependency_links.txt +1 -0
  26. pyintent-0.1.0/pyintent.egg-info/entry_points.txt +5 -0
  27. pyintent-0.1.0/pyintent.egg-info/requires.txt +16 -0
  28. pyintent-0.1.0/pyintent.egg-info/top_level.txt +1 -0
  29. pyintent-0.1.0/pyproject.toml +66 -0
  30. pyintent-0.1.0/setup.cfg +4 -0
  31. pyintent-0.1.0/tests/test_parser.py +103 -0
  32. pyintent-0.1.0/tests/test_spec.py +164 -0
  33. 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.
@@ -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)
@@ -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