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 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