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/declarations.py
ADDED
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
"""Declarations: the marks an author puts on a Tag, and how they are read.
|
|
2
|
+
|
|
3
|
+
Agent scope: @Action, @Record (external by default, @Secret hides)
|
|
4
|
+
Tag scope: @Operation, @Report (internal by default, @Public publishes)
|
|
5
|
+
Protocols: @Imprint, @Pre, @Post, @Rip, @Delete
|
|
6
|
+
Composition: @Underlay (extend the prior visible contribution)
|
|
7
|
+
|
|
8
|
+
A Tag class is scanned once; the result is cached per class.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from inspect import Parameter
|
|
15
|
+
from inspect import signature
|
|
16
|
+
from typing import Any
|
|
17
|
+
from typing import Callable
|
|
18
|
+
from weakref import WeakKeyDictionary
|
|
19
|
+
|
|
20
|
+
from .errors import TagDeclarationError
|
|
21
|
+
from .errors import TagImprintError
|
|
22
|
+
from .errors import TagPostconditionError
|
|
23
|
+
from .errors import TagPreconditionError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_KIND = "__topkit_kind__"
|
|
27
|
+
_UNDERLAY = "__topkit_underlay__"
|
|
28
|
+
_RIP = "__topkit_rip__"
|
|
29
|
+
_SECRET = "__topkit_secret__"
|
|
30
|
+
_PUBLIC = "__topkit_public__"
|
|
31
|
+
_FLAG = "__topkit_flag__"
|
|
32
|
+
_PIN = "__topkit_pin__"
|
|
33
|
+
|
|
34
|
+
STATE = "_TOPKIT_STATE"
|
|
35
|
+
|
|
36
|
+
_MISSING = object()
|
|
37
|
+
|
|
38
|
+
Function = Callable[..., Any]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
# Marks
|
|
43
|
+
# ------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_CONDITION_KINDS = ("precondition", "postcondition", "condition")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _mark(
|
|
50
|
+
function: Function,
|
|
51
|
+
kind: str,
|
|
52
|
+
) -> Function:
|
|
53
|
+
"""Mark a function with its kind. ``@Pre`` and ``@Post`` stacked on one
|
|
54
|
+
function make it a *condition*: necessary to enter and to stay."""
|
|
55
|
+
|
|
56
|
+
existing = getattr(
|
|
57
|
+
function,
|
|
58
|
+
_KIND,
|
|
59
|
+
None,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
existing is not None
|
|
64
|
+
and existing != kind
|
|
65
|
+
and existing in _CONDITION_KINDS
|
|
66
|
+
and kind in _CONDITION_KINDS
|
|
67
|
+
):
|
|
68
|
+
kind = "condition"
|
|
69
|
+
|
|
70
|
+
setattr(
|
|
71
|
+
function,
|
|
72
|
+
_KIND,
|
|
73
|
+
kind,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return function
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _flag(
|
|
80
|
+
target: Any,
|
|
81
|
+
name: str,
|
|
82
|
+
) -> Any:
|
|
83
|
+
if isinstance(target, Report):
|
|
84
|
+
function = target.builder
|
|
85
|
+
else:
|
|
86
|
+
function = getattr(
|
|
87
|
+
target,
|
|
88
|
+
"__func__",
|
|
89
|
+
target,
|
|
90
|
+
)
|
|
91
|
+
setattr(
|
|
92
|
+
function,
|
|
93
|
+
name,
|
|
94
|
+
True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return target
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def Action(
|
|
101
|
+
function: Function,
|
|
102
|
+
) -> Function:
|
|
103
|
+
"""Agent behaviour. A bare method on a Tag is already an Action; this
|
|
104
|
+
is the explicit, stackable spelling."""
|
|
105
|
+
|
|
106
|
+
return _mark(
|
|
107
|
+
function,
|
|
108
|
+
"action",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def Record(
|
|
113
|
+
function: Function,
|
|
114
|
+
) -> Function:
|
|
115
|
+
"""Agent state. The builder runs at tagging and its value is stored on
|
|
116
|
+
the Agent. A second positional parameter receives the value already
|
|
117
|
+
stored under that name, or None when there is none::
|
|
118
|
+
|
|
119
|
+
@Record
|
|
120
|
+
def spells(agent, stored):
|
|
121
|
+
return (stored or []) + ["Fireball"]
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
return _mark(
|
|
125
|
+
function,
|
|
126
|
+
"record",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def Underlay(
|
|
131
|
+
function: Function,
|
|
132
|
+
) -> Function:
|
|
133
|
+
"""Extend the prior visible contribution of the same name.
|
|
134
|
+
|
|
135
|
+
For an Action or a condition the second positional parameter receives a
|
|
136
|
+
callable that runs the prior contribution. For a Record the second
|
|
137
|
+
positional parameter receives the stored value (the mark is optional
|
|
138
|
+
there; the parameter alone is enough).
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
return _flag(
|
|
142
|
+
function,
|
|
143
|
+
_UNDERLAY,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def Rip(
|
|
148
|
+
function: Function,
|
|
149
|
+
) -> Function:
|
|
150
|
+
"""Teardown. Runs when the Agent leaves the Tag's Field. It is also a
|
|
151
|
+
normally callable Action."""
|
|
152
|
+
|
|
153
|
+
return _flag(
|
|
154
|
+
function,
|
|
155
|
+
_RIP,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class _Check_Mark:
|
|
160
|
+
"""A mark for a named check: Imprint, Precondition, Postcondition.
|
|
161
|
+
|
|
162
|
+
Called, it marks the function. Read as a namespace, it names the
|
|
163
|
+
failure of one check: ``Precondition.Is_A_Caster`` is the error raised
|
|
164
|
+
when the Precondition declared as ``Is_A_Caster`` refuses, so a program
|
|
165
|
+
writes ``except Precondition.Is_A_Caster:`` in its own words.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(
|
|
169
|
+
mark,
|
|
170
|
+
kind: str,
|
|
171
|
+
failure: type | None,
|
|
172
|
+
doc: str,
|
|
173
|
+
name: str | None = None,
|
|
174
|
+
) -> None:
|
|
175
|
+
mark.kind = kind
|
|
176
|
+
mark.failure = failure
|
|
177
|
+
mark.__name__ = name if name is not None else kind.capitalize()
|
|
178
|
+
mark.__doc__ = doc
|
|
179
|
+
|
|
180
|
+
def __call__(
|
|
181
|
+
mark,
|
|
182
|
+
function: Function,
|
|
183
|
+
) -> Function:
|
|
184
|
+
return _mark(
|
|
185
|
+
function,
|
|
186
|
+
mark.kind,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def __getattr__(
|
|
190
|
+
mark,
|
|
191
|
+
name: str,
|
|
192
|
+
) -> type:
|
|
193
|
+
if mark.failure is None:
|
|
194
|
+
raise AttributeError(
|
|
195
|
+
f"@{mark.__name__} names no failure of its own: it fails"
|
|
196
|
+
f" as a Precondition at the door, or as a Postcondition"
|
|
197
|
+
f" afterwards. Catch `Precondition.{name}` or"
|
|
198
|
+
f" `Postcondition.{name}`, whichever you mean to repair."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return getattr(
|
|
202
|
+
mark.failure,
|
|
203
|
+
name,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def __repr__(
|
|
207
|
+
mark,
|
|
208
|
+
) -> str:
|
|
209
|
+
return f"<TopKit mark @{mark.__name__}>"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
Imprint = _Check_Mark(
|
|
213
|
+
"imprint",
|
|
214
|
+
TagImprintError,
|
|
215
|
+
"Work performed after the Tag has applied.",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
Precondition = _Check_Mark(
|
|
220
|
+
"precondition",
|
|
221
|
+
TagPreconditionError,
|
|
222
|
+
"A gate on the incoming Agent. Evaluated before the Tag applies.",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
Postcondition = _Check_Mark(
|
|
226
|
+
"postcondition",
|
|
227
|
+
TagPostconditionError,
|
|
228
|
+
"A promise about the finished Agent. Evaluated after every Tagging.",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
Pre = Precondition
|
|
233
|
+
Post = Postcondition
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
Requirement = _Check_Mark(
|
|
237
|
+
"condition",
|
|
238
|
+
None,
|
|
239
|
+
"A necessity in both directions: a gate on the incoming Agent and a"
|
|
240
|
+
" promise about it afterwards. The same as stacking @Pre and @Post"
|
|
241
|
+
" on one function, said in one word.",
|
|
242
|
+
"Requirement",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def Delete(
|
|
247
|
+
function: Function,
|
|
248
|
+
) -> Function:
|
|
249
|
+
"""Remove a visible contribution (or host member) by name."""
|
|
250
|
+
|
|
251
|
+
return _mark(
|
|
252
|
+
function,
|
|
253
|
+
"delete",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def Operation(
|
|
258
|
+
function: Function,
|
|
259
|
+
) -> classmethod:
|
|
260
|
+
"""Tag behaviour. The Tag is its first input."""
|
|
261
|
+
|
|
262
|
+
_mark(
|
|
263
|
+
function,
|
|
264
|
+
"operation",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return classmethod(function)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def Secret(
|
|
271
|
+
function: Function,
|
|
272
|
+
) -> Function:
|
|
273
|
+
"""Hide an Action or Record from code outside composition."""
|
|
274
|
+
|
|
275
|
+
return _flag(
|
|
276
|
+
function,
|
|
277
|
+
_SECRET,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def Public(
|
|
282
|
+
member: Any,
|
|
283
|
+
) -> Any:
|
|
284
|
+
"""Publish a Report or Operation on the Agent: a Report as a read-only
|
|
285
|
+
name, an Operation as an Action that forwards to it with the Agent as
|
|
286
|
+
its second input. Stacks with ``@Report`` / ``@Operation`` in either
|
|
287
|
+
order."""
|
|
288
|
+
|
|
289
|
+
if isinstance(member, Report):
|
|
290
|
+
member.public = True
|
|
291
|
+
|
|
292
|
+
return member
|
|
293
|
+
|
|
294
|
+
return _flag(
|
|
295
|
+
member,
|
|
296
|
+
_PUBLIC,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def Flag(
|
|
301
|
+
tag: type,
|
|
302
|
+
) -> type:
|
|
303
|
+
"""Mark a Tag as a keyword: searchable from the Agent's side by name
|
|
304
|
+
or by class, ``"Undead" in ghoul`` and ``Undead in ghoul``.
|
|
305
|
+
|
|
306
|
+
Applying a Flag to a host that defines its own ``in`` is refused.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
if not isinstance(tag, type) or not hasattr(tag, "_topkit_field"):
|
|
310
|
+
raise TagDeclarationError(
|
|
311
|
+
"@Flag marks a Tag class"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
setattr(
|
|
315
|
+
tag,
|
|
316
|
+
_FLAG,
|
|
317
|
+
True,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return tag
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _is_flag(
|
|
324
|
+
tag: type,
|
|
325
|
+
) -> bool:
|
|
326
|
+
return bool(
|
|
327
|
+
tag.__dict__.get(
|
|
328
|
+
_FLAG,
|
|
329
|
+
False,
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def Pin(
|
|
335
|
+
tag: type,
|
|
336
|
+
) -> type:
|
|
337
|
+
"""Mark a Tag whose Targets are Tags (STEP-SPEC-9).
|
|
338
|
+
|
|
339
|
+
``Rare(Wizard)`` makes the Tag ``Wizard`` an Agent of ``Rare``: its
|
|
340
|
+
Records land on ``Wizard`` as Reports, its Actions as Operations, and
|
|
341
|
+
the Field of ``Rare`` is a population of Tags. A Pin applies to
|
|
342
|
+
nothing else, and its Bases must be Pins.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
if not isinstance(tag, type) or not hasattr(tag, "_topkit_field"):
|
|
346
|
+
raise TagDeclarationError(
|
|
347
|
+
"@Pin marks a Tag class"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
setattr(
|
|
351
|
+
tag,
|
|
352
|
+
_PIN,
|
|
353
|
+
True,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
_check_pin_bases(tag)
|
|
357
|
+
_declarations_of(tag) # validate the members now, not at first pinning
|
|
358
|
+
|
|
359
|
+
return tag
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _is_pin(
|
|
363
|
+
tag: type,
|
|
364
|
+
) -> bool:
|
|
365
|
+
"""A Shape of a Pin is a Pin."""
|
|
366
|
+
|
|
367
|
+
return bool(
|
|
368
|
+
getattr(
|
|
369
|
+
tag,
|
|
370
|
+
_PIN,
|
|
371
|
+
False,
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _is_tag_base(
|
|
377
|
+
base: type,
|
|
378
|
+
) -> bool:
|
|
379
|
+
"""A Tag class other than the root ``Tag`` (the root is the only Tag
|
|
380
|
+
with no Tag among its own bases)."""
|
|
381
|
+
|
|
382
|
+
return hasattr(base, "_topkit_field") and any(
|
|
383
|
+
hasattr(deeper, "_topkit_field")
|
|
384
|
+
for deeper in base.__bases__
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _check_pin_bases(
|
|
389
|
+
tag: type,
|
|
390
|
+
) -> None:
|
|
391
|
+
"""One Form is all Pins or no Pins."""
|
|
392
|
+
|
|
393
|
+
bases = tuple(
|
|
394
|
+
base
|
|
395
|
+
for base in tag.__bases__
|
|
396
|
+
if _is_tag_base(base)
|
|
397
|
+
)
|
|
398
|
+
pins = [
|
|
399
|
+
base
|
|
400
|
+
for base in bases
|
|
401
|
+
if _is_pin(base)
|
|
402
|
+
]
|
|
403
|
+
marked = bool(tag.__dict__.get(_PIN, False))
|
|
404
|
+
|
|
405
|
+
if not bases or len(pins) == len(bases):
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
if not pins and not marked:
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
raise TagDeclarationError(
|
|
412
|
+
f"{tag.__name__} mixes Pins and Tags in one Form; a Pin's"
|
|
413
|
+
" Bases must be Pins (STEP-SPEC-9 §2)"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class Report:
|
|
418
|
+
"""Shared data belonging to a Tag, written like a Record::
|
|
419
|
+
|
|
420
|
+
@Report
|
|
421
|
+
def hit_die(tag):
|
|
422
|
+
return 8
|
|
423
|
+
|
|
424
|
+
The builder receives the Tag and runs once per Tag, on first read. A
|
|
425
|
+
second positional parameter receives the value the Tag's Bases give
|
|
426
|
+
that name, or None, so a Shape can extend a Base's Report the way a
|
|
427
|
+
Record extends what is stored.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
def __init__(
|
|
431
|
+
report,
|
|
432
|
+
builder: Function,
|
|
433
|
+
) -> None:
|
|
434
|
+
if not callable(builder):
|
|
435
|
+
raise TagDeclarationError(
|
|
436
|
+
"@Report marks a builder: `@Report def name(tag): ...`"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
report.builder = builder
|
|
440
|
+
report.public = _has_flag(builder, _PUBLIC)
|
|
441
|
+
report.__name__ = builder.__name__
|
|
442
|
+
report.__doc__ = builder.__doc__
|
|
443
|
+
report._name = builder.__name__
|
|
444
|
+
report._values: "WeakKeyDictionary[type, Any]" = WeakKeyDictionary()
|
|
445
|
+
|
|
446
|
+
def __set_name__(
|
|
447
|
+
report,
|
|
448
|
+
owner: type,
|
|
449
|
+
name: str,
|
|
450
|
+
) -> None:
|
|
451
|
+
report._name = name
|
|
452
|
+
|
|
453
|
+
def __get__(
|
|
454
|
+
report,
|
|
455
|
+
instance: object,
|
|
456
|
+
owner: type | None = None,
|
|
457
|
+
) -> Any:
|
|
458
|
+
if owner is None:
|
|
459
|
+
owner = type(instance)
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
return report._values[owner]
|
|
463
|
+
except KeyError:
|
|
464
|
+
pass
|
|
465
|
+
|
|
466
|
+
value = report._build(owner)
|
|
467
|
+
report._values[owner] = value
|
|
468
|
+
|
|
469
|
+
return value
|
|
470
|
+
|
|
471
|
+
def _build(
|
|
472
|
+
report,
|
|
473
|
+
owner: type,
|
|
474
|
+
) -> Any:
|
|
475
|
+
if _parameters_of(report.builder).positional >= 2:
|
|
476
|
+
return report.builder(
|
|
477
|
+
owner,
|
|
478
|
+
report._inherited(owner),
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
return report.builder(owner)
|
|
482
|
+
|
|
483
|
+
def _inherited(
|
|
484
|
+
report,
|
|
485
|
+
owner: type,
|
|
486
|
+
) -> Any:
|
|
487
|
+
"""The value the Bases give this name, or None: the first member of
|
|
488
|
+
that name declared after this Report's own class in the MRO."""
|
|
489
|
+
|
|
490
|
+
passed_own_class = False
|
|
491
|
+
|
|
492
|
+
for klass in owner.__mro__:
|
|
493
|
+
member = klass.__dict__.get(report._name)
|
|
494
|
+
|
|
495
|
+
if member is report:
|
|
496
|
+
passed_own_class = True
|
|
497
|
+
continue
|
|
498
|
+
|
|
499
|
+
if not passed_own_class or member is None:
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
if isinstance(member, Report):
|
|
503
|
+
return member.__get__(
|
|
504
|
+
None,
|
|
505
|
+
owner,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
return member
|
|
509
|
+
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
def __repr__(
|
|
513
|
+
report,
|
|
514
|
+
) -> str:
|
|
515
|
+
return f"<Report {report._name}>"
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# ------------------------------------------------------------------
|
|
519
|
+
# Scanning a Tag class
|
|
520
|
+
# ------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@dataclass(frozen=True)
|
|
524
|
+
class _Declarations:
|
|
525
|
+
actions: tuple[tuple[str, Function], ...]
|
|
526
|
+
records: tuple[tuple[str, Function], ...]
|
|
527
|
+
secrets: frozenset[str]
|
|
528
|
+
imprints: tuple[tuple[str, Function], ...]
|
|
529
|
+
preconditions: tuple[tuple[str, Function], ...]
|
|
530
|
+
postconditions: tuple[tuple[str, Function], ...]
|
|
531
|
+
deletions: tuple[str, ...]
|
|
532
|
+
reports: tuple[tuple[str, Any, bool], ...]
|
|
533
|
+
operations: tuple[tuple[str, Function, bool], ...]
|
|
534
|
+
rips: tuple[str, ...]
|
|
535
|
+
dunders: frozenset[str]
|
|
536
|
+
published: frozenset[str] # Agent-scope members marked @Public (Pins)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
_scan_cache: "WeakKeyDictionary[type, _Declarations]" = WeakKeyDictionary()
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _declarations_of(
|
|
543
|
+
tag: type,
|
|
544
|
+
) -> _Declarations:
|
|
545
|
+
cached = _scan_cache.get(tag)
|
|
546
|
+
|
|
547
|
+
if cached is None:
|
|
548
|
+
cached = _scan(tag)
|
|
549
|
+
_scan_cache[tag] = cached
|
|
550
|
+
|
|
551
|
+
return cached
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _is_private(
|
|
555
|
+
name: str,
|
|
556
|
+
) -> bool:
|
|
557
|
+
return (
|
|
558
|
+
name.startswith("_")
|
|
559
|
+
and not _is_dunder(name)
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _is_dunder(
|
|
564
|
+
name: str,
|
|
565
|
+
) -> bool:
|
|
566
|
+
return (
|
|
567
|
+
name.startswith("__")
|
|
568
|
+
and name.endswith("__")
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _kind_of(
|
|
573
|
+
attribute: Any,
|
|
574
|
+
) -> str | None:
|
|
575
|
+
function = getattr(
|
|
576
|
+
attribute,
|
|
577
|
+
"__func__",
|
|
578
|
+
attribute,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
return getattr(
|
|
582
|
+
function,
|
|
583
|
+
_KIND,
|
|
584
|
+
None,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _has_flag(
|
|
589
|
+
attribute: Any,
|
|
590
|
+
name: str,
|
|
591
|
+
) -> bool:
|
|
592
|
+
function = getattr(
|
|
593
|
+
attribute,
|
|
594
|
+
"__func__",
|
|
595
|
+
attribute,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
return bool(
|
|
599
|
+
getattr(
|
|
600
|
+
function,
|
|
601
|
+
name,
|
|
602
|
+
False,
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
_NAMED_FAILURES: dict[str, type] = {
|
|
608
|
+
"imprint": TagImprintError,
|
|
609
|
+
"precondition": TagPreconditionError,
|
|
610
|
+
"postcondition": TagPostconditionError,
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _name_checks(
|
|
615
|
+
namespace: dict[str, Any],
|
|
616
|
+
) -> None:
|
|
617
|
+
"""Give every check in a Tag body its named failure, at class creation.
|
|
618
|
+
|
|
619
|
+
Done when the class is made, not at the first tagging, so
|
|
620
|
+
``except Precondition.Is_A_Caster`` is valid as soon as the Tag exists.
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
for name, attribute in namespace.items():
|
|
624
|
+
if _is_private(name):
|
|
625
|
+
continue
|
|
626
|
+
|
|
627
|
+
kind = _kind_of(attribute)
|
|
628
|
+
|
|
629
|
+
if kind == "condition":
|
|
630
|
+
TagPreconditionError.Named(name)
|
|
631
|
+
TagPostconditionError.Named(name)
|
|
632
|
+
continue
|
|
633
|
+
|
|
634
|
+
failure = _NAMED_FAILURES.get(kind)
|
|
635
|
+
|
|
636
|
+
if failure is not None:
|
|
637
|
+
failure.Named(name)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _scan(
|
|
641
|
+
tag: type,
|
|
642
|
+
) -> _Declarations:
|
|
643
|
+
actions: list[tuple[str, Function]] = []
|
|
644
|
+
records: list[tuple[str, Function]] = []
|
|
645
|
+
secrets: set[str] = set()
|
|
646
|
+
imprints: list[tuple[str, Function]] = []
|
|
647
|
+
preconditions: list[tuple[str, Function]] = []
|
|
648
|
+
postconditions: list[tuple[str, Function]] = []
|
|
649
|
+
deletions: list[str] = []
|
|
650
|
+
reports: list[tuple[str, Any, bool]] = []
|
|
651
|
+
operations: list[tuple[str, Function, bool]] = []
|
|
652
|
+
rips: list[str] = []
|
|
653
|
+
dunders: set[str] = set()
|
|
654
|
+
published: set[str] = set()
|
|
655
|
+
managed = tag.__dict__.get(STATE) # names a Pin landed here
|
|
656
|
+
|
|
657
|
+
if managed is not None:
|
|
658
|
+
_emit_published_pins(
|
|
659
|
+
managed,
|
|
660
|
+
reports,
|
|
661
|
+
operations,
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
for name, attribute in tag.__dict__.items():
|
|
665
|
+
if _is_private(name):
|
|
666
|
+
continue
|
|
667
|
+
|
|
668
|
+
if managed is not None and (
|
|
669
|
+
name in managed.actions
|
|
670
|
+
or name in managed.records
|
|
671
|
+
):
|
|
672
|
+
continue
|
|
673
|
+
|
|
674
|
+
if isinstance(attribute, Report):
|
|
675
|
+
if attribute.public and _has_flag(attribute.builder, _SECRET):
|
|
676
|
+
_reject_both(tag, name, True, True)
|
|
677
|
+
|
|
678
|
+
reports.append(
|
|
679
|
+
(
|
|
680
|
+
name,
|
|
681
|
+
attribute,
|
|
682
|
+
attribute.public,
|
|
683
|
+
)
|
|
684
|
+
)
|
|
685
|
+
continue
|
|
686
|
+
|
|
687
|
+
kind = _kind_of(attribute)
|
|
688
|
+
secret = _has_flag(attribute, _SECRET)
|
|
689
|
+
public = _has_flag(attribute, _PUBLIC)
|
|
690
|
+
|
|
691
|
+
if kind == "operation":
|
|
692
|
+
_reject_both(tag, name, secret, public)
|
|
693
|
+
operations.append(
|
|
694
|
+
(
|
|
695
|
+
name,
|
|
696
|
+
attribute.__func__,
|
|
697
|
+
public,
|
|
698
|
+
)
|
|
699
|
+
)
|
|
700
|
+
continue
|
|
701
|
+
|
|
702
|
+
if isinstance(
|
|
703
|
+
attribute,
|
|
704
|
+
(
|
|
705
|
+
classmethod,
|
|
706
|
+
staticmethod,
|
|
707
|
+
),
|
|
708
|
+
):
|
|
709
|
+
continue
|
|
710
|
+
|
|
711
|
+
if not callable(attribute):
|
|
712
|
+
continue
|
|
713
|
+
|
|
714
|
+
if public:
|
|
715
|
+
published.add(name)
|
|
716
|
+
|
|
717
|
+
if kind == "record":
|
|
718
|
+
_reject_both(tag, name, secret, public)
|
|
719
|
+
records.append(
|
|
720
|
+
(
|
|
721
|
+
name,
|
|
722
|
+
attribute,
|
|
723
|
+
)
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
if secret:
|
|
727
|
+
secrets.add(name)
|
|
728
|
+
|
|
729
|
+
continue
|
|
730
|
+
|
|
731
|
+
if kind == "imprint":
|
|
732
|
+
imprints.append(
|
|
733
|
+
(
|
|
734
|
+
name,
|
|
735
|
+
attribute,
|
|
736
|
+
)
|
|
737
|
+
)
|
|
738
|
+
continue
|
|
739
|
+
|
|
740
|
+
if kind in ("precondition", "condition"):
|
|
741
|
+
preconditions.append(
|
|
742
|
+
(
|
|
743
|
+
name,
|
|
744
|
+
attribute,
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
if kind in ("postcondition", "condition"):
|
|
749
|
+
postconditions.append(
|
|
750
|
+
(
|
|
751
|
+
name,
|
|
752
|
+
attribute,
|
|
753
|
+
)
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
if kind in _CONDITION_KINDS:
|
|
757
|
+
continue
|
|
758
|
+
|
|
759
|
+
if kind == "delete":
|
|
760
|
+
deletions.append(name)
|
|
761
|
+
continue
|
|
762
|
+
|
|
763
|
+
# Anything else callable is an Action (kind "action" or unmarked).
|
|
764
|
+
_reject_both(tag, name, secret, public)
|
|
765
|
+
actions.append(
|
|
766
|
+
(
|
|
767
|
+
name,
|
|
768
|
+
attribute,
|
|
769
|
+
)
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
if secret:
|
|
773
|
+
secrets.add(name)
|
|
774
|
+
|
|
775
|
+
if _has_flag(attribute, _RIP):
|
|
776
|
+
rips.append(name)
|
|
777
|
+
|
|
778
|
+
if _is_dunder(name):
|
|
779
|
+
dunders.add(name)
|
|
780
|
+
|
|
781
|
+
declarations = _Declarations(
|
|
782
|
+
actions=tuple(actions),
|
|
783
|
+
records=tuple(records),
|
|
784
|
+
secrets=frozenset(secrets),
|
|
785
|
+
imprints=tuple(imprints),
|
|
786
|
+
preconditions=tuple(preconditions),
|
|
787
|
+
postconditions=tuple(postconditions),
|
|
788
|
+
deletions=tuple(deletions),
|
|
789
|
+
reports=tuple(reports),
|
|
790
|
+
operations=tuple(operations),
|
|
791
|
+
rips=tuple(rips),
|
|
792
|
+
dunders=frozenset(dunders),
|
|
793
|
+
published=frozenset(published),
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
if _is_pin(tag):
|
|
797
|
+
_validate_pin(
|
|
798
|
+
tag,
|
|
799
|
+
declarations,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
return declarations
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _emit_published_pins(
|
|
806
|
+
managed: Any,
|
|
807
|
+
reports: list[tuple[str, Any, bool]],
|
|
808
|
+
operations: list[tuple[str, Function, bool]],
|
|
809
|
+
) -> None:
|
|
810
|
+
"""Members a Pin landed on this Tag with @Public are the Tag's own
|
|
811
|
+
published Reports and Operations to every Agent tagged from now on
|
|
812
|
+
(STEP-SPEC-9 §5). Present Agents were reached at pinning."""
|
|
813
|
+
|
|
814
|
+
for name in managed.published:
|
|
815
|
+
if name in managed.records:
|
|
816
|
+
reports.append(
|
|
817
|
+
(
|
|
818
|
+
name,
|
|
819
|
+
None,
|
|
820
|
+
True,
|
|
821
|
+
)
|
|
822
|
+
)
|
|
823
|
+
elif name in managed.actions:
|
|
824
|
+
operations.append(
|
|
825
|
+
(
|
|
826
|
+
name,
|
|
827
|
+
managed.actions[name],
|
|
828
|
+
True,
|
|
829
|
+
)
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _validate_pin(
|
|
834
|
+
tag: type,
|
|
835
|
+
declarations: _Declarations,
|
|
836
|
+
) -> None:
|
|
837
|
+
"""A Pin's members carry no @Delete and no special-method Actions, and
|
|
838
|
+
its own Reports and Operations are not published: each would need a
|
|
839
|
+
descriptor or a hook on the Tag's metaclass, and none has a meaning
|
|
840
|
+
there yet. @Secret and @Public on its Agent-scope members do."""
|
|
841
|
+
|
|
842
|
+
problems: list[str] = []
|
|
843
|
+
|
|
844
|
+
published = [
|
|
845
|
+
name
|
|
846
|
+
for name, _value, public in declarations.reports
|
|
847
|
+
if public
|
|
848
|
+
] + [
|
|
849
|
+
name
|
|
850
|
+
for name, _function, public in declarations.operations
|
|
851
|
+
if public
|
|
852
|
+
]
|
|
853
|
+
|
|
854
|
+
if published:
|
|
855
|
+
problems.append(
|
|
856
|
+
"@Public on the Pin's own Reports / Operations " + ", ".join(published)
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
if declarations.deletions:
|
|
860
|
+
problems.append(
|
|
861
|
+
"@Delete of " + ", ".join(declarations.deletions)
|
|
862
|
+
)
|
|
863
|
+
|
|
864
|
+
if declarations.dunders:
|
|
865
|
+
problems.append(
|
|
866
|
+
"special-method Actions " + ", ".join(sorted(declarations.dunders))
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
if problems:
|
|
870
|
+
raise TagDeclarationError(
|
|
871
|
+
f"{tag.__name__} is a Pin; its members are plain:"
|
|
872
|
+
f" {'; '.join(problems)} (STEP-SPEC-9 §5)"
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def _reject_both(
|
|
877
|
+
tag: type,
|
|
878
|
+
name: str,
|
|
879
|
+
secret: bool,
|
|
880
|
+
public: bool,
|
|
881
|
+
) -> None:
|
|
882
|
+
"""A modifier that restates the default is accepted; both at once is
|
|
883
|
+
a contradiction."""
|
|
884
|
+
|
|
885
|
+
if secret and public:
|
|
886
|
+
raise TagDeclarationError(
|
|
887
|
+
f"{tag.__name__}.{name}: @Secret and @Public together say"
|
|
888
|
+
" nothing; a member is internal or external"
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
# ------------------------------------------------------------------
|
|
893
|
+
# Parameters
|
|
894
|
+
# ------------------------------------------------------------------
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
@dataclass(frozen=True)
|
|
898
|
+
class _Parameters:
|
|
899
|
+
positional: int
|
|
900
|
+
named: tuple[tuple[str, bool], ...]
|
|
901
|
+
var_keyword: bool
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
_parameter_cache: "WeakKeyDictionary[Function, _Parameters]" = (
|
|
905
|
+
WeakKeyDictionary()
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _parameters_of(
|
|
910
|
+
function: Function,
|
|
911
|
+
) -> _Parameters:
|
|
912
|
+
cached = _parameter_cache.get(function)
|
|
913
|
+
|
|
914
|
+
if cached is not None:
|
|
915
|
+
return cached
|
|
916
|
+
|
|
917
|
+
positional = 0
|
|
918
|
+
named: list[tuple[str, bool]] = []
|
|
919
|
+
var_keyword = False
|
|
920
|
+
|
|
921
|
+
for parameter in signature(function).parameters.values():
|
|
922
|
+
if parameter.kind is Parameter.VAR_KEYWORD:
|
|
923
|
+
var_keyword = True
|
|
924
|
+
continue
|
|
925
|
+
|
|
926
|
+
if parameter.kind is Parameter.VAR_POSITIONAL:
|
|
927
|
+
continue
|
|
928
|
+
|
|
929
|
+
if parameter.kind is not Parameter.KEYWORD_ONLY:
|
|
930
|
+
positional += 1
|
|
931
|
+
|
|
932
|
+
named.append(
|
|
933
|
+
(
|
|
934
|
+
parameter.name,
|
|
935
|
+
parameter.default is not Parameter.empty,
|
|
936
|
+
)
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
spec = _Parameters(
|
|
940
|
+
positional=positional,
|
|
941
|
+
named=tuple(named),
|
|
942
|
+
var_keyword=var_keyword,
|
|
943
|
+
)
|
|
944
|
+
_parameter_cache[function] = spec
|
|
945
|
+
|
|
946
|
+
return spec
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def _takes_underlay(
|
|
950
|
+
function: Function,
|
|
951
|
+
) -> bool:
|
|
952
|
+
"""An Action or condition extends the prior contribution when marked
|
|
953
|
+
@Underlay. The mark requires a second positional parameter."""
|
|
954
|
+
|
|
955
|
+
if not _has_flag(function, _UNDERLAY):
|
|
956
|
+
return False
|
|
957
|
+
|
|
958
|
+
if _parameters_of(function).positional < 2:
|
|
959
|
+
raise TagDeclarationError(
|
|
960
|
+
f"{function.__qualname__} is marked @Underlay but has no"
|
|
961
|
+
" second positional parameter to receive the underlay"
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
return True
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _takes_stored(
|
|
968
|
+
function: Function,
|
|
969
|
+
) -> bool:
|
|
970
|
+
"""A Record builder receives the stored value when it declares a second
|
|
971
|
+
positional parameter (the @Underlay mark is accepted as documentation)."""
|
|
972
|
+
|
|
973
|
+
return _parameters_of(function).positional >= 2
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _protocol_inputs(
|
|
977
|
+
function: Function,
|
|
978
|
+
inputs: dict[str, Any],
|
|
979
|
+
skip: int,
|
|
980
|
+
) -> dict[str, Any]:
|
|
981
|
+
"""Bind application inputs to a protocol's named parameters.
|
|
982
|
+
|
|
983
|
+
The first ``skip`` positional parameters are bound by position (the
|
|
984
|
+
Agent, and an underlay or stored value when present), so their names
|
|
985
|
+
are the author's choice. Later parameters, positional or keyword-only,
|
|
986
|
+
are filled from ``inputs`` by name. A parameter the caller did not
|
|
987
|
+
supply keeps its own default, or receives None when it has none.
|
|
988
|
+
``**kwargs`` receives any remaining inputs.
|
|
989
|
+
"""
|
|
990
|
+
|
|
991
|
+
spec = _parameters_of(function)
|
|
992
|
+
bound: dict[str, Any] = {}
|
|
993
|
+
|
|
994
|
+
for index, (name, has_default) in enumerate(spec.named):
|
|
995
|
+
if index < skip:
|
|
996
|
+
continue
|
|
997
|
+
|
|
998
|
+
if name in inputs:
|
|
999
|
+
bound[name] = inputs[name]
|
|
1000
|
+
elif not has_default:
|
|
1001
|
+
bound[name] = None
|
|
1002
|
+
|
|
1003
|
+
if spec.var_keyword:
|
|
1004
|
+
for name, value in inputs.items():
|
|
1005
|
+
bound.setdefault(
|
|
1006
|
+
name,
|
|
1007
|
+
value,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
return bound
|