stipulate 0.1.0__py3-none-any.whl
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.
- stipulate/__init__.py +16 -0
- stipulate/_annotations.py +154 -0
- stipulate/_cache.py +73 -0
- stipulate/_compatibility.py +538 -0
- stipulate/_compile.py +311 -0
- stipulate/_contract.py +83 -0
- stipulate/_errors.py +76 -0
- stipulate/_evidence.py +112 -0
- stipulate/_relations.py +301 -0
- stipulate/_render.py +64 -0
- stipulate/_signatures.py +259 -0
- stipulate/py.typed +0 -0
- stipulate-0.1.0.dist-info/METADATA +153 -0
- stipulate-0.1.0.dist-info/RECORD +16 -0
- stipulate-0.1.0.dist-info/WHEEL +5 -0
- stipulate-0.1.0.dist-info/top_level.txt +1 -0
stipulate/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from ._compatibility import CompatibilityResult, CompatibilityStatus
|
|
2
|
+
from ._contract import Contract
|
|
3
|
+
from ._errors import ContractDefinitionError, ContractError, StipulateError
|
|
4
|
+
from ._evidence import DiagnosticRecord, Evidence, EvidenceStatus
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Contract",
|
|
8
|
+
"CompatibilityResult",
|
|
9
|
+
"CompatibilityStatus",
|
|
10
|
+
"Evidence",
|
|
11
|
+
"EvidenceStatus",
|
|
12
|
+
"DiagnosticRecord",
|
|
13
|
+
"StipulateError",
|
|
14
|
+
"ContractError",
|
|
15
|
+
"ContractDefinitionError",
|
|
16
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import builtins
|
|
5
|
+
import importlib
|
|
6
|
+
import inspect
|
|
7
|
+
import sys
|
|
8
|
+
import types
|
|
9
|
+
import typing
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from typing import Annotated, Callable, Literal, cast
|
|
12
|
+
|
|
13
|
+
from ._relations import class_dict, form_parts, inspect_missing, unresolved
|
|
14
|
+
|
|
15
|
+
Policy = Literal["trusted", "raw"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _forward_names(value: object, active: frozenset[int] = frozenset()) -> set[str]:
|
|
19
|
+
"""Collect possible forward-reference names without evaluating annotations."""
|
|
20
|
+
if id(value) in active:
|
|
21
|
+
return set()
|
|
22
|
+
active = active | {id(value)}
|
|
23
|
+
if isinstance(value, typing.ForwardRef):
|
|
24
|
+
return _forward_names(value.__forward_arg__, active)
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
try:
|
|
27
|
+
expression = ast.parse(value, mode="eval")
|
|
28
|
+
except SyntaxError:
|
|
29
|
+
return set()
|
|
30
|
+
expression_names = {node.id for node in ast.walk(expression) if isinstance(node, ast.Name)}
|
|
31
|
+
for node in ast.walk(expression):
|
|
32
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
33
|
+
expression_names.update(_forward_names(node.value, active))
|
|
34
|
+
return expression_names
|
|
35
|
+
origin, args = form_parts(value)
|
|
36
|
+
if origin is Literal:
|
|
37
|
+
return set()
|
|
38
|
+
if origin is Annotated:
|
|
39
|
+
args = args[:1]
|
|
40
|
+
names: set[str] = set()
|
|
41
|
+
for arg in args:
|
|
42
|
+
names.update(_forward_names(arg, active))
|
|
43
|
+
return names
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _contains_unresolved(value: object, active: frozenset[int] = frozenset()) -> bool:
|
|
47
|
+
if value is unresolved:
|
|
48
|
+
return True
|
|
49
|
+
if id(value) in active:
|
|
50
|
+
return False
|
|
51
|
+
origin, args = form_parts(value)
|
|
52
|
+
if origin is Literal:
|
|
53
|
+
return False
|
|
54
|
+
if origin is Annotated:
|
|
55
|
+
args = args[:1]
|
|
56
|
+
return any(_contains_unresolved(arg, active | {id(value)}) for arg in args)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _materialized(value: object) -> Mapping[str, object]:
|
|
60
|
+
if type(value) is not dict:
|
|
61
|
+
return {"__unresolved__": unresolved}
|
|
62
|
+
return cast(dict[str, object], value)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def annotation_map(obj: types.FunctionType | type[object], policy: Policy) -> Mapping[str, object]:
|
|
66
|
+
if isinstance(obj, types.FunctionType):
|
|
67
|
+
# Reading __annotate__ does not invoke it. Reading __annotations__ can.
|
|
68
|
+
if policy == "raw" and getattr(obj, "__annotate__", None) is not None:
|
|
69
|
+
return {"__unresolved__": unresolved}
|
|
70
|
+
try:
|
|
71
|
+
if getattr(obj, "__annotate__", None) is not None:
|
|
72
|
+
library = importlib.import_module("annotationlib")
|
|
73
|
+
getter = cast(Callable[..., Mapping[str, object]], library.get_annotations)
|
|
74
|
+
return getter(obj, format=library.Format.STRING)
|
|
75
|
+
return _materialized(obj.__annotations__)
|
|
76
|
+
except Exception:
|
|
77
|
+
# Exactly the annotation-factory boundary, not an engine-wide catch.
|
|
78
|
+
return {"__unresolved__": unresolved}
|
|
79
|
+
namespace = class_dict(obj)
|
|
80
|
+
if "__annotations__" in namespace:
|
|
81
|
+
return _materialized(namespace["__annotations__"])
|
|
82
|
+
if "__annotations_cache__" in namespace:
|
|
83
|
+
return _materialized(namespace["__annotations_cache__"])
|
|
84
|
+
if namespace.get("__annotate__") is not None or namespace.get("__annotate_func__") is not None:
|
|
85
|
+
if policy == "raw":
|
|
86
|
+
return {"__unresolved__": unresolved}
|
|
87
|
+
try:
|
|
88
|
+
library = importlib.import_module("annotationlib")
|
|
89
|
+
getter = cast(Callable[..., Mapping[str, object]], library.get_annotations)
|
|
90
|
+
return getter(obj, format=library.Format.STRING)
|
|
91
|
+
except Exception:
|
|
92
|
+
return {"__unresolved__": unresolved}
|
|
93
|
+
return {}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve(
|
|
97
|
+
value: object,
|
|
98
|
+
obj: types.FunctionType | type[object],
|
|
99
|
+
owner: type[object] | None,
|
|
100
|
+
policy: Policy,
|
|
101
|
+
globalns: Mapping[str, object] | None = None,
|
|
102
|
+
localns: Mapping[str, object] | None = None,
|
|
103
|
+
) -> object:
|
|
104
|
+
if value is inspect.Signature.empty:
|
|
105
|
+
return inspect_missing
|
|
106
|
+
if value is unresolved or value is inspect_missing:
|
|
107
|
+
return value
|
|
108
|
+
if policy == "raw":
|
|
109
|
+
return unresolved if isinstance(value, (str, typing.ForwardRef)) else value
|
|
110
|
+
globals_map: dict[str, object]
|
|
111
|
+
if isinstance(obj, types.FunctionType):
|
|
112
|
+
globals_map = dict(obj.__globals__)
|
|
113
|
+
else:
|
|
114
|
+
module = sys.modules.get(cast(str, class_dict(obj).get("__module__", "")))
|
|
115
|
+
globals_map = {} if module is None else dict(vars(module))
|
|
116
|
+
globals_map.update(globalns or {})
|
|
117
|
+
locals_map: dict[str, object]
|
|
118
|
+
if owner is None:
|
|
119
|
+
# Plain bindings also guard Python 3.14's optimized ForwardRef lookup,
|
|
120
|
+
# which can bypass dictionary __missing__ hooks. Owner-local precedence
|
|
121
|
+
# cannot be inferred from a global/builtin with the same spelling.
|
|
122
|
+
names = set(globals_map) | set(vars(builtins)) | _forward_names(value)
|
|
123
|
+
locals_map = {name: unresolved for name in names}
|
|
124
|
+
else:
|
|
125
|
+
locals_map = dict(class_dict(owner))
|
|
126
|
+
locals_map[cast(str, type.__getattribute__(owner, "__name__"))] = owner
|
|
127
|
+
locals_map.update(localns or {})
|
|
128
|
+
|
|
129
|
+
# A one-annotation carrier resolves each expression independently, with extras.
|
|
130
|
+
def carrier() -> None:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
carrier.__annotations__ = {"value": value}
|
|
134
|
+
try:
|
|
135
|
+
hints = typing.get_type_hints(
|
|
136
|
+
carrier, globalns=globals_map, localns=locals_map, include_extras=True
|
|
137
|
+
)
|
|
138
|
+
resolved = hints["value"]
|
|
139
|
+
return unresolved if owner is None and _contains_unresolved(resolved) else resolved
|
|
140
|
+
except Exception:
|
|
141
|
+
return unresolved
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def declared_annotation(
|
|
145
|
+
obj: types.FunctionType | type[object],
|
|
146
|
+
owner: type[object],
|
|
147
|
+
name: str,
|
|
148
|
+
policy: Policy,
|
|
149
|
+
globalns: Mapping[str, object] | None = None,
|
|
150
|
+
localns: Mapping[str, object] | None = None,
|
|
151
|
+
) -> object:
|
|
152
|
+
values = annotation_map(obj, policy)
|
|
153
|
+
value = values.get(name, values.get("__unresolved__", inspect_missing))
|
|
154
|
+
return resolve(value, obj, owner, policy, globalns, localns)
|
stipulate/_cache.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import weakref
|
|
5
|
+
from typing import Literal, cast
|
|
6
|
+
|
|
7
|
+
from ._compile import ContractIR, compile_contract
|
|
8
|
+
from ._relations import is_class
|
|
9
|
+
|
|
10
|
+
_lock = threading.RLock()
|
|
11
|
+
_cache: dict[
|
|
12
|
+
int,
|
|
13
|
+
tuple[
|
|
14
|
+
weakref.ReferenceType[type[object]],
|
|
15
|
+
dict[tuple[str], weakref.ReferenceType[ContractIR]],
|
|
16
|
+
],
|
|
17
|
+
] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _drop_declaration(cache_id: int, declaration_ref: weakref.ReferenceType[type[object]]) -> None:
|
|
21
|
+
with _lock:
|
|
22
|
+
entry = _cache.get(cache_id)
|
|
23
|
+
if entry is not None and entry[0] is declaration_ref:
|
|
24
|
+
_cache.pop(cache_id, None)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_or_compile(
|
|
28
|
+
declaration: object,
|
|
29
|
+
*,
|
|
30
|
+
policy: Literal["trusted", "raw"],
|
|
31
|
+
globalns: dict[str, object] | None,
|
|
32
|
+
localns: dict[str, object] | None,
|
|
33
|
+
refresh: bool,
|
|
34
|
+
) -> ContractIR:
|
|
35
|
+
if not is_class(declaration):
|
|
36
|
+
return compile_contract(declaration, policy=policy, globalns=globalns, localns=localns)
|
|
37
|
+
declaration = cast(type[object], declaration)
|
|
38
|
+
use_cache = globalns is None and localns is None
|
|
39
|
+
key = (policy,)
|
|
40
|
+
if use_cache and not refresh:
|
|
41
|
+
with _lock:
|
|
42
|
+
entry = _cache.get(id(declaration))
|
|
43
|
+
if entry is not None and entry[0]() is declaration:
|
|
44
|
+
ref = entry[1].get(key)
|
|
45
|
+
if ref is not None:
|
|
46
|
+
ir = ref()
|
|
47
|
+
if ir is not None:
|
|
48
|
+
return ir
|
|
49
|
+
elif entry is not None:
|
|
50
|
+
_cache.pop(id(declaration), None)
|
|
51
|
+
ir = compile_contract(declaration, policy=policy, globalns=globalns, localns=localns)
|
|
52
|
+
if use_cache:
|
|
53
|
+
with _lock:
|
|
54
|
+
if not refresh:
|
|
55
|
+
entry = _cache.get(id(declaration))
|
|
56
|
+
if entry is not None and entry[0]() is declaration:
|
|
57
|
+
ref = entry[1].get(key)
|
|
58
|
+
existing = None if ref is None else ref()
|
|
59
|
+
if existing is not None:
|
|
60
|
+
return existing
|
|
61
|
+
cache_id = id(declaration)
|
|
62
|
+
entry = _cache.get(cache_id)
|
|
63
|
+
if entry is None or entry[0]() is not declaration:
|
|
64
|
+
|
|
65
|
+
def on_collect(ref: weakref.ReferenceType[type[object]]) -> None:
|
|
66
|
+
_drop_declaration(cache_id, ref)
|
|
67
|
+
|
|
68
|
+
declaration_ref = weakref.ref(declaration, on_collect)
|
|
69
|
+
values: dict[tuple[str], weakref.ReferenceType[ContractIR]] = {}
|
|
70
|
+
entry = (declaration_ref, values)
|
|
71
|
+
_cache[cache_id] = entry
|
|
72
|
+
entry[1][key] = weakref.ref(ir)
|
|
73
|
+
return ir
|