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/tags.py ADDED
@@ -0,0 +1,251 @@
1
+ """The public TOP types: Tag and its metaclass.
2
+
3
+ TOP borrows the language's own syntax for Tag-level acts and leaves the
4
+ Tag's dotted namespace to the program:
5
+
6
+ Wizard(charlie) apply (Bases first)
7
+ charlie in Wizard active membership, sound or defective
8
+ "Deprecated" in Wizard a keyword: the Tag carries the Flag Pin Deprecated
9
+ Wizard in charlie the same, from the Agent's side
10
+ "Wizard" in charlie the same, by name
11
+ isinstance(charlie, Wizard) ever a member ("ever a Wizard, always a Wizard")
12
+ for w in Wizard the sound population
13
+ if Wizard is there a sound Wizard at all?
14
+ for w in ~Wizard the defective population
15
+ Wizard[:] everyone: the whole Field
16
+ Wizard[charlie] the Agent-bound view
17
+ del Wizard[charlie] leave the Field (Rip)
18
+ Form(Wizard) the Base-first closure, as Tags
19
+ f"{Wizard:form}" the same, as text
20
+
21
+ A Tag marked @Pin applies to Tags (STEP-SPEC-9); the pinned Tag is then
22
+ an Agent in every spelling above: Rare(Wizard), Wizard in Rare,
23
+ for tag in Rare, Rare[Wizard], del Rare[Wizard], f"{Wizard:pins}".
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from typing import Any
29
+ from typing import Iterator
30
+
31
+ from .access import _view_of
32
+ from .contracts import _holds
33
+ from .declarations import _MISSING
34
+ from .declarations import _check_pin_bases
35
+ from .declarations import _is_pin
36
+ from .declarations import _name_checks
37
+ from .errors import TagCompositionError
38
+ from .fields import _Field
39
+ from .fields import _Partition
40
+ from .geometry import _form_of
41
+ from .geometry import _is_tag
42
+ from .lifecycle import _rip
43
+ from .state import Tagged
44
+ from .state import _name_of
45
+ from .state import _state_of
46
+ from .transactions import _apply
47
+
48
+
49
+ class MetaTag(type):
50
+ """Metaclass of every Tag: the Tag-level acts, in language syntax."""
51
+
52
+ def __new__(
53
+ meta,
54
+ name: str,
55
+ bases: tuple[type, ...],
56
+ namespace: dict[str, Any],
57
+ **kwargs: Any,
58
+ ) -> "MetaTag":
59
+ namespace.setdefault(
60
+ "_topkit_field",
61
+ _Field(),
62
+ )
63
+ _name_checks(namespace)
64
+
65
+ tag = super().__new__(
66
+ meta,
67
+ name,
68
+ bases,
69
+ namespace,
70
+ **kwargs,
71
+ )
72
+ _check_pin_bases(tag)
73
+
74
+ return tag
75
+
76
+ def __call__(
77
+ tag,
78
+ target: object = _MISSING,
79
+ /,
80
+ **inputs: Any,
81
+ ) -> object:
82
+ if target is _MISSING:
83
+ raise TypeError(
84
+ f"{tag.__name__} is a Tag: apply it to a Target,"
85
+ f" {tag.__name__}(target)"
86
+ )
87
+
88
+ if _is_pin(tag):
89
+ _check_pin_target(
90
+ tag,
91
+ target,
92
+ )
93
+ elif isinstance(target, type):
94
+ raise TypeError(
95
+ f"{tag.__name__} is applied to objects, not classes"
96
+ )
97
+
98
+ return _apply(
99
+ target,
100
+ tag,
101
+ inputs,
102
+ )
103
+
104
+ def __contains__(
105
+ tag,
106
+ candidate: object,
107
+ ) -> bool:
108
+ """``agent in Wizard``: membership. ``"Deprecated" in Wizard``: a
109
+ keyword among the Tag's Flag Pins; a string is never a member."""
110
+
111
+ if isinstance(candidate, str):
112
+ from .access import _keyword
113
+
114
+ return _keyword(
115
+ tag,
116
+ candidate,
117
+ )
118
+
119
+ state = _state_of(candidate)
120
+
121
+ return (
122
+ state is not None
123
+ and tag in state.active
124
+ )
125
+
126
+ def __instancecheck__(
127
+ tag,
128
+ candidate: object,
129
+ ) -> bool:
130
+ state = _state_of(candidate)
131
+
132
+ if state is not None and tag in state.ever:
133
+ return True
134
+
135
+ return super().__instancecheck__(candidate)
136
+
137
+ def _sound(
138
+ tag,
139
+ ) -> _Partition:
140
+ return _Partition(
141
+ tag._topkit_field,
142
+ _holds,
143
+ "sound",
144
+ )
145
+
146
+ def __iter__(
147
+ tag,
148
+ ) -> Iterator[object]:
149
+ return iter(tag._sound())
150
+
151
+ def __len__(
152
+ tag,
153
+ ) -> int:
154
+ return len(tag._sound())
155
+
156
+ def __bool__(
157
+ tag,
158
+ ) -> bool:
159
+ return bool(tag._sound())
160
+
161
+ def __invert__(
162
+ tag,
163
+ ) -> _Partition:
164
+ return ~tag._sound()
165
+
166
+ def __getitem__(
167
+ tag,
168
+ key: Any,
169
+ ) -> Any:
170
+ if isinstance(key, slice):
171
+ if key != slice(None):
172
+ raise TypeError(
173
+ "Tag[:] is the whole Field; positional slices have"
174
+ " no meaning for a population"
175
+ )
176
+
177
+ return tag._topkit_field
178
+
179
+ return _view_of(
180
+ key,
181
+ tag,
182
+ )
183
+
184
+ def __delitem__(
185
+ tag,
186
+ agent: object,
187
+ ) -> None:
188
+ _rip(
189
+ agent,
190
+ tag,
191
+ )
192
+
193
+ def __format__(
194
+ tag,
195
+ spec: str,
196
+ ) -> str:
197
+ if spec == "":
198
+ return str(tag)
199
+
200
+ if spec == "form":
201
+ return " → ".join(
202
+ member.__name__
203
+ for member in _form_of(tag)
204
+ )
205
+
206
+ if spec == "pins":
207
+ from .queries import Tags
208
+
209
+ return ", ".join(
210
+ pin.__name__
211
+ for pin in Tags(tag)
212
+ )
213
+
214
+ if spec == "contract":
215
+ from .contracts import Contract
216
+
217
+ return Contract.Display(tag)
218
+
219
+ raise ValueError(
220
+ f"unknown format spec {spec!r} for a Tag; use 'form',"
221
+ " 'pins', or 'contract'"
222
+ )
223
+
224
+
225
+ def _check_pin_target(
226
+ pin: type,
227
+ target: object,
228
+ ) -> None:
229
+ if not _is_tag(target):
230
+ raise TagCompositionError(
231
+ f"{pin.__name__} is a Pin: apply it to a Tag, not to"
232
+ f" {_name_of(target)}"
233
+ )
234
+
235
+ if target in _form_of(pin):
236
+ raise TagCompositionError(
237
+ f"{pin.__name__} cannot pin itself"
238
+ )
239
+
240
+
241
+ class Tag(metaclass=MetaTag):
242
+ """A semantic category. Subclass it; its members are its contributions."""
243
+
244
+ __slots__ = ()
245
+
246
+
247
+ __all__ = [
248
+ "MetaTag",
249
+ "Tag",
250
+ "Tagged",
251
+ ]
TopKit/transactions.py ADDED
@@ -0,0 +1,474 @@
1
+ """Applying a Tag: the tagging sequence and its call boundary.
2
+
3
+ Once for the whole call:
4
+
5
+ 1. Gate: the Preconditions visible in the composed Form of this call
6
+ inspect the incoming Agent. A Shape's override wins over its Base's.
7
+
8
+ For each Tag in the Form (Bases first), in order:
9
+
10
+ 2. Its Records are built (each may read the value already stored).
11
+ 3. Commit: membership, Overlay, runtime type.
12
+ 4. Its Imprints run.
13
+
14
+ Once for the whole call:
15
+
16
+ 5. Every visible Postcondition is checked.
17
+
18
+ A failure in 1 or 2 rolls the whole call back: the Agent is exactly as it
19
+ was, including Bases pulled in by this call. A failure in 4 or 5 raises
20
+ but the Tags stay: the product left the line, defective, to be repaired
21
+ or Ripped.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Any
27
+ import warnings
28
+
29
+ from .contracts import _evaluate
30
+ from .declarations import _Declarations
31
+ from .declarations import _declarations_of
32
+ from .declarations import _protocol_inputs
33
+ from .errors import TagCompositionError
34
+ from .errors import TagError
35
+ from .errors import TagImprintError
36
+ from .errors import TagPostconditionError
37
+ from .errors import TagPreconditionError
38
+ from .geometry import _form_of
39
+ from .overlay import _install
40
+ from .overlay import _materialize
41
+ from .state import STATE
42
+ from .state import Tagged
43
+ from .state import _Snapshot
44
+ from .state import _State
45
+ from .state import _bind_to
46
+ from .state import _namespace_of
47
+ from .state import _rebind_all
48
+ from .state import _restore_namespace
49
+ from .state import _runtime_type_for
50
+ from .state import _state_for
51
+ from .state import _state_of
52
+
53
+
54
+ def _apply(
55
+ agent: object,
56
+ tag: type,
57
+ inputs: dict[str, Any],
58
+ ) -> object:
59
+ """Apply ``tag`` and its missing Bases to ``agent`` as one call."""
60
+
61
+ entry_namespace = dict(_namespace_of(agent) or {})
62
+ entry_state = _state_of(agent)
63
+ entry_copy = entry_state.Copy() if entry_state is not None else None
64
+ entry_tags = tuple(entry_state.active) if entry_state is not None else ()
65
+ entry_class = type(agent)
66
+
67
+ try:
68
+ state = _state_for(agent)
69
+ pending = [
70
+ member
71
+ for member in _form_of(tag)
72
+ if member not in state.active
73
+ ]
74
+
75
+ if pending:
76
+ if any(
77
+ _declarations_of(member).preconditions
78
+ for member in pending
79
+ ):
80
+ _gate(
81
+ agent,
82
+ state,
83
+ pending,
84
+ inputs,
85
+ )
86
+
87
+ for member in pending:
88
+ _apply_one(
89
+ agent,
90
+ member,
91
+ inputs,
92
+ )
93
+
94
+ _inspect(agent)
95
+ except (TagImprintError, TagPostconditionError):
96
+ raise
97
+ except BaseException:
98
+ _rollback(
99
+ agent,
100
+ entry_namespace,
101
+ entry_copy,
102
+ entry_tags,
103
+ entry_class,
104
+ )
105
+ raise
106
+
107
+ return agent
108
+
109
+
110
+ def _rollback(
111
+ agent: object,
112
+ entry_namespace: dict[str, Any],
113
+ entry_copy: _State | None,
114
+ entry_tags: tuple[type, ...],
115
+ entry_class: type,
116
+ ) -> None:
117
+ current = _state_of(agent)
118
+
119
+ if current is not None:
120
+ for tag in current.active:
121
+ if tag not in entry_tags:
122
+ tag._topkit_field.Remove(agent)
123
+
124
+ _restore_namespace(
125
+ agent,
126
+ entry_namespace,
127
+ )
128
+ namespace = _namespace_of(agent)
129
+
130
+ if namespace is None:
131
+ return
132
+
133
+ if entry_copy is not None:
134
+ namespace[STATE] = entry_copy
135
+ else:
136
+ namespace.pop(STATE, None)
137
+
138
+ if type(agent) is not entry_class:
139
+ agent.__class__ = entry_class
140
+
141
+
142
+ def _gate(
143
+ agent: object,
144
+ state: _State,
145
+ pending: list[type],
146
+ inputs: dict[str, Any],
147
+ ) -> None:
148
+ """Inspect the incoming materials once, against the Form as it will be.
149
+
150
+ Every pending Tag is laid over a scratch copy so that a Shape's
151
+ Precondition overrides (relaxes) its Base's, and so that declaration
152
+ errors and collisions surface before anything changes.
153
+ """
154
+
155
+ scratch = state.Copy()
156
+ names: list[str] = []
157
+
158
+ with warnings.catch_warnings():
159
+ warnings.simplefilter("ignore")
160
+
161
+ for tag in pending:
162
+ declarations = _declarations_of(tag)
163
+
164
+ _install(
165
+ scratch,
166
+ tag,
167
+ declarations,
168
+ )
169
+
170
+ for name, _function in declarations.preconditions:
171
+ if name not in names:
172
+ names.append(name)
173
+
174
+ state.composing += 1
175
+
176
+ try:
177
+ _evaluate(
178
+ (
179
+ (name, scratch.preconditions[name])
180
+ for name in names
181
+ if name in scratch.preconditions
182
+ ),
183
+ agent,
184
+ inputs,
185
+ TagPreconditionError,
186
+ "Precondition",
187
+ )
188
+ finally:
189
+ state.composing -= 1
190
+
191
+
192
+ def _apply_one(
193
+ agent: object,
194
+ tag: type,
195
+ inputs: dict[str, Any],
196
+ ) -> None:
197
+ # The state is laid over in place: the call boundary (_apply) holds the
198
+ # entry copy that a Record failure rolls back to, and nothing reads the
199
+ # new Overlay before commit binds it on the Agent.
200
+ state = _state_for(agent)
201
+ declarations = _declarations_of(tag)
202
+ deleted_before = set(state.deleted)
203
+ state.composing += 1
204
+
205
+ try:
206
+ _install(
207
+ state,
208
+ tag,
209
+ declarations,
210
+ )
211
+
212
+ _materialize(
213
+ agent,
214
+ declarations,
215
+ deleted_before,
216
+ inputs,
217
+ )
218
+
219
+ _commit(
220
+ agent,
221
+ state,
222
+ deleted_before,
223
+ tag,
224
+ declarations,
225
+ )
226
+
227
+ try:
228
+ _imprint(
229
+ agent,
230
+ declarations,
231
+ inputs,
232
+ )
233
+ finally:
234
+ state.snapshots[tag] = _snapshot(
235
+ agent,
236
+ state,
237
+ )
238
+ finally:
239
+ state.composing -= 1
240
+
241
+
242
+ def _inspect(
243
+ agent: object,
244
+ ) -> None:
245
+ """Quality check of the finished product: every visible Postcondition."""
246
+
247
+ state = _state_of(agent)
248
+ state.composing += 1
249
+
250
+ try:
251
+ _evaluate(
252
+ state.postconditions.items(),
253
+ agent,
254
+ {},
255
+ TagPostconditionError,
256
+ "Postcondition",
257
+ )
258
+ finally:
259
+ state.composing -= 1
260
+
261
+
262
+ def _commit(
263
+ agent: object,
264
+ state: _State,
265
+ deleted_before: set[str],
266
+ tag: type,
267
+ declarations: _Declarations,
268
+ ) -> None:
269
+ namespace = _namespace_of(agent)
270
+
271
+ for name in state.deleted - deleted_before:
272
+ namespace.pop(name, None)
273
+
274
+ state.active.append(tag)
275
+ state.ever.add(tag)
276
+
277
+ if declarations.secrets:
278
+ # A first secret turns every bound Action into a composing one.
279
+ _rebind_all(
280
+ agent,
281
+ state,
282
+ )
283
+ else:
284
+ for name, _function in declarations.actions:
285
+ if name in state.actions:
286
+ _bind_to(
287
+ agent,
288
+ state,
289
+ name,
290
+ )
291
+
292
+ for name, _operation, public in declarations.operations:
293
+ if public and name in state.actions:
294
+ _bind_to(
295
+ agent,
296
+ state,
297
+ name,
298
+ )
299
+
300
+ if _needs_new_type(
301
+ agent,
302
+ declarations,
303
+ ):
304
+ next_type = _runtime_type_for(state)
305
+ else:
306
+ next_type = type(agent)
307
+
308
+ if type(agent) is not next_type:
309
+ try:
310
+ agent.__class__ = next_type
311
+ except TypeError as error:
312
+ raise TagCompositionError(
313
+ f"{type(agent).__name__} cannot be actualized in place"
314
+ ) from error
315
+
316
+ tag._topkit_field.Add(agent)
317
+
318
+ if state.pinned is not None and declarations.published:
319
+ _publish_to_field(
320
+ state.pinned,
321
+ declarations.published,
322
+ )
323
+
324
+
325
+ def _publish_to_field(
326
+ pinned: type,
327
+ names: frozenset[str],
328
+ ) -> None:
329
+ """A Pin's @Public members reach the pinned Tag's Field: every present
330
+ Agent receives them now as the Tag's own published Reports and
331
+ Operations; every future Agent through the Tag's declarations. The
332
+ whole Field is checked on copies first, so no Agent is touched unless
333
+ all can be."""
334
+
335
+ from .declarations import _scan_cache
336
+ from .overlay import _adapter
337
+
338
+ state = _state_of(pinned)
339
+ _scan_cache.pop(pinned, None) # the Tag's declarations grew
340
+ agents = list(pinned[:])
341
+ plans: list[tuple[object, _State]] = []
342
+
343
+ with warnings.catch_warnings():
344
+ warnings.simplefilter("ignore") # the real pass warns once
345
+
346
+ for agent in agents:
347
+ candidate = _state_of(agent).Copy()
348
+ _publish_into(
349
+ candidate,
350
+ pinned,
351
+ state,
352
+ names,
353
+ _adapter,
354
+ )
355
+ plans.append(
356
+ (
357
+ agent,
358
+ candidate,
359
+ )
360
+ )
361
+
362
+ for agent, candidate in plans:
363
+ agent_state = _state_of(agent)
364
+ _publish_into(
365
+ agent_state,
366
+ pinned,
367
+ state,
368
+ names,
369
+ _adapter,
370
+ )
371
+
372
+ for name in names:
373
+ if name in agent_state.actions:
374
+ _bind_to(
375
+ agent,
376
+ agent_state,
377
+ name,
378
+ )
379
+
380
+ next_type = _runtime_type_for(agent_state)
381
+
382
+ if type(agent) is not next_type:
383
+ agent.__class__ = next_type
384
+
385
+
386
+ def _publish_into(
387
+ agent_state: _State,
388
+ pinned: type,
389
+ state: _State,
390
+ names: frozenset[str],
391
+ adapter: Any,
392
+ ) -> None:
393
+ from .overlay import _install_action
394
+
395
+ for name in names:
396
+ if name in state.records:
397
+ agent_state.reports[name] = (pinned, None)
398
+ agent_state.published.add(name)
399
+ elif name in state.actions:
400
+ _install_action(
401
+ agent_state,
402
+ pinned,
403
+ name,
404
+ adapter(pinned, name, state.actions[name]),
405
+ )
406
+
407
+
408
+ def _needs_new_type(
409
+ agent: object,
410
+ declarations: _Declarations,
411
+ ) -> bool:
412
+ """Only type-level facts change the runtime type: the first tagging,
413
+ deletions, secrets, published Reports, dunder Actions, a first Post."""
414
+
415
+ if not isinstance(agent, Tagged):
416
+ return True
417
+
418
+ return bool(
419
+ declarations.deletions
420
+ or declarations.secrets
421
+ or declarations.dunders
422
+ or declarations.postconditions
423
+ or any(public for _name, _value, public in declarations.reports)
424
+ )
425
+
426
+
427
+ def _imprint(
428
+ agent: object,
429
+ declarations: _Declarations,
430
+ inputs: dict[str, Any],
431
+ ) -> None:
432
+ for name, imprint in declarations.imprints:
433
+ try:
434
+ imprint(
435
+ agent,
436
+ **_protocol_inputs(imprint, inputs, 1),
437
+ )
438
+ except TagError:
439
+ raise
440
+ except Exception as error:
441
+ raise TagImprintError.Named(name)(
442
+ f"Imprint {imprint.__qualname__} failed:"
443
+ f" {type(error).__name__}: {error}"
444
+ ) from error
445
+
446
+
447
+ def _snapshot(
448
+ agent: object,
449
+ state: _State,
450
+ ) -> _Snapshot:
451
+ namespace = _namespace_of(agent)
452
+ records = {
453
+ name: namespace[name]
454
+ for name in state.records
455
+ if name in namespace
456
+ }
457
+
458
+ for name in state.records:
459
+ if name in state.secret_values:
460
+ records[name] = state.secret_values[name]
461
+
462
+ reports = {
463
+ name: (origin, getattr(origin, name))
464
+ for name, (origin, _report) in state.reports.items()
465
+ }
466
+
467
+ return _Snapshot(
468
+ actions=dict(state.actions),
469
+ records=records,
470
+ reports=reports,
471
+ operations=dict(state.operations),
472
+ deleted=frozenset(state.deleted),
473
+ secrets=frozenset(state.secrets),
474
+ )