contractme 2.3.0__tar.gz → 2.4.2__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.
- {contractme-2.3.0 → contractme-2.4.2}/.flake8 +1 -1
- {contractme-2.3.0 → contractme-2.4.2}/.gitignore +1 -0
- {contractme-2.3.0 → contractme-2.4.2}/.gitlab-ci.yml +79 -12
- {contractme-2.3.0 → contractme-2.4.2}/CHANGELOG.md +53 -0
- {contractme-2.3.0 → contractme-2.4.2}/CLAUDE.md +4 -0
- contractme-2.3.0/README.md → contractme-2.4.2/PKG-INFO +41 -8
- contractme-2.3.0/PKG-INFO → contractme-2.4.2/README.md +22 -27
- {contractme-2.3.0 → contractme-2.4.2}/justfile +5 -0
- {contractme-2.3.0 → contractme-2.4.2}/pyproject.toml +6 -2
- contractme-2.4.2/release_notes.md +8 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/annotations.py +25 -14
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/config.py +15 -2
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/contracting.py +215 -70
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/probing.py +3 -3
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/records.py +11 -14
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/skills/contractme/SKILL.md +22 -4
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/typecheck.py +204 -22
- {contractme-2.3.0 → contractme-2.4.2}/tests/conftest.py +2 -2
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_contracting.py +218 -1
- contractme-2.4.2/tests/test_evaluation_order.py +323 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_optimized_policy.py +61 -0
- contractme-2.4.2/tests/test_predicate_source.py +408 -0
- contractme-2.4.2/tox.ini +21 -0
- {contractme-2.3.0 → contractme-2.4.2}/uv.lock +186 -13
- {contractme-2.3.0 → contractme-2.4.2}/.pre-commit-config.yaml +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/.python-version +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/.vscode/settings.json +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/LICENSE +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/context7.json +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/docs/comparison-facts.yaml +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/docs/comparison.md +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/docs/types.md +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/release/prerelease.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/release/pypi_mint_token.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/release/release_notes.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/__init__.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/policy.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/py.typed +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/result.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/testing.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/__init__.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/containers.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/data.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/identifiers.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/network.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/numeric.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/paths.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/predicates.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/system.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/temporal.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/types/text.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/src/contractme/typeshape.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_annotations.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_annotations_future.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_async.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_canaries.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_contracting_optimized_python.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_hypothesis.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_hypothesis_async.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_layering.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_literal.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_policy.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_probing.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_records.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_reentrancy.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_show_source.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_typechecks.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_types.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_types_advanced.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_types_extra.py +0 -0
- {contractme-2.3.0 → contractme-2.4.2}/tests/test_typeshape.py +0 -0
|
@@ -4,4 +4,4 @@ max-line-length = 100
|
|
|
4
4
|
# E741: `l` is used on purpose as the list parameter in the design-by-contract
|
|
5
5
|
# examples (e.g. append_count(l, n)), mirroring the README
|
|
6
6
|
extend-ignore = E203, E741
|
|
7
|
-
exclude = .venv, obj, build, dist, .git, __pycache__
|
|
7
|
+
exclude = .venv, .uv-cache, obj, build, dist, .git, __pycache__
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
variables:
|
|
2
2
|
UV_VERSION: 0.9
|
|
3
|
-
|
|
3
|
+
# Matches pyproject's requires-python (>=3.13): the image ships the interpreter,
|
|
4
|
+
# so uv no longer downloads one in every job (saves ~10s per job).
|
|
5
|
+
PYTHON_VERSION: 3.13
|
|
4
6
|
BASE_LAYER: bookworm-slim
|
|
5
7
|
# GitLab CI creates a separate mountpoint for the build directory,
|
|
6
8
|
# so we need to copy instead of using hard links.
|
|
7
9
|
UV_LINK_MODE: copy
|
|
10
|
+
# Project-local so the `cache:` of .uv below can store it between jobs.
|
|
11
|
+
UV_CACHE_DIR: .uv-cache
|
|
12
|
+
|
|
13
|
+
# Pipelines exist only for MR events, main pushes and release tags. A new commit
|
|
14
|
+
# cancels the still-running `interruptible` jobs of the older pipeline, so a burst
|
|
15
|
+
# of pushes bills one test run instead of one per push.
|
|
16
|
+
workflow:
|
|
17
|
+
auto_cancel:
|
|
18
|
+
on_new_commit: interruptible
|
|
19
|
+
rules:
|
|
20
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
21
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
22
|
+
- if: $CI_COMMIT_TAG =~ /^v/
|
|
8
23
|
|
|
9
24
|
stages:
|
|
10
25
|
- test
|
|
@@ -17,16 +32,50 @@ stages:
|
|
|
17
32
|
rules:
|
|
18
33
|
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/
|
|
19
34
|
|
|
35
|
+
# Skip the test jobs on pushes that touch nothing the tests read (docs, README,
|
|
36
|
+
# CHANGELOG...): no minutes spent. release/ is linted and type-checked, hence listed.
|
|
37
|
+
.on-code-change:
|
|
38
|
+
rules:
|
|
39
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
40
|
+
changes: &code-paths
|
|
41
|
+
- src/**/*
|
|
42
|
+
- tests/**/*
|
|
43
|
+
- release/**/*
|
|
44
|
+
- pyproject.toml
|
|
45
|
+
- uv.lock
|
|
46
|
+
- justfile
|
|
47
|
+
- tox.ini
|
|
48
|
+
- .flake8
|
|
49
|
+
- .gitlab-ci.yml
|
|
50
|
+
when: always
|
|
51
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
52
|
+
changes: *code-paths
|
|
53
|
+
when: always
|
|
54
|
+
|
|
55
|
+
# The uv cache (wheels, hypothesis/pyright deps) is shared across jobs and
|
|
56
|
+
# pipelines, keyed on uv.lock: saves the re-download of every dependency each run.
|
|
57
|
+
# `uv cache prune --ci` drops what is cheap to rebuild so the cache stays small.
|
|
20
58
|
.uv:
|
|
21
59
|
image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
|
|
60
|
+
cache:
|
|
61
|
+
key:
|
|
62
|
+
files:
|
|
63
|
+
- uv.lock
|
|
64
|
+
paths:
|
|
65
|
+
- .uv-cache
|
|
66
|
+
after_script:
|
|
67
|
+
- uv cache prune --ci
|
|
22
68
|
|
|
23
69
|
# The scripts are one-liner justfile recipe calls (`uv run just ...`, the just
|
|
24
70
|
# binary comes from the dev deps) so CI runs exactly what `just check` runs
|
|
25
71
|
# locally; only the report/artifact flags are CI extras, passed through the
|
|
26
72
|
# recipes' *args.
|
|
27
73
|
checks:
|
|
28
|
-
extends:
|
|
74
|
+
extends:
|
|
75
|
+
- .uv
|
|
76
|
+
- .on-code-change
|
|
29
77
|
stage: test
|
|
78
|
+
interruptible: true
|
|
30
79
|
before_script:
|
|
31
80
|
- |
|
|
32
81
|
if echo "$CI_MERGE_REQUEST_TITLE" | grep -iq "^draft:\|^wip:"; then
|
|
@@ -61,11 +110,6 @@ checks:
|
|
|
61
110
|
coverage_format: cobertura
|
|
62
111
|
path: obj/cov.xml
|
|
63
112
|
junit: obj/tests_report.xml
|
|
64
|
-
rules:
|
|
65
|
-
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
66
|
-
when: always
|
|
67
|
-
- if: $CI_COMMIT_BRANCH == "main"
|
|
68
|
-
when: always
|
|
69
113
|
|
|
70
114
|
# Differential run: contracts are pure redundancy, so with them compiled out
|
|
71
115
|
# (python -O) the suite must still pass on valid inputs. Contract-violation
|
|
@@ -74,18 +118,41 @@ checks:
|
|
|
74
118
|
# The ignored warning is pytest noticing library asserts are off — which is
|
|
75
119
|
# exactly what this job is about.
|
|
76
120
|
checks-optimized:
|
|
77
|
-
extends:
|
|
121
|
+
extends:
|
|
122
|
+
- .uv
|
|
123
|
+
- .on-code-change
|
|
78
124
|
stage: test
|
|
125
|
+
interruptible: true
|
|
79
126
|
script:
|
|
80
127
|
- uv run just test-optimized --junitxml=obj/tests_report_optimized.xml
|
|
81
128
|
artifacts:
|
|
82
129
|
reports:
|
|
83
130
|
junit: obj/tests_report_optimized.xml
|
|
131
|
+
|
|
132
|
+
# Python 3.14: one extra job runs both version-sensitive gates
|
|
133
|
+
# (coverage + the -O run) through tox; lint and types stay on 3.13 in `checks`.
|
|
134
|
+
# UV_PYTHON makes the outer `uv run` use the image's interpreter instead of
|
|
135
|
+
# downloading .python-version's 3.13. A draft MR tolerates failures here, like
|
|
136
|
+
# the lax mode of `checks`.
|
|
137
|
+
checks-py314:
|
|
138
|
+
extends: .uv
|
|
139
|
+
stage: test
|
|
140
|
+
interruptible: true
|
|
141
|
+
variables:
|
|
142
|
+
PYTHON_VERSION: "3.14"
|
|
143
|
+
UV_PYTHON: "3.14"
|
|
84
144
|
rules:
|
|
85
|
-
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
145
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TITLE =~ /^(draft|wip):/i
|
|
146
|
+
changes: *code-paths
|
|
147
|
+
allow_failure: true
|
|
148
|
+
- !reference [.on-code-change, rules]
|
|
149
|
+
script:
|
|
150
|
+
- uv run just tox -e py314
|
|
151
|
+
artifacts:
|
|
152
|
+
reports:
|
|
153
|
+
junit:
|
|
154
|
+
- obj/py314/tests_report.xml
|
|
155
|
+
- obj/py314/tests_report_optimized.xml
|
|
89
156
|
|
|
90
157
|
# Validate the tag against pyproject + CHANGELOG, extract the release notes,
|
|
91
158
|
# then build dists.
|
|
@@ -7,6 +7,59 @@ section whose heading matches the tag via
|
|
|
7
7
|
and GitLab release description — so every `## vX.Y.Z` heading must match its
|
|
8
8
|
tag exactly.
|
|
9
9
|
|
|
10
|
+
## v2.4.2
|
|
11
|
+
|
|
12
|
+
* On Python 3.14, `@annotated` stacked over `@precondition` (or any contracted function
|
|
13
|
+
passed to `typing.get_type_hints`) works again: it raised
|
|
14
|
+
`NameError: name 'ClassVar' is not defined` at decoration since v2.4.0
|
|
15
|
+
([#104](https://gitlab.com/leogermond/contractme/-/issues/104)).
|
|
16
|
+
* The test suite runs on Python 3.13 and 3.14, locally (`just tox`) and in CI
|
|
17
|
+
([9b69657](https://gitlab.com/leogermond/contractme/-/commit/9b69657)).
|
|
18
|
+
* `just types` installs pyright with its bundled node, so a broken nodeenv no longer fails CI
|
|
19
|
+
([51e80c6](https://gitlab.com/leogermond/contractme/-/commit/51e80c6)).
|
|
20
|
+
|
|
21
|
+
## v2.4.1
|
|
22
|
+
|
|
23
|
+
* A failed `Predicate` constraint is named by its source when it is a lambda
|
|
24
|
+
(`Predicate(lambda s: s.islower())`), by its qualified name otherwise, in `type_error`,
|
|
25
|
+
`@record` and the `@annotated` header, where it used to print `<function <lambda> at 0x…>`;
|
|
26
|
+
a lambda with default arguments, or whose source cannot be proven to match, falls back to
|
|
27
|
+
its qualified name ([#80](https://gitlab.com/leogermond/contractme/-/issues/80)).
|
|
28
|
+
* A passing check and every `accepts`/`rejects` probe build no message, unions included:
|
|
29
|
+
the text is written only for the failure that gets reported
|
|
30
|
+
([#80](https://gitlab.com/leogermond/contractme/-/issues/80)).
|
|
31
|
+
* The `@annotated` header lists its constraints as `under constraints [...]`, where it used
|
|
32
|
+
to show a tuple ([#80](https://gitlab.com/leogermond/contractme/-/issues/80)).
|
|
33
|
+
* v2.4.0 never reached PyPI (its CI build failed on the in-project uv cache): v2.4.1 ships
|
|
34
|
+
its changes too, see below ([5739ee2](https://gitlab.com/leogermond/contractme/-/commit/5739ee2)).
|
|
35
|
+
|
|
36
|
+
## v2.4.0
|
|
37
|
+
|
|
38
|
+
* A decorator built with `functools.wraps` around a contracted function keeps its own
|
|
39
|
+
body: it used to be silently skipped, the contracted function running in its place
|
|
40
|
+
([#73](https://gitlab.com/leogermond/contractme/-/issues/73)).
|
|
41
|
+
* `@annotated` type checks now run before the explicit `@precondition`/`@postcondition`,
|
|
42
|
+
whatever the decorator order, so a wrong type is reported as such instead of a more
|
|
43
|
+
distant contract failing, or a precondition crashing on the ill-typed argument, or a
|
|
44
|
+
postcondition crashing on the ill-typed result under `observe`/`warn`
|
|
45
|
+
([#78](https://gitlab.com/leogermond/contractme/-/issues/78)).
|
|
46
|
+
* A callable `message=` receives the parameters it declares itself (`result` and
|
|
47
|
+
`old` included), independently of the predicate; it used to raise `KeyError` in
|
|
48
|
+
place of the violation
|
|
49
|
+
([#70](https://gitlab.com/leogermond/contractme/-/issues/70)).
|
|
50
|
+
* A callable `message=` naming a parameter the function lacks is refused with a
|
|
51
|
+
`TypeError` when the contract is declared, instead of a `KeyError` at its first
|
|
52
|
+
failure ([#70](https://gitlab.com/leogermond/contractme/-/issues/70)).
|
|
53
|
+
* A `functools.wraps` wrapper is told apart from the function it wraps by
|
|
54
|
+
`@no_contracts` and by the contract decorators: marking a wrapper of a contracted
|
|
55
|
+
function, or contracting a wrapper of a `@no_contracts` function, now works
|
|
56
|
+
([#74](https://gitlab.com/leogermond/contractme/-/issues/74)).
|
|
57
|
+
* A contracted function exposes `__name__`, `__qualname__` and `__doc__` to the type
|
|
58
|
+
checker, like a plain function: `step.__name__` passes pyright
|
|
59
|
+
([#77](https://gitlab.com/leogermond/contractme/-/issues/77)).
|
|
60
|
+
* CI spends fewer runner minutes (Python 3.13 image, uv cache, tests skipped on
|
|
61
|
+
doc-only pushes) ([5f70e0d](https://gitlab.com/leogermond/contractme/-/commit/5f70e0d)).
|
|
62
|
+
|
|
10
63
|
## v2.3.0
|
|
11
64
|
|
|
12
65
|
* Contracts now work on `async def`. `@precondition`, `@postcondition` and
|
|
@@ -24,6 +24,10 @@ just test-optimized # differential run: contracts compiled out via -O, valid-p
|
|
|
24
24
|
# behavior must be identical (mirrors the checks-optimized CI
|
|
25
25
|
# job). Contract-violation tests skip themselves via
|
|
26
26
|
# tests/conftest.py:contract_violation().
|
|
27
|
+
just tox [args...] # cov + test-optimized on Python 3.13 and 3.14 (tox.ini, tox-uv:
|
|
28
|
+
# each env synced from uv.lock's dev group); mirrors the
|
|
29
|
+
# checks-py314 CI job:
|
|
30
|
+
# just tox -e py314 -- -x
|
|
27
31
|
just fmt # format code (black)
|
|
28
32
|
just lint # black --check + flake8
|
|
29
33
|
just types # type checking (pinned pyright from the dev deps — matches CI)
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: contractme
|
|
3
|
+
Version: 2.4.2
|
|
4
|
+
Summary: Executable specifications for python. Keep control of the code AI writes for you.
|
|
5
|
+
Project-URL: Repository, https://gitlab.com/leogermond/contractme
|
|
6
|
+
Author-email: Leo Germond <leo.germond@gmail.com>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Requires-Dist: annotated-types>=0.7.0
|
|
11
|
+
Requires-Dist: hypothesis>=6.131.9
|
|
12
|
+
Provides-Extra: cron
|
|
13
|
+
Requires-Dist: croniter>=2.0.0; extra == 'cron'
|
|
14
|
+
Provides-Extra: iso
|
|
15
|
+
Requires-Dist: pycountry>=23.12.11; extra == 'iso'
|
|
16
|
+
Provides-Extra: yaml
|
|
17
|
+
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
1
20
|
# ContractMe — executable specifications for Python
|
|
2
21
|
|
|
3
22
|
## Code is free. Bugs are not. Write the spec instead.
|
|
@@ -50,11 +69,12 @@ ready-made types](docs/types.md) (`Port`, `EmailStr`, `Positive`, …).
|
|
|
50
69
|
|
|
51
70
|
```python
|
|
52
71
|
import math
|
|
53
|
-
from contractme import
|
|
72
|
+
from contractme import annotated, postcondition
|
|
73
|
+
from contractme.types import NonNegativeFloat
|
|
54
74
|
|
|
55
|
-
@
|
|
75
|
+
@annotated
|
|
56
76
|
@postcondition(lambda x, result: math.isclose(result * result, x, abs_tol=1e-12))
|
|
57
|
-
def square_root(x:
|
|
77
|
+
def square_root(x: NonNegativeFloat) -> NonNegativeFloat:
|
|
58
78
|
return x**0.5
|
|
59
79
|
```
|
|
60
80
|
|
|
@@ -65,13 +85,24 @@ it — this is the ground only contracts cover.
|
|
|
65
85
|
Rules can even compare the state before and after the call (`old`):
|
|
66
86
|
|
|
67
87
|
```python
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
88
|
+
from contractme import annotated, postcondition
|
|
89
|
+
from contractme.types import NonNegativeInt
|
|
90
|
+
|
|
91
|
+
@annotated
|
|
92
|
+
@postcondition(lambda l, n: l[-1] == n) # element appended
|
|
93
|
+
@postcondition(lambda l, old: l[:-1] == old.l) # nothing lost
|
|
94
|
+
def append_count(l: list[NonNegativeInt], n: NonNegativeInt):
|
|
72
95
|
l.append(n)
|
|
73
96
|
```
|
|
74
97
|
|
|
98
|
+
Stack `@annotated` and the explicit rules in any order: the type checks run
|
|
99
|
+
first — parameter types, in signature order, before the preconditions; the return
|
|
100
|
+
type before the postconditions — so a violation names its most immediate cause.
|
|
101
|
+
The types guard the relational rules of their side: when a parameter type fails,
|
|
102
|
+
it is reported and the preconditions stay unevaluated; when the return type fails,
|
|
103
|
+
it is reported and the postconditions stay unevaluated. Write the relational rules
|
|
104
|
+
for well-typed values only.
|
|
105
|
+
|
|
75
106
|
## When the generated code is wrong, you hear about it
|
|
76
107
|
|
|
77
108
|
Say your agent "implements" `square_root` as `return x / 2` — plausible at a
|
|
@@ -464,7 +495,9 @@ cost nothing where it matters.
|
|
|
464
495
|
`just test`
|
|
465
496
|
|
|
466
497
|
Dev commands are `just` recipes — bare `just` lists them; `just check` runs the
|
|
467
|
-
full local gate (tests + coverage, the `-O` differential run, lint, types)
|
|
498
|
+
full local gate (tests + coverage, the `-O` differential run, lint, types); `just tox`
|
|
499
|
+
runs the version-sensitive gates (coverage, the `-O` run) on every supported Python
|
|
500
|
+
(3.13, 3.14).
|
|
468
501
|
|
|
469
502
|
# Deploy new version
|
|
470
503
|
|
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: contractme
|
|
3
|
-
Version: 2.3.0
|
|
4
|
-
Summary: Executable specifications for python. Keep control of the code AI writes for you.
|
|
5
|
-
Project-URL: Repository, https://gitlab.com/leogermond/contractme
|
|
6
|
-
Author-email: Leo Germond <leo.germond@gmail.com>
|
|
7
|
-
License-Expression: Apache-2.0
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Python: >=3.13
|
|
10
|
-
Requires-Dist: annotated-types>=0.7.0
|
|
11
|
-
Requires-Dist: hypothesis>=6.131.9
|
|
12
|
-
Provides-Extra: cron
|
|
13
|
-
Requires-Dist: croniter>=2.0.0; extra == 'cron'
|
|
14
|
-
Provides-Extra: iso
|
|
15
|
-
Requires-Dist: pycountry>=23.12.11; extra == 'iso'
|
|
16
|
-
Provides-Extra: yaml
|
|
17
|
-
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
|
|
20
1
|
# ContractMe — executable specifications for Python
|
|
21
2
|
|
|
22
3
|
## Code is free. Bugs are not. Write the spec instead.
|
|
@@ -69,11 +50,12 @@ ready-made types](docs/types.md) (`Port`, `EmailStr`, `Positive`, …).
|
|
|
69
50
|
|
|
70
51
|
```python
|
|
71
52
|
import math
|
|
72
|
-
from contractme import
|
|
53
|
+
from contractme import annotated, postcondition
|
|
54
|
+
from contractme.types import NonNegativeFloat
|
|
73
55
|
|
|
74
|
-
@
|
|
56
|
+
@annotated
|
|
75
57
|
@postcondition(lambda x, result: math.isclose(result * result, x, abs_tol=1e-12))
|
|
76
|
-
def square_root(x:
|
|
58
|
+
def square_root(x: NonNegativeFloat) -> NonNegativeFloat:
|
|
77
59
|
return x**0.5
|
|
78
60
|
```
|
|
79
61
|
|
|
@@ -84,13 +66,24 @@ it — this is the ground only contracts cover.
|
|
|
84
66
|
Rules can even compare the state before and after the call (`old`):
|
|
85
67
|
|
|
86
68
|
```python
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
69
|
+
from contractme import annotated, postcondition
|
|
70
|
+
from contractme.types import NonNegativeInt
|
|
71
|
+
|
|
72
|
+
@annotated
|
|
73
|
+
@postcondition(lambda l, n: l[-1] == n) # element appended
|
|
74
|
+
@postcondition(lambda l, old: l[:-1] == old.l) # nothing lost
|
|
75
|
+
def append_count(l: list[NonNegativeInt], n: NonNegativeInt):
|
|
91
76
|
l.append(n)
|
|
92
77
|
```
|
|
93
78
|
|
|
79
|
+
Stack `@annotated` and the explicit rules in any order: the type checks run
|
|
80
|
+
first — parameter types, in signature order, before the preconditions; the return
|
|
81
|
+
type before the postconditions — so a violation names its most immediate cause.
|
|
82
|
+
The types guard the relational rules of their side: when a parameter type fails,
|
|
83
|
+
it is reported and the preconditions stay unevaluated; when the return type fails,
|
|
84
|
+
it is reported and the postconditions stay unevaluated. Write the relational rules
|
|
85
|
+
for well-typed values only.
|
|
86
|
+
|
|
94
87
|
## When the generated code is wrong, you hear about it
|
|
95
88
|
|
|
96
89
|
Say your agent "implements" `square_root` as `return x / 2` — plausible at a
|
|
@@ -483,7 +476,9 @@ cost nothing where it matters.
|
|
|
483
476
|
`just test`
|
|
484
477
|
|
|
485
478
|
Dev commands are `just` recipes — bare `just` lists them; `just check` runs the
|
|
486
|
-
full local gate (tests + coverage, the `-O` differential run, lint, types)
|
|
479
|
+
full local gate (tests + coverage, the `-O` differential run, lint, types); `just tox`
|
|
480
|
+
runs the version-sensitive gates (coverage, the `-O` run) on every supported Python
|
|
481
|
+
(3.13, 3.14).
|
|
487
482
|
|
|
488
483
|
# Deploy new version
|
|
489
484
|
|
|
@@ -24,6 +24,11 @@ cov *args:
|
|
|
24
24
|
test-optimized *args:
|
|
25
25
|
uv run python -O -m pytest -W ignore::pytest.PytestConfigWarning "$@"
|
|
26
26
|
|
|
27
|
+
# Envs and args pass through to tox (e.g. `just tox -e py314 -- -x`); see tox.ini.
|
|
28
|
+
[doc('Coverage gate + -O differential run on every supported Python (3.13, 3.14)')]
|
|
29
|
+
tox *args:
|
|
30
|
+
uv run tox "$@"
|
|
31
|
+
|
|
27
32
|
# Format the code (black, line length 100)
|
|
28
33
|
fmt:
|
|
29
34
|
uv run black .
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "contractme"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.2"
|
|
4
4
|
description = "Executable specifications for python. Keep control of the code AI writes for you."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -30,7 +30,9 @@ build-backend = "hatchling.build"
|
|
|
30
30
|
[dependency-groups]
|
|
31
31
|
dev = [
|
|
32
32
|
"epycs>=1.4.0",
|
|
33
|
-
|
|
33
|
+
# [nodejs] ships node as a wheel: pyright's nodeenv fallback crashes on
|
|
34
|
+
# some node release listings (CI image has no node of its own)
|
|
35
|
+
"pyright[nodejs]==1.1.407",
|
|
34
36
|
"pytest>=8.3.5",
|
|
35
37
|
"pytest-cov>=6.0.0",
|
|
36
38
|
"black>=25.9.0",
|
|
@@ -42,6 +44,8 @@ dev = [
|
|
|
42
44
|
"croniter>=2.0.0",
|
|
43
45
|
"pycountry>=23.12.11",
|
|
44
46
|
"pyyaml>=6.0",
|
|
47
|
+
"tox>=4.67.12",
|
|
48
|
+
"tox-uv>=1.37.5",
|
|
45
49
|
]
|
|
46
50
|
|
|
47
51
|
[tool.pytest.ini_options]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
* On Python 3.14, `@annotated` stacked over `@precondition` (or any contracted function
|
|
2
|
+
passed to `typing.get_type_hints`) works again: it raised
|
|
3
|
+
`NameError: name 'ClassVar' is not defined` at decoration since v2.4.0
|
|
4
|
+
([#104](https://gitlab.com/leogermond/contractme/-/issues/104)).
|
|
5
|
+
* The test suite runs on Python 3.13 and 3.14, locally (`just tox`) and in CI
|
|
6
|
+
([9b69657](https://gitlab.com/leogermond/contractme/-/commit/9b69657)).
|
|
7
|
+
* `just types` installs pyright with its bundled node, so a broken nodeenv no longer fails CI
|
|
8
|
+
([51e80c6](https://gitlab.com/leogermond/contractme/-/commit/51e80c6)).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Callable, cast, get_type_hints
|
|
2
|
-
from contractme.contracting import
|
|
3
|
-
from contractme.typecheck import type_error
|
|
2
|
+
from contractme.contracting import PRE, POST, ContractedFunction, condition_decorator
|
|
3
|
+
from contractme.typecheck import conforms, constraint_text, type_error
|
|
4
4
|
from contractme.typeshape import (
|
|
5
5
|
CompositeTypeInfo,
|
|
6
6
|
resolve_type,
|
|
@@ -16,9 +16,11 @@ def annotated[**F_Params, F_Ret](
|
|
|
16
16
|
For each parameter and the return value, ``@annotated`` auto-generates a
|
|
17
17
|
``@precondition`` (parameters) or ``@postcondition`` (return) that validates
|
|
18
18
|
both the type structure and any ``annotated-types`` constraints (``Ge``,
|
|
19
|
-
``Lt``, ``MinLen``, ``Predicate``, ...).
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
``Lt``, ``MinLen``, ``Predicate``, ...). These type checks run before the
|
|
20
|
+
explicit ``@precondition``/``@postcondition`` of the same function, whatever
|
|
21
|
+
the decorator order: a violation names its most immediate cause. Every type
|
|
22
|
+
shape :mod:`typeshape` models is supported — scalars, containers, unions,
|
|
23
|
+
tuples, dataclasses (field-by-field), enums, ``Literal`` alternatives, ``Protocol`` duck typing,
|
|
22
24
|
and recursive types.
|
|
23
25
|
|
|
24
26
|
Validation is two-phase: structure first ("is this a list?"), then
|
|
@@ -40,28 +42,35 @@ def annotated[**F_Params, F_Ret](
|
|
|
40
42
|
|
|
41
43
|
def check_annotated_arg(arg_name, arg_an: AnnotationForm):
|
|
42
44
|
info = CompositeTypeInfo(resolve_type(arg_an))
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
def header() -> str:
|
|
47
|
+
# built on failure only: naming a lambda by its source reads and
|
|
48
|
+
# compiles its module, a cost decoration (an import) never pays
|
|
49
|
+
msg = f"{arg_name} should be instance of {info.base}"
|
|
50
|
+
if info.constraints:
|
|
51
|
+
msg += f" under constraints [{', '.join(map(constraint_text, info.constraints))}]"
|
|
52
|
+
return msg
|
|
46
53
|
|
|
47
54
|
if arg_name == "return":
|
|
48
55
|
cond_arg_name = "result"
|
|
49
|
-
|
|
56
|
+
kind = POST
|
|
50
57
|
else:
|
|
51
58
|
cond_arg_name = arg_name
|
|
52
|
-
|
|
59
|
+
kind = PRE
|
|
53
60
|
|
|
54
61
|
def chk(**kw):
|
|
55
|
-
return
|
|
62
|
+
return conforms(kw[cond_arg_name], arg_an)
|
|
56
63
|
|
|
57
64
|
def describe_failure(**kw):
|
|
58
65
|
# only evaluated at failure time (by the raising path, never by the
|
|
59
66
|
# accepts/rejects probes), so re-running the check is affordable and
|
|
60
67
|
# the error is known to exist
|
|
61
68
|
err = type_error(kw[cond_arg_name], arg_an)
|
|
62
|
-
return f"{
|
|
69
|
+
return f"{header()}: {err}"
|
|
63
70
|
|
|
64
|
-
|
|
71
|
+
# a type check: it runs before the explicit pre/postconditions,
|
|
72
|
+
# whatever the decorator order (issue #78)
|
|
73
|
+
return condition_decorator(kind, chk, describe_failure, type_check=True)
|
|
65
74
|
|
|
66
75
|
# `include_extras=True` keeps the `Annotated[...]` metadata (plain
|
|
67
76
|
# `get_type_hints` strips it), and, crucially, `get_type_hints` *resolves*
|
|
@@ -70,7 +79,9 @@ def annotated[**F_Params, F_Ret](
|
|
|
70
79
|
# `f.__annotations__` would be strings and silently disable every check.
|
|
71
80
|
ans = get_type_hints(f, include_extras=True)
|
|
72
81
|
|
|
73
|
-
|
|
82
|
+
# each check prepends to the type checks: apply them last hint first, so the
|
|
83
|
+
# parameter types run in signature order
|
|
84
|
+
for n, an in reversed(ans.items()):
|
|
74
85
|
f = check_annotated_arg(n, an)(f)
|
|
75
86
|
|
|
76
87
|
# the loop is invisible statically: with any hint at all, `f` is now the
|
|
@@ -27,6 +27,7 @@ from __future__ import annotations
|
|
|
27
27
|
|
|
28
28
|
import os
|
|
29
29
|
from contextlib import contextmanager
|
|
30
|
+
from types import MethodType
|
|
30
31
|
from typing import Any, Callable
|
|
31
32
|
|
|
32
33
|
from contractme.policy import (
|
|
@@ -50,10 +51,22 @@ def no_contracts(f: Callable) -> Callable:
|
|
|
50
51
|
"""Mark ``f`` as never contractable: applying a contract to it raises at
|
|
51
52
|
decoration time. The engine's check-path helpers rely on it; also usable on
|
|
52
53
|
any function that must stay contract-free (hot paths, engine extensions).
|
|
54
|
+
|
|
55
|
+
Both ways are decided by *identity* (issue #74, as #73 for ``get_wrapped``):
|
|
56
|
+
``functools.wraps`` copies ``__dict__``, so a wrapper carries the wrapped
|
|
57
|
+
function's ``contractme_*`` attributes. "Already contracted" is the engine's
|
|
58
|
+
wrapper — its *class* carries ``contractme_engine_wrapper``, which ``wraps``
|
|
59
|
+
never copies — or, under ``optimized``, a function whose
|
|
60
|
+
``contractme_wrapped`` is itself. The mark stores the marked function, so it
|
|
61
|
+
designates that function alone. A bound method designates its function
|
|
62
|
+
(``__func__``, the very object, so identity still holds).
|
|
53
63
|
"""
|
|
54
|
-
if
|
|
64
|
+
target = f.__func__ if isinstance(f, MethodType) else f
|
|
65
|
+
if getattr(type(target), "contractme_engine_wrapper", False) or (
|
|
66
|
+
getattr(target, "contractme_wrapped", None) is target
|
|
67
|
+
):
|
|
55
68
|
raise RuntimeError(f"{f.__name__}() is already contracted")
|
|
56
|
-
setattr(f, "contractme_no_contracts",
|
|
69
|
+
setattr(f, "contractme_no_contracts", f)
|
|
57
70
|
return f
|
|
58
71
|
|
|
59
72
|
|