topkit 0.2.0a3__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.
TopKit/contracts.py ADDED
@@ -0,0 +1,360 @@
1
+ """Contracts: Preconditions gate the incoming Agent, Postconditions
2
+ promise about the finished one.
3
+
4
+ A condition is strictly boolean: True or a fall-through (None) holds,
5
+ False fails, anything else is rejected. An assert-style body fails by
6
+ raising.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+ from typing import Callable
13
+ from typing import Iterable
14
+
15
+ from .declarations import _protocol_inputs
16
+ from .declarations import _takes_underlay
17
+ from .errors import _Named
18
+ from .errors import TagContractError
19
+ from .errors import TagError
20
+ from .errors import TagPostconditionError
21
+ from .errors import TagPreconditionError
22
+ from .geometry import _leaves
23
+ from .state import _State
24
+ from .state import _name_of
25
+ from .state import _state_of
26
+
27
+
28
+ Check = Callable[[object, dict[str, Any]], Any]
29
+
30
+
31
+ def _verdict(
32
+ result: Any,
33
+ label: str,
34
+ ) -> bool:
35
+ if result is True or result is None:
36
+ return True
37
+
38
+ if result is False:
39
+ return False
40
+
41
+ raise TagContractError(
42
+ f"{label} returned {result!r} ({type(result).__name__}); a"
43
+ " condition must yield True, False, or None. TOP does not"
44
+ " coerce truthy / falsy values: write the comparison you mean,"
45
+ " such as `x != 0`, `x > 0`, or `x is not None`."
46
+ )
47
+
48
+
49
+ def _bind_condition(
50
+ function: Callable[..., Any],
51
+ prior: Check | None,
52
+ with_inputs: bool,
53
+ ) -> Check:
54
+ """Bind one condition, giving it its Underlay when marked.
55
+
56
+ The Underlay is a callable reporting whether the prior condition of the
57
+ same name holds (True / False), so ``assert base()`` and
58
+ ``return base() and ...`` both compose.
59
+ """
60
+
61
+ uses_underlay = _takes_underlay(function)
62
+
63
+ if uses_underlay and prior is None:
64
+ from .errors import TagResolutionError
65
+
66
+ raise TagResolutionError(
67
+ f"{function.__qualname__} is @Underlay but no prior"
68
+ " condition of that name is visible"
69
+ )
70
+
71
+ skip = 2 if uses_underlay else 1
72
+
73
+ def Check(
74
+ agent: object,
75
+ inputs: dict[str, Any],
76
+ ) -> Any:
77
+ named = (
78
+ _protocol_inputs(function, inputs, skip)
79
+ if with_inputs
80
+ else {}
81
+ )
82
+
83
+ if not uses_underlay:
84
+ return function(
85
+ agent,
86
+ **named,
87
+ )
88
+
89
+ def base() -> bool:
90
+ try:
91
+ return _verdict(
92
+ prior(agent, inputs),
93
+ "underlay",
94
+ )
95
+ except Exception:
96
+ return False
97
+
98
+ return function(
99
+ agent,
100
+ base,
101
+ **named,
102
+ )
103
+
104
+ return Check
105
+
106
+
107
+ def _evaluate(
108
+ checks: Iterable[tuple[str, Check]],
109
+ agent: object,
110
+ inputs: dict[str, Any],
111
+ failure: _Named,
112
+ phase: str,
113
+ ) -> None:
114
+ """Run conditions; raise ``failure`` naming the first that does not hold."""
115
+
116
+ for name, check in checks:
117
+ try:
118
+ result = check(agent, inputs)
119
+ except TagContractError:
120
+ raise
121
+ except Exception as error:
122
+ raise failure.Named(name)(
123
+ f"{phase} {name!r} raised {type(error).__name__}: {error}"
124
+ ) from error
125
+
126
+ if not _verdict(result, f"{phase} {name!r}"):
127
+ raise failure.Named(name)(
128
+ f"{phase} {name!r} failed"
129
+ )
130
+
131
+
132
+ def _guarded(
133
+ agent: object,
134
+ scope: str,
135
+ detailed: bool,
136
+ failure: _Named,
137
+ phase: str,
138
+ ) -> bool:
139
+ """Run one scope of the Agent's visible conditions on demand.
140
+
141
+ Re-entrancy guarded: a nested ``bool(agent)`` inside a condition
142
+ answers True instead of recursing.
143
+ """
144
+
145
+ state = _state_of(agent)
146
+
147
+ if state is None or state.checking:
148
+ return True
149
+
150
+ state.checking = True
151
+ state.composing += 1
152
+
153
+ try:
154
+ checks = getattr(state, scope).items()
155
+
156
+ if detailed:
157
+ _evaluate(
158
+ checks,
159
+ agent,
160
+ {},
161
+ failure,
162
+ phase,
163
+ )
164
+
165
+ return True
166
+
167
+ for name, check in checks:
168
+ try:
169
+ if not _verdict(check(agent, {}), name):
170
+ return False
171
+ except Exception:
172
+ return False
173
+
174
+ return True
175
+ finally:
176
+ state.composing -= 1
177
+ state.checking = False
178
+
179
+
180
+ def _holds(
181
+ agent: object,
182
+ ) -> bool:
183
+ """True exactly when every visible Postcondition holds."""
184
+
185
+ return _guarded(
186
+ agent,
187
+ "postconditions",
188
+ False,
189
+ TagPostconditionError,
190
+ "Postcondition",
191
+ )
192
+
193
+
194
+ def _status_of(
195
+ agent: object,
196
+ scope: str,
197
+ ) -> dict[str, bool]:
198
+ state = _state_of(agent)
199
+
200
+ if state is None:
201
+ return {}
202
+
203
+ reentrant = state.checking
204
+ state.checking = True
205
+ state.composing += 1
206
+ status: dict[str, bool] = {}
207
+
208
+ try:
209
+ for name, check in getattr(state, scope).items():
210
+ try:
211
+ status[name] = _verdict(
212
+ check(agent, {}),
213
+ name,
214
+ )
215
+ except Exception:
216
+ status[name] = False
217
+ finally:
218
+ state.composing -= 1
219
+ state.checking = reentrant
220
+
221
+ return status
222
+
223
+
224
+ def _title_of(
225
+ agent: object,
226
+ ) -> str:
227
+ state = _state_of(agent)
228
+ host = _name_of(agent)
229
+
230
+ if state is None or not state.active:
231
+ return host
232
+
233
+ leaves = ", ".join(
234
+ tag.__name__
235
+ for tag in _leaves(state.active)
236
+ )
237
+
238
+ return f"{host}[{leaves}]"
239
+
240
+
241
+ class Contract:
242
+ """Named, on-demand contract checks for an Agent.
243
+
244
+ ``bool(agent)`` is the boolean form. ``Contract`` names the culprit.
245
+ """
246
+
247
+ @staticmethod
248
+ def Holds(
249
+ agent: object,
250
+ ) -> bool:
251
+ return _holds(agent)
252
+
253
+ @staticmethod
254
+ def Postconditions(
255
+ agent: object,
256
+ ) -> bool:
257
+ return _guarded(
258
+ agent,
259
+ "postconditions",
260
+ True,
261
+ TagPostconditionError,
262
+ "Postcondition",
263
+ )
264
+
265
+ @staticmethod
266
+ def Preconditions(
267
+ agent: object,
268
+ ) -> bool:
269
+ return _guarded(
270
+ agent,
271
+ "preconditions",
272
+ True,
273
+ TagPreconditionError,
274
+ "Precondition",
275
+ )
276
+
277
+ @staticmethod
278
+ def Conditions(
279
+ agent: object,
280
+ ) -> bool:
281
+ Contract.Preconditions(agent)
282
+ Contract.Postconditions(agent)
283
+
284
+ return True
285
+
286
+ @staticmethod
287
+ def Delete(
288
+ agent: object,
289
+ *names: str,
290
+ ) -> None:
291
+ """End the named conditions on the Agent, explicitly.
292
+
293
+ Conditions are sticky: Rip does not remove them. A Tag whose gate
294
+ or promise should end with its membership deletes it here, from
295
+ its own ``@Rip`` protocol, one deliberate name at a time. A name
296
+ that is not a condition on the Agent is a Resolution Failure: an
297
+ author who ends a promise must be ending a real one.
298
+ """
299
+
300
+ from .errors import TagResolutionError
301
+
302
+ state = _state_of(agent)
303
+
304
+ for name in names:
305
+ found = False
306
+
307
+ for scope in (
308
+ state.preconditions if state is not None else {},
309
+ state.postconditions if state is not None else {},
310
+ ):
311
+ if name in scope:
312
+ del scope[name]
313
+ found = True
314
+
315
+ if not found:
316
+ raise TagResolutionError(
317
+ f"{name!r} is not a condition on {_name_of(agent)};"
318
+ " Contract.Delete ends a gate or a promise that is"
319
+ " there"
320
+ )
321
+
322
+ @staticmethod
323
+ def Status(
324
+ agent: object,
325
+ ) -> dict[str, bool]:
326
+ """``{condition: holds?}`` for every visible condition, never raising."""
327
+
328
+ return {
329
+ **_status_of(agent, "preconditions"),
330
+ **_status_of(agent, "postconditions"),
331
+ }
332
+
333
+ @staticmethod
334
+ def Display(
335
+ agent: object,
336
+ ) -> str:
337
+ title = _title_of(agent)
338
+ pre = _status_of(agent, "preconditions")
339
+ post = _status_of(agent, "postconditions")
340
+
341
+ if not pre and not post:
342
+ return f"{title}: no conditions"
343
+
344
+ lines = [f"{title} contract:"]
345
+
346
+ for heading, scope in (
347
+ ("Pre", pre),
348
+ ("Post", post),
349
+ ):
350
+ if not scope:
351
+ continue
352
+
353
+ lines.append(f" {heading}:")
354
+
355
+ for name, holds in scope.items():
356
+ lines.append(
357
+ f" {'OK' if holds else 'XX'} {name}"
358
+ )
359
+
360
+ return "\n".join(lines)