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/__init__.py +82 -0
- TopKit/access.py +357 -0
- TopKit/contracts.py +360 -0
- TopKit/declarations.py +1010 -0
- TopKit/errors.py +138 -0
- TopKit/fields.py +165 -0
- TopKit/geometry.py +120 -0
- TopKit/lifecycle.py +220 -0
- TopKit/overlay.py +791 -0
- TopKit/queries.py +90 -0
- TopKit/state.py +862 -0
- TopKit/tags.py +251 -0
- TopKit/transactions.py +474 -0
- topkit-0.2.0a3.dist-info/METADATA +136 -0
- topkit-0.2.0a3.dist-info/RECORD +18 -0
- topkit-0.2.0a3.dist-info/WHEEL +5 -0
- topkit-0.2.0a3.dist-info/licenses/LICENSE +202 -0
- topkit-0.2.0a3.dist-info/top_level.txt +1 -0
TopKit/overlay.py
ADDED
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
"""Overlay: installing one Tag's declarations into a candidate state.
|
|
2
|
+
|
|
3
|
+
The latest applied Layer is the visible Overlay for a name. Within one
|
|
4
|
+
scope a name is one slot: an Action or a Record, never both at once.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Any
|
|
11
|
+
from typing import Callable
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
from .contracts import _bind_condition
|
|
15
|
+
from .declarations import _Declarations
|
|
16
|
+
from .declarations import _is_flag
|
|
17
|
+
from .declarations import _parameters_of
|
|
18
|
+
from .declarations import _protocol_inputs
|
|
19
|
+
from .declarations import _takes_stored
|
|
20
|
+
from .declarations import _takes_underlay
|
|
21
|
+
from .errors import TagCompositionError
|
|
22
|
+
from .errors import TagDeclarationError
|
|
23
|
+
from .errors import TagError
|
|
24
|
+
from .errors import TagOverwriteWarning
|
|
25
|
+
from .errors import TagContractWarning
|
|
26
|
+
from .errors import TagResolutionError
|
|
27
|
+
from .declarations import STATE
|
|
28
|
+
from .declarations import Report
|
|
29
|
+
from .declarations import _declarations_of
|
|
30
|
+
from .geometry import _related
|
|
31
|
+
from .declarations import _MISSING
|
|
32
|
+
from .state import _Bound
|
|
33
|
+
from .state import _Pinned_Operation
|
|
34
|
+
from .state import _State
|
|
35
|
+
from .state import _namespace_of
|
|
36
|
+
from .state import _state_of
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
Function = Callable[..., Any]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
# Helpers
|
|
44
|
+
# ------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_tag_type(
|
|
48
|
+
candidate: object,
|
|
49
|
+
) -> bool:
|
|
50
|
+
from .tags import MetaTag
|
|
51
|
+
|
|
52
|
+
return isinstance(candidate, MetaTag)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _independent(
|
|
56
|
+
tag: type,
|
|
57
|
+
origin: type,
|
|
58
|
+
) -> bool:
|
|
59
|
+
"""True when ``origin`` is a Tag unrelated to ``tag`` (not in its Form)."""
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
_is_tag_type(origin)
|
|
63
|
+
and not _related(tag, origin)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _host_function(
|
|
68
|
+
host_type: type,
|
|
69
|
+
name: str,
|
|
70
|
+
) -> Function | None:
|
|
71
|
+
"""A plain callable the host class defines under ``name``, wrapped so it
|
|
72
|
+
can serve as an Underlay."""
|
|
73
|
+
|
|
74
|
+
for klass in host_type.__mro__:
|
|
75
|
+
attribute = klass.__dict__.get(name)
|
|
76
|
+
|
|
77
|
+
if attribute is None:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
if isinstance(attribute, (classmethod, staticmethod)):
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
if not callable(attribute):
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
@wraps(attribute)
|
|
87
|
+
def Host_Action(
|
|
88
|
+
agent: object,
|
|
89
|
+
*args: Any,
|
|
90
|
+
**kwargs: Any,
|
|
91
|
+
) -> Any:
|
|
92
|
+
return attribute(
|
|
93
|
+
agent,
|
|
94
|
+
*args,
|
|
95
|
+
**kwargs,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return Host_Action
|
|
99
|
+
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _host_data_descriptor(
|
|
104
|
+
host_type: type,
|
|
105
|
+
name: str,
|
|
106
|
+
) -> bool:
|
|
107
|
+
for klass in host_type.__mro__:
|
|
108
|
+
attribute = klass.__dict__.get(name)
|
|
109
|
+
|
|
110
|
+
if attribute is not None:
|
|
111
|
+
return hasattr(
|
|
112
|
+
type(attribute),
|
|
113
|
+
"__set__",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _compose(
|
|
120
|
+
function: Function,
|
|
121
|
+
underlay: Function | None,
|
|
122
|
+
) -> Function:
|
|
123
|
+
"""Bind an Action to its Underlay (when it asks for one)."""
|
|
124
|
+
|
|
125
|
+
uses_underlay = _takes_underlay(function)
|
|
126
|
+
|
|
127
|
+
if not uses_underlay:
|
|
128
|
+
return function
|
|
129
|
+
|
|
130
|
+
if underlay is None:
|
|
131
|
+
raise TagResolutionError(
|
|
132
|
+
f"{function.__qualname__} requires a visible Underlay"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@wraps(function)
|
|
136
|
+
def Call(
|
|
137
|
+
agent: object,
|
|
138
|
+
*args: Any,
|
|
139
|
+
**kwargs: Any,
|
|
140
|
+
) -> Any:
|
|
141
|
+
def prior(
|
|
142
|
+
*next_args: Any,
|
|
143
|
+
**next_kwargs: Any,
|
|
144
|
+
) -> Any:
|
|
145
|
+
if next_args or next_kwargs:
|
|
146
|
+
return underlay(
|
|
147
|
+
agent,
|
|
148
|
+
*next_args,
|
|
149
|
+
**next_kwargs,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return underlay(
|
|
153
|
+
agent,
|
|
154
|
+
*args,
|
|
155
|
+
**kwargs,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return function(
|
|
159
|
+
agent,
|
|
160
|
+
prior,
|
|
161
|
+
*args,
|
|
162
|
+
**kwargs,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return Call
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _adapter(
|
|
169
|
+
tag: type,
|
|
170
|
+
name: str,
|
|
171
|
+
operation: Function,
|
|
172
|
+
) -> Function:
|
|
173
|
+
"""The Action a Public Operation publishes: the Agent is passed to the
|
|
174
|
+
Operation as its second input. A published member answers members
|
|
175
|
+
only, so a Rogue Agent's call fails closed (STEP-SPEC-10)."""
|
|
176
|
+
|
|
177
|
+
def Published(
|
|
178
|
+
agent: object,
|
|
179
|
+
*args: Any,
|
|
180
|
+
**kwargs: Any,
|
|
181
|
+
) -> Any:
|
|
182
|
+
_require_membership(
|
|
183
|
+
agent,
|
|
184
|
+
tag,
|
|
185
|
+
name,
|
|
186
|
+
"Operation",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return operation(
|
|
190
|
+
tag,
|
|
191
|
+
agent,
|
|
192
|
+
*args,
|
|
193
|
+
**kwargs,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
Published.__name__ = name
|
|
197
|
+
Published.__qualname__ = f"{tag.__qualname__}.{name}"
|
|
198
|
+
Published.__doc__ = operation.__doc__
|
|
199
|
+
|
|
200
|
+
return Published
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _require_membership(
|
|
204
|
+
agent: object,
|
|
205
|
+
tag: type,
|
|
206
|
+
name: str,
|
|
207
|
+
kind: str,
|
|
208
|
+
) -> None:
|
|
209
|
+
"""A published member answers members only, and only sound ones
|
|
210
|
+
(STEP-SPEC-10): the Agent must still belong to the publishing Tag, and
|
|
211
|
+
every promise on the Agent must hold. A Rogue Agent raises a Rogue
|
|
212
|
+
Access Failure; a defective one raises the broken promise by name."""
|
|
213
|
+
|
|
214
|
+
from .contracts import _guarded
|
|
215
|
+
from .errors import TagPostconditionError
|
|
216
|
+
from .errors import TagRogueAccessError
|
|
217
|
+
from .state import _name_of
|
|
218
|
+
from .state import _state_of
|
|
219
|
+
|
|
220
|
+
state = _state_of(agent)
|
|
221
|
+
|
|
222
|
+
if state is None or tag not in state.active:
|
|
223
|
+
raise TagRogueAccessError(
|
|
224
|
+
f"{name!r} is a published {kind} of {tag.__name__};"
|
|
225
|
+
f" {_name_of(agent)} is a Rogue Agent of that Tag, and a"
|
|
226
|
+
" published member answers members only"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if state.postconditions:
|
|
230
|
+
_guarded(
|
|
231
|
+
agent,
|
|
232
|
+
"postconditions",
|
|
233
|
+
True,
|
|
234
|
+
TagPostconditionError,
|
|
235
|
+
"Postcondition",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
# Installing
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _install(
|
|
245
|
+
state: _State,
|
|
246
|
+
tag: type,
|
|
247
|
+
declarations: _Declarations,
|
|
248
|
+
) -> None:
|
|
249
|
+
"""Lay ``tag`` over ``state`` (a candidate copy). Order matters: deletions
|
|
250
|
+
free names first; conditions, Tag members, Actions, Records follow."""
|
|
251
|
+
|
|
252
|
+
if _is_flag(tag):
|
|
253
|
+
_refuse_container_host(
|
|
254
|
+
state,
|
|
255
|
+
tag,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
for name in declarations.deletions:
|
|
259
|
+
_delete(state, name)
|
|
260
|
+
|
|
261
|
+
for name, function in declarations.preconditions:
|
|
262
|
+
prior = state.preconditions.get(name)
|
|
263
|
+
state.preconditions[name] = _stamp(
|
|
264
|
+
_bind_condition(
|
|
265
|
+
function,
|
|
266
|
+
prior,
|
|
267
|
+
True,
|
|
268
|
+
),
|
|
269
|
+
tag,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
for name, function in declarations.postconditions:
|
|
273
|
+
prior = state.postconditions.get(name)
|
|
274
|
+
|
|
275
|
+
if (
|
|
276
|
+
prior is not None
|
|
277
|
+
and not _takes_underlay(function)
|
|
278
|
+
and _origin_of(prior) is not tag
|
|
279
|
+
):
|
|
280
|
+
warnings.warn(
|
|
281
|
+
f"{tag.__name__}.{name} overrides a Base Postcondition"
|
|
282
|
+
" without @Underlay (weakens a promise; see Forward-Post)",
|
|
283
|
+
TagContractWarning,
|
|
284
|
+
stacklevel=6,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
state.postconditions[name] = _stamp(
|
|
288
|
+
_bind_condition(
|
|
289
|
+
function,
|
|
290
|
+
prior,
|
|
291
|
+
False,
|
|
292
|
+
),
|
|
293
|
+
tag,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
for name, report, public in declarations.reports:
|
|
297
|
+
state.reports[name] = (tag, report)
|
|
298
|
+
|
|
299
|
+
if public:
|
|
300
|
+
state.published.add(name)
|
|
301
|
+
|
|
302
|
+
for name, operation, public in declarations.operations:
|
|
303
|
+
state.operations[name] = (tag, operation)
|
|
304
|
+
|
|
305
|
+
if public:
|
|
306
|
+
_install_action(
|
|
307
|
+
state,
|
|
308
|
+
tag,
|
|
309
|
+
name,
|
|
310
|
+
_adapter(tag, name, operation),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
for name, function in declarations.actions:
|
|
314
|
+
_install_action(
|
|
315
|
+
state,
|
|
316
|
+
tag,
|
|
317
|
+
name,
|
|
318
|
+
function,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
for name, builder in declarations.records:
|
|
322
|
+
_install_record(
|
|
323
|
+
state,
|
|
324
|
+
tag,
|
|
325
|
+
name,
|
|
326
|
+
builder,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
state.secrets.update(declarations.secrets)
|
|
330
|
+
|
|
331
|
+
if state.pinned is not None:
|
|
332
|
+
state.published.update(declarations.published)
|
|
333
|
+
|
|
334
|
+
if declarations.rips:
|
|
335
|
+
state.rips[tag] = tuple(
|
|
336
|
+
state.actions[name]
|
|
337
|
+
for name in declarations.rips
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _refuse_stored_input_collision(
|
|
342
|
+
builder: Function,
|
|
343
|
+
inputs: dict[str, Any],
|
|
344
|
+
) -> None:
|
|
345
|
+
"""A Record's second positional parameter is the stored value. If it is
|
|
346
|
+
named like a supplied input, the author almost certainly meant the
|
|
347
|
+
input; say so rather than hand over the stored value in silence."""
|
|
348
|
+
|
|
349
|
+
second = _parameters_of(builder).named[1][0]
|
|
350
|
+
|
|
351
|
+
if second in inputs:
|
|
352
|
+
raise TagDeclarationError(
|
|
353
|
+
f"{builder.__qualname__}: its second parameter {second!r} is"
|
|
354
|
+
f" the stored value, but an input named {second!r} was"
|
|
355
|
+
" supplied. Take the input by name after a `*`:"
|
|
356
|
+
f" `def {builder.__name__}(agent, *, {second})`, or rename"
|
|
357
|
+
" the stored parameter."
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _refuse_container_host(
|
|
362
|
+
state: _State,
|
|
363
|
+
tag: type,
|
|
364
|
+
) -> None:
|
|
365
|
+
from .access import _host_member
|
|
366
|
+
|
|
367
|
+
if state.pinned is not None:
|
|
368
|
+
return # on a Tag, TOP owns `in`: a string in it asks for a keyword
|
|
369
|
+
|
|
370
|
+
if _host_member(state.host_type, "__contains__") is not None:
|
|
371
|
+
raise TagCompositionError(
|
|
372
|
+
f"{tag.__name__} is a Flag, but the host"
|
|
373
|
+
f" {state.host_type.__name__} defines its own `in`; a"
|
|
374
|
+
" keyword needs that seat empty"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _stamp(
|
|
379
|
+
check: Function,
|
|
380
|
+
tag: type,
|
|
381
|
+
) -> Function:
|
|
382
|
+
"""Remember which Tag bound a condition, so a Tag re-applied after a
|
|
383
|
+
Rip replaces its own promise silently (§0.7). Conditions are sticky:
|
|
384
|
+
Rip never touches them; the author ends them (STEP-SPEC-12)."""
|
|
385
|
+
|
|
386
|
+
check.__topkit_origin__ = tag # type: ignore[attr-defined]
|
|
387
|
+
|
|
388
|
+
return check
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _origin_of(
|
|
392
|
+
check: Function,
|
|
393
|
+
) -> type | None:
|
|
394
|
+
return getattr(
|
|
395
|
+
check,
|
|
396
|
+
"__topkit_origin__",
|
|
397
|
+
None,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _own_declaration(
|
|
402
|
+
pinned: type,
|
|
403
|
+
name: str,
|
|
404
|
+
) -> tuple[type, str, Any] | None:
|
|
405
|
+
"""Where the pinned Tag itself declares ``name``, as what, and the
|
|
406
|
+
declared object: ("operation" | "report" | "value" | "agent" |
|
|
407
|
+
"protocol"). None when the name is absent or TOP-managed (landed by a
|
|
408
|
+
Pin), in which case the Overlay laws decide."""
|
|
409
|
+
|
|
410
|
+
for klass in pinned.__mro__:
|
|
411
|
+
if name not in klass.__dict__:
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
managed = klass.__dict__.get(STATE)
|
|
415
|
+
|
|
416
|
+
if managed is not None and (
|
|
417
|
+
name in managed.actions
|
|
418
|
+
or name in managed.records
|
|
419
|
+
):
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
declared = klass.__dict__[name]
|
|
423
|
+
|
|
424
|
+
if not hasattr(klass, "_topkit_field"):
|
|
425
|
+
return (klass, "protocol", declared)
|
|
426
|
+
|
|
427
|
+
declarations = _declarations_of(klass)
|
|
428
|
+
|
|
429
|
+
if any(name == n for n, _f, _p in declarations.operations):
|
|
430
|
+
return (klass, "operation", declared)
|
|
431
|
+
|
|
432
|
+
if any(name == n for n, _r, _p in declarations.reports):
|
|
433
|
+
return (klass, "report", declared)
|
|
434
|
+
|
|
435
|
+
if any(name == n for n, _f in declarations.actions) or any(
|
|
436
|
+
name == n for n, _f in declarations.records
|
|
437
|
+
):
|
|
438
|
+
return (klass, "agent", declared)
|
|
439
|
+
|
|
440
|
+
if (
|
|
441
|
+
any(name == n for n, _f in declarations.preconditions)
|
|
442
|
+
or any(name == n for n, _f in declarations.postconditions)
|
|
443
|
+
or any(name == n for n, _f in declarations.imprints)
|
|
444
|
+
or name in declarations.deletions
|
|
445
|
+
):
|
|
446
|
+
return (klass, "protocol", declared)
|
|
447
|
+
|
|
448
|
+
return (klass, "value", declared)
|
|
449
|
+
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _refuse_tag_member(
|
|
454
|
+
state: _State,
|
|
455
|
+
tag: type,
|
|
456
|
+
name: str,
|
|
457
|
+
) -> None:
|
|
458
|
+
"""Collision control for a Pin (STEP-SPEC-9 §4). A Pin's members are
|
|
459
|
+
Tag scope, so they may overlay what the Tag declares in Tag scope (an
|
|
460
|
+
Operation, a Report, a plain value) as a host member: silently, with
|
|
461
|
+
the declaration as Underlay or stored value. They may never take a
|
|
462
|
+
name the Tag declares in Agent scope or as a protocol: on a class the
|
|
463
|
+
two scopes share one dictionary, and the write would silently remove
|
|
464
|
+
the declaration from every future Agent's contract. Nor a name every
|
|
465
|
+
Tag answers through its metaclass."""
|
|
466
|
+
|
|
467
|
+
pinned = state.pinned
|
|
468
|
+
|
|
469
|
+
if hasattr(type(pinned), name):
|
|
470
|
+
raise TagCompositionError(
|
|
471
|
+
f"{tag.__name__}.{name}: a Pin may not name what every Tag"
|
|
472
|
+
f" already answers ({name!r} belongs to the Tag's metaclass)"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
found = _own_declaration(
|
|
476
|
+
pinned,
|
|
477
|
+
name,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
if found is None:
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
klass, kind, declared = found
|
|
484
|
+
|
|
485
|
+
if kind in ("operation", "report", "value"):
|
|
486
|
+
state.originals.setdefault(
|
|
487
|
+
name,
|
|
488
|
+
declared,
|
|
489
|
+
) # the first patch remembers the declaration
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
where = (
|
|
493
|
+
"itself"
|
|
494
|
+
if klass is pinned
|
|
495
|
+
else f"in its Base {klass.__name__}"
|
|
496
|
+
)
|
|
497
|
+
what = (
|
|
498
|
+
"an Agent-scope member"
|
|
499
|
+
if kind == "agent"
|
|
500
|
+
else "a protocol"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
raise TagCompositionError(
|
|
504
|
+
f"{tag.__name__}.{name}: {pinned.__name__} declares {name!r}"
|
|
505
|
+
f" {where} as {what}; a Pin overlays a Tag's Operations and"
|
|
506
|
+
" Reports, never its Agent members or protocols (STEP-SPEC-9 §4)"
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _pinned_host_function(
|
|
511
|
+
pinned: type,
|
|
512
|
+
name: str,
|
|
513
|
+
) -> Function | None:
|
|
514
|
+
"""The pinned Tag's own Operation under ``name``, as the Underlay of a
|
|
515
|
+
Pin Action: the patch can call the engine it replaces."""
|
|
516
|
+
|
|
517
|
+
found = _own_declaration(
|
|
518
|
+
pinned,
|
|
519
|
+
name,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
if found is None or found[1] != "operation":
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
operation = found[2].__func__
|
|
526
|
+
|
|
527
|
+
@wraps(operation)
|
|
528
|
+
def Host_Operation(
|
|
529
|
+
tag: type,
|
|
530
|
+
*args: Any,
|
|
531
|
+
**kwargs: Any,
|
|
532
|
+
) -> Any:
|
|
533
|
+
return operation(
|
|
534
|
+
tag,
|
|
535
|
+
*args,
|
|
536
|
+
**kwargs,
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
return Host_Operation
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _pinned_stored(
|
|
543
|
+
pinned: type,
|
|
544
|
+
name: str,
|
|
545
|
+
) -> Any:
|
|
546
|
+
"""What the pinned Tag currently holds under ``name``, for a Pin
|
|
547
|
+
Record's stored seat: its own Report's value, a plain value, or what an
|
|
548
|
+
earlier Pin landed (own or inherited). Behaviour is never a stored
|
|
549
|
+
value."""
|
|
550
|
+
|
|
551
|
+
for klass in pinned.__mro__:
|
|
552
|
+
if name not in klass.__dict__:
|
|
553
|
+
continue
|
|
554
|
+
|
|
555
|
+
declared = klass.__dict__[name]
|
|
556
|
+
|
|
557
|
+
if isinstance(declared, Report):
|
|
558
|
+
return declared.__get__(
|
|
559
|
+
None,
|
|
560
|
+
pinned,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if isinstance(
|
|
564
|
+
declared,
|
|
565
|
+
(
|
|
566
|
+
_Bound,
|
|
567
|
+
_Pinned_Operation,
|
|
568
|
+
classmethod,
|
|
569
|
+
staticmethod,
|
|
570
|
+
),
|
|
571
|
+
) or callable(declared):
|
|
572
|
+
return None
|
|
573
|
+
|
|
574
|
+
return declared
|
|
575
|
+
|
|
576
|
+
return None
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _delete(
|
|
580
|
+
state: _State,
|
|
581
|
+
name: str,
|
|
582
|
+
) -> None:
|
|
583
|
+
state.actions.pop(name, None)
|
|
584
|
+
state.action_origins.pop(name, None)
|
|
585
|
+
state.records.pop(name, None)
|
|
586
|
+
state.preconditions.pop(name, None)
|
|
587
|
+
state.postconditions.pop(name, None)
|
|
588
|
+
state.reports.pop(name, None)
|
|
589
|
+
state.operations.pop(name, None)
|
|
590
|
+
state.published.discard(name)
|
|
591
|
+
state.secrets.discard(name)
|
|
592
|
+
state.deleted.add(name)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _install_action(
|
|
596
|
+
state: _State,
|
|
597
|
+
tag: type,
|
|
598
|
+
name: str,
|
|
599
|
+
function: Function,
|
|
600
|
+
) -> None:
|
|
601
|
+
if state.pinned is not None:
|
|
602
|
+
_refuse_tag_member(
|
|
603
|
+
state,
|
|
604
|
+
tag,
|
|
605
|
+
name,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
record_origin = state.records.get(name)
|
|
609
|
+
|
|
610
|
+
if record_origin is not None:
|
|
611
|
+
if _independent(tag, record_origin):
|
|
612
|
+
raise TagCompositionError(
|
|
613
|
+
f"{tag.__name__}.{name} is an Action but"
|
|
614
|
+
f" {record_origin.__name__} already contributes a Record"
|
|
615
|
+
" of that name; independent Tags cannot share one Agent"
|
|
616
|
+
" name across kinds"
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
state.records.pop(name)
|
|
620
|
+
|
|
621
|
+
underlay = state.actions.get(name)
|
|
622
|
+
origin = state.action_origins.get(name, state.host_type)
|
|
623
|
+
|
|
624
|
+
if underlay is None and name not in state.deleted:
|
|
625
|
+
if state.pinned is not None:
|
|
626
|
+
underlay = _pinned_host_function(
|
|
627
|
+
state.pinned,
|
|
628
|
+
name,
|
|
629
|
+
)
|
|
630
|
+
else:
|
|
631
|
+
underlay = _host_function(
|
|
632
|
+
state.host_type,
|
|
633
|
+
name,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
if (
|
|
637
|
+
underlay is not None
|
|
638
|
+
and not _takes_underlay(function)
|
|
639
|
+
and _independent(tag, origin)
|
|
640
|
+
):
|
|
641
|
+
warnings.warn(
|
|
642
|
+
f"{tag.__name__}.{name} replaces the Action of independent"
|
|
643
|
+
f" Tag {origin.__name__}",
|
|
644
|
+
TagOverwriteWarning,
|
|
645
|
+
stacklevel=6,
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
state.actions[name] = _compose(
|
|
649
|
+
function,
|
|
650
|
+
underlay,
|
|
651
|
+
)
|
|
652
|
+
state.action_origins[name] = tag
|
|
653
|
+
state.deleted.discard(name)
|
|
654
|
+
state.published.discard(name)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _install_record(
|
|
658
|
+
state: _State,
|
|
659
|
+
tag: type,
|
|
660
|
+
name: str,
|
|
661
|
+
builder: Function,
|
|
662
|
+
) -> None:
|
|
663
|
+
if state.pinned is not None:
|
|
664
|
+
_refuse_tag_member(
|
|
665
|
+
state,
|
|
666
|
+
tag,
|
|
667
|
+
name,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
if _host_data_descriptor(state.host_type, name):
|
|
671
|
+
raise TagCompositionError(
|
|
672
|
+
f"{tag.__name__}.{name} is a Record but the host"
|
|
673
|
+
f" {state.host_type.__name__} defines {name!r} as a property"
|
|
674
|
+
" or slot; Records need an ordinary attribute"
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
action_origin = state.action_origins.get(name)
|
|
678
|
+
|
|
679
|
+
if action_origin is not None:
|
|
680
|
+
if _independent(tag, action_origin):
|
|
681
|
+
raise TagCompositionError(
|
|
682
|
+
f"{tag.__name__}.{name} is a Record but"
|
|
683
|
+
f" {action_origin.__name__} already contributes an"
|
|
684
|
+
" Action of that name; independent Tags cannot share"
|
|
685
|
+
" one Agent name across kinds"
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
state.actions.pop(name)
|
|
689
|
+
state.action_origins.pop(name)
|
|
690
|
+
|
|
691
|
+
prior = state.records.get(name)
|
|
692
|
+
|
|
693
|
+
if (
|
|
694
|
+
prior is not None
|
|
695
|
+
and not _takes_stored(builder)
|
|
696
|
+
and _independent(tag, prior)
|
|
697
|
+
):
|
|
698
|
+
warnings.warn(
|
|
699
|
+
f"{tag.__name__}.{name} replaces the Record of independent"
|
|
700
|
+
f" Tag {prior.__name__}",
|
|
701
|
+
TagOverwriteWarning,
|
|
702
|
+
stacklevel=6,
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
state.records[name] = tag
|
|
706
|
+
state.deleted.discard(name)
|
|
707
|
+
state.published.discard(name)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
# ------------------------------------------------------------------
|
|
711
|
+
# Materializing Records
|
|
712
|
+
# ------------------------------------------------------------------
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _materialize(
|
|
716
|
+
agent: object,
|
|
717
|
+
declarations: _Declarations,
|
|
718
|
+
deleted_before: set[str],
|
|
719
|
+
inputs: dict[str, Any],
|
|
720
|
+
) -> None:
|
|
721
|
+
"""Run the Tag's Record builders and store their values on the Agent.
|
|
722
|
+
|
|
723
|
+
The builder's optional second positional input is the value already
|
|
724
|
+
stored under that name, or None when there is none (or the name was
|
|
725
|
+
deleted). Application inputs bind by name to the parameters after
|
|
726
|
+
that, so ``def code(agent, *, code)`` stores the input directly.
|
|
727
|
+
"""
|
|
728
|
+
|
|
729
|
+
namespace = _namespace_of(agent)
|
|
730
|
+
pinned = isinstance(agent, type)
|
|
731
|
+
|
|
732
|
+
for name, builder in declarations.records:
|
|
733
|
+
if pinned:
|
|
734
|
+
stored = _state_of(agent).secret_values.get(name, _MISSING)
|
|
735
|
+
|
|
736
|
+
if stored is _MISSING:
|
|
737
|
+
stored = _pinned_stored(
|
|
738
|
+
agent,
|
|
739
|
+
name,
|
|
740
|
+
)
|
|
741
|
+
else:
|
|
742
|
+
stored = namespace.get(name)
|
|
743
|
+
|
|
744
|
+
if name in deleted_before or isinstance(
|
|
745
|
+
stored,
|
|
746
|
+
(
|
|
747
|
+
_Bound,
|
|
748
|
+
_Pinned_Operation,
|
|
749
|
+
),
|
|
750
|
+
):
|
|
751
|
+
stored = None
|
|
752
|
+
|
|
753
|
+
takes_stored = _takes_stored(builder)
|
|
754
|
+
|
|
755
|
+
if takes_stored:
|
|
756
|
+
_refuse_stored_input_collision(
|
|
757
|
+
builder,
|
|
758
|
+
inputs,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
named = _protocol_inputs(
|
|
762
|
+
builder,
|
|
763
|
+
inputs,
|
|
764
|
+
2 if takes_stored else 1,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
try:
|
|
768
|
+
if takes_stored:
|
|
769
|
+
value = builder(
|
|
770
|
+
agent,
|
|
771
|
+
stored,
|
|
772
|
+
**named,
|
|
773
|
+
)
|
|
774
|
+
else:
|
|
775
|
+
value = builder(
|
|
776
|
+
agent,
|
|
777
|
+
**named,
|
|
778
|
+
)
|
|
779
|
+
except TagError:
|
|
780
|
+
raise
|
|
781
|
+
except Exception as error:
|
|
782
|
+
raise TagCompositionError(
|
|
783
|
+
f"Record {builder.__qualname__} could not be"
|
|
784
|
+
f" materialized: {type(error).__name__}: {error}"
|
|
785
|
+
) from error
|
|
786
|
+
|
|
787
|
+
if pinned and name in declarations.secrets:
|
|
788
|
+
namespace.pop(name, None)
|
|
789
|
+
_state_of(agent).secret_values[name] = value
|
|
790
|
+
else:
|
|
791
|
+
namespace[name] = value
|