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/state.py ADDED
@@ -0,0 +1,862 @@
1
+ """Agent state and the runtime type.
2
+
3
+ An Agent keeps one ``_TOPKIT_STATE`` in its instance dictionary. Actions
4
+ live in that dictionary as bound callables, Records as plain values, so
5
+ ordinary attribute access costs what it costs on a plain object. The
6
+ runtime type is neutral (host first, then the ``Tagged`` marker) and only
7
+ carries what Python requires on a type: special-method Actions and the
8
+ descriptors that gate deleted, secret, and published names.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from dataclasses import field
15
+ from types import MethodType
16
+ from typing import Any
17
+ from typing import Callable
18
+ from typing import Iterator
19
+ from weakref import WeakValueDictionary
20
+ import weakref
21
+
22
+ from .declarations import STATE
23
+ from .declarations import _MISSING
24
+ from .declarations import _is_dunder
25
+ from .declarations import _is_flag
26
+ from .errors import TagCompositionError
27
+
28
+ Function = Callable[..., Any]
29
+ Check = Callable[..., Any]
30
+
31
+
32
+ # ------------------------------------------------------------------
33
+ # State
34
+ # ------------------------------------------------------------------
35
+
36
+
37
+ @dataclass
38
+ class _Snapshot:
39
+ actions: dict[str, Function]
40
+ records: dict[str, Any]
41
+ reports: dict[str, tuple[type, Any]]
42
+ operations: dict[str, tuple[type, Function]]
43
+ deleted: frozenset[str]
44
+ secrets: frozenset[str]
45
+
46
+
47
+ @dataclass
48
+ class _State:
49
+ host_type: type
50
+ pinned: type | None = None # the Tag itself, when the Agent is a Tag
51
+ active: list[type] = field(default_factory=list)
52
+ ever: set[type] = field(default_factory=set)
53
+ actions: dict[str, Function] = field(default_factory=dict)
54
+ action_origins: dict[str, type] = field(default_factory=dict)
55
+ records: dict[str, type] = field(default_factory=dict)
56
+ preconditions: dict[str, Check] = field(default_factory=dict)
57
+ postconditions: dict[str, Check] = field(default_factory=dict)
58
+ reports: dict[str, tuple[type, Any]] = field(default_factory=dict)
59
+ operations: dict[str, tuple[type, Function]] = field(default_factory=dict)
60
+ published: set[str] = field(default_factory=set)
61
+ secrets: set[str] = field(default_factory=set)
62
+ deleted: set[str] = field(default_factory=set)
63
+ snapshots: dict[type, _Snapshot] = field(default_factory=dict)
64
+ rips: dict[type, tuple[Function, ...]] = field(default_factory=dict)
65
+ secret_values: dict[str, Any] = field(default_factory=dict) # a pinned Tag's @Secret members
66
+ originals: dict[str, Any] = field(default_factory=dict) # what a Pin patched, as declared
67
+ composing: int = 0
68
+ checking: bool = False
69
+
70
+ def Copy(
71
+ state,
72
+ ) -> "_State":
73
+ return _State(
74
+ host_type=state.host_type,
75
+ pinned=state.pinned,
76
+ active=list(state.active),
77
+ ever=set(state.ever),
78
+ actions=dict(state.actions),
79
+ action_origins=dict(state.action_origins),
80
+ records=dict(state.records),
81
+ preconditions=dict(state.preconditions),
82
+ postconditions=dict(state.postconditions),
83
+ reports=dict(state.reports),
84
+ operations=dict(state.operations),
85
+ published=set(state.published),
86
+ secrets=set(state.secrets),
87
+ deleted=set(state.deleted),
88
+ snapshots=dict(state.snapshots),
89
+ rips=dict(state.rips),
90
+ secret_values=dict(state.secret_values),
91
+ originals=dict(state.originals),
92
+ composing=state.composing,
93
+ checking=state.checking,
94
+ )
95
+
96
+
97
+ class _Class_Namespace:
98
+ """The namespace of a Tag used as a Target (a pinned Tag).
99
+
100
+ A class dictionary is read through a proxy and written through the
101
+ class. This adapter gives the kernel the few dictionary operations it
102
+ uses, so the tagging sequence is one code path for objects and Tags.
103
+ """
104
+
105
+ __slots__ = ("_owner",)
106
+
107
+ def __init__(
108
+ namespace,
109
+ owner: type,
110
+ ) -> None:
111
+ namespace._owner = owner
112
+
113
+ def get(
114
+ namespace,
115
+ name: str,
116
+ default: Any = None,
117
+ ) -> Any:
118
+ return namespace._owner.__dict__.get(
119
+ name,
120
+ default,
121
+ )
122
+
123
+ def __getitem__(
124
+ namespace,
125
+ name: str,
126
+ ) -> Any:
127
+ return namespace._owner.__dict__[name]
128
+
129
+ def __setitem__(
130
+ namespace,
131
+ name: str,
132
+ value: Any,
133
+ ) -> None:
134
+ setattr(
135
+ namespace._owner,
136
+ name,
137
+ value,
138
+ )
139
+
140
+ def __contains__(
141
+ namespace,
142
+ name: object,
143
+ ) -> bool:
144
+ return name in namespace._owner.__dict__
145
+
146
+ def pop(
147
+ namespace,
148
+ name: str,
149
+ default: Any = _MISSING,
150
+ ) -> Any:
151
+ value = namespace._owner.__dict__.get(
152
+ name,
153
+ _MISSING,
154
+ )
155
+
156
+ if value is _MISSING:
157
+ if default is _MISSING:
158
+ raise KeyError(name)
159
+
160
+ return default
161
+
162
+ delattr(
163
+ namespace._owner,
164
+ name,
165
+ )
166
+
167
+ return value
168
+
169
+ def keys(
170
+ namespace,
171
+ ) -> Any:
172
+ return namespace._owner.__dict__.keys()
173
+
174
+ def items(
175
+ namespace,
176
+ ) -> Any:
177
+ return namespace._owner.__dict__.items()
178
+
179
+ def __iter__(
180
+ namespace,
181
+ ) -> Iterator[str]:
182
+ return iter(namespace._owner.__dict__)
183
+
184
+ def __len__(
185
+ namespace,
186
+ ) -> int:
187
+ return len(namespace._owner.__dict__)
188
+
189
+
190
+ def _namespace_of(
191
+ agent: object,
192
+ ) -> Any:
193
+ """The Agent's writable namespace: its dictionary, or the adapter over
194
+ a Tag's class dictionary; None when it has neither."""
195
+
196
+ try:
197
+ namespace = object.__getattribute__(
198
+ agent,
199
+ "__dict__",
200
+ )
201
+ except AttributeError:
202
+ return None
203
+
204
+ if isinstance(namespace, dict):
205
+ return namespace
206
+
207
+ if isinstance(agent, type):
208
+ return _Class_Namespace(agent)
209
+
210
+ return None
211
+
212
+
213
+ def _restore_namespace(
214
+ agent: object,
215
+ entry: dict[str, Any],
216
+ ) -> None:
217
+ """Put the namespace back exactly as it was at entry."""
218
+
219
+ namespace = _namespace_of(agent)
220
+
221
+ if namespace is None:
222
+ return
223
+
224
+ if isinstance(namespace, dict):
225
+ namespace.clear()
226
+ namespace.update(entry)
227
+ return
228
+
229
+ for name in list(namespace.keys()):
230
+ if name not in entry:
231
+ namespace.pop(name, None)
232
+
233
+ for name, value in entry.items():
234
+ if namespace.get(name, _MISSING) is not value:
235
+ namespace[name] = value
236
+
237
+
238
+ def _name_of(
239
+ agent: object,
240
+ ) -> str:
241
+ """How an Agent is called in messages: a Tag by its own name, an
242
+ object by its type's."""
243
+
244
+ if isinstance(agent, type):
245
+ return agent.__name__
246
+
247
+ return type(agent).__name__
248
+
249
+
250
+ def _state_of(
251
+ agent: object,
252
+ ) -> _State | None:
253
+ """The Agent's state, or None. Read straight from the dictionary (or a
254
+ Tag's dictionary proxy): this is on the path of ``agent in Tag``."""
255
+
256
+ try:
257
+ return object.__getattribute__(
258
+ agent,
259
+ "__dict__",
260
+ ).get(STATE)
261
+ except AttributeError:
262
+ return None
263
+
264
+
265
+ def _state_for(
266
+ agent: object,
267
+ ) -> _State:
268
+ """The Agent's state, attached on first use."""
269
+
270
+ namespace = _namespace_of(agent)
271
+
272
+ if namespace is None:
273
+ raise TagCompositionError(
274
+ f"{type(agent).__name__} cannot carry TOP state"
275
+ " (no instance dictionary)"
276
+ )
277
+
278
+ state = namespace.get(STATE)
279
+
280
+ if state is None:
281
+ state = _State(
282
+ host_type=type(agent),
283
+ pinned=agent if isinstance(agent, type) else None,
284
+ )
285
+ namespace[STATE] = state
286
+
287
+ return state
288
+
289
+
290
+ def _set_state(
291
+ agent: object,
292
+ state: _State,
293
+ ) -> None:
294
+ _namespace_of(agent)[STATE] = state
295
+
296
+
297
+ # ------------------------------------------------------------------
298
+ # Bound Actions
299
+ # ------------------------------------------------------------------
300
+
301
+
302
+ class _Bound:
303
+ """An Action bound to one Agent, stored in the Agent's dictionary.
304
+
305
+ Holds the Agent weakly so the Field's weak references stay honest.
306
+ """
307
+
308
+ __slots__ = (
309
+ "_function",
310
+ "_reference",
311
+ )
312
+
313
+ def __init__(
314
+ bound,
315
+ function: Function,
316
+ agent: object,
317
+ ) -> None:
318
+ bound._function = function
319
+ bound._reference = weakref.ref(agent)
320
+
321
+ def __call__(
322
+ bound,
323
+ *args: Any,
324
+ **kwargs: Any,
325
+ ) -> Any:
326
+ agent = bound._reference()
327
+
328
+ if agent is None:
329
+ raise ReferenceError("the Agent of this Action no longer exists")
330
+
331
+ return bound._function(
332
+ agent,
333
+ *args,
334
+ **kwargs,
335
+ )
336
+
337
+ @property
338
+ def __name__(
339
+ bound,
340
+ ) -> str:
341
+ return bound._function.__name__
342
+
343
+ @property
344
+ def __doc__(
345
+ bound,
346
+ ) -> str | None:
347
+ return bound._function.__doc__
348
+
349
+ @property
350
+ def __func__(
351
+ bound,
352
+ ) -> Function:
353
+ return bound._function
354
+
355
+ def __repr__(
356
+ bound,
357
+ ) -> str:
358
+ agent = bound._reference()
359
+ owner = _name_of(agent) if agent is not None else "<gone>"
360
+
361
+ return f"<Action {bound._function.__name__} of {owner}>"
362
+
363
+
364
+ class _Pinned_Operation:
365
+ """An Action a Pin landed on a Tag: an Operation of that Tag.
366
+
367
+ Read from the pinned Tag or from any of its Shapes, it binds to the
368
+ Tag it was read from, as a classmethod does, so a Shape inherits it
369
+ the way it inherits every Tag-scope member.
370
+ """
371
+
372
+ __slots__ = ("_function",)
373
+
374
+ def __init__(
375
+ operation,
376
+ function: Function,
377
+ ) -> None:
378
+ operation._function = function
379
+
380
+ def __get__(
381
+ operation,
382
+ instance: object,
383
+ owner: type | None = None,
384
+ ) -> Any:
385
+ if owner is None:
386
+ owner = type(instance)
387
+
388
+ return MethodType(
389
+ operation._function,
390
+ owner,
391
+ )
392
+
393
+ @property
394
+ def __func__(
395
+ operation,
396
+ ) -> Function:
397
+ return operation._function
398
+
399
+ def __repr__(
400
+ operation,
401
+ ) -> str:
402
+ return f"<pinned Operation {operation._function.__name__}>"
403
+
404
+
405
+ class _Originals:
406
+ """The pinned Tag's declarations as they were before its Pins patched
407
+ them, handed to a Pin's @Rip teardown as its second seat, so
408
+ un-patching is one deliberate line: ``tag.Control = original.Control``.
409
+ """
410
+
411
+ __slots__ = ("_declared",)
412
+
413
+ def __init__(
414
+ original,
415
+ declared: dict[str, Any],
416
+ ) -> None:
417
+ object.__setattr__(original, "_declared", dict(declared))
418
+
419
+ def __getattr__(
420
+ original,
421
+ name: str,
422
+ ) -> Any:
423
+ try:
424
+ return original._declared[name]
425
+ except KeyError:
426
+ raise AttributeError(
427
+ f"no original declaration named {name!r} was patched"
428
+ ) from None
429
+
430
+ def __setattr__(
431
+ original,
432
+ name: str,
433
+ value: Any,
434
+ ) -> None:
435
+ raise AttributeError("the originals are a record; they are read-only")
436
+
437
+ def __contains__(
438
+ original,
439
+ name: object,
440
+ ) -> bool:
441
+ return name in original._declared
442
+
443
+ def __iter__(
444
+ original,
445
+ ) -> Iterator[str]:
446
+ return iter(original._declared)
447
+
448
+ def __repr__(
449
+ original,
450
+ ) -> str:
451
+ return f"<originals of {', '.join(original._declared) or 'nothing'}>"
452
+
453
+
454
+ class _Composing_Pinned_Operation(_Pinned_Operation):
455
+ """A pinned Operation that opens the pinned Tag's composition door
456
+ while it runs, so the Pin's @Secret members resolve inside it."""
457
+
458
+ __slots__ = ("_state",)
459
+
460
+ def __init__(
461
+ operation,
462
+ function: Function,
463
+ state: "_State",
464
+ ) -> None:
465
+ super().__init__(function)
466
+ operation._state = state
467
+
468
+ def __get__(
469
+ operation,
470
+ instance: object,
471
+ owner: type | None = None,
472
+ ) -> Any:
473
+ if owner is None:
474
+ owner = type(instance)
475
+
476
+ function = operation._function
477
+ state = operation._state
478
+
479
+ def Composing(
480
+ *args: Any,
481
+ **kwargs: Any,
482
+ ) -> Any:
483
+ state.composing += 1
484
+
485
+ try:
486
+ return function(
487
+ owner,
488
+ *args,
489
+ **kwargs,
490
+ )
491
+ finally:
492
+ state.composing -= 1
493
+
494
+ Composing.__name__ = function.__name__
495
+ Composing.__doc__ = function.__doc__
496
+
497
+ return Composing
498
+
499
+
500
+ class _Composing_Bound(_Bound):
501
+ """A bound Action that opens the composition door while it runs, so
502
+ @Secret members resolve inside it."""
503
+
504
+ __slots__ = ()
505
+
506
+ def __call__(
507
+ bound,
508
+ *args: Any,
509
+ **kwargs: Any,
510
+ ) -> Any:
511
+ agent = bound._reference()
512
+
513
+ if agent is None:
514
+ raise ReferenceError("the Agent of this Action no longer exists")
515
+
516
+ state = _state_of(agent)
517
+ state.composing += 1
518
+
519
+ try:
520
+ return bound._function(
521
+ agent,
522
+ *args,
523
+ **kwargs,
524
+ )
525
+ finally:
526
+ state.composing -= 1
527
+
528
+
529
+ def _bind_to(
530
+ agent: object,
531
+ state: _State,
532
+ name: str,
533
+ ) -> None:
534
+ """Store the Agent's visible Action ``name`` in its dictionary."""
535
+
536
+ function = state.actions[name]
537
+
538
+ if state.pinned is not None:
539
+ _bind_pinned(
540
+ agent,
541
+ state,
542
+ name,
543
+ function,
544
+ )
545
+ return
546
+
547
+ if state.secrets:
548
+ bound: _Bound = _Composing_Bound(function, agent)
549
+ else:
550
+ bound = _Bound(function, agent)
551
+
552
+ _namespace_of(agent)[name] = bound
553
+
554
+
555
+ def _bind_pinned(
556
+ tag: type,
557
+ state: _State,
558
+ name: str,
559
+ function: Function,
560
+ ) -> None:
561
+ """A Pin's Action on a Tag: a secret one lives in the state and is
562
+ read through the door; the rest live in the class dictionary."""
563
+
564
+ if state.secrets:
565
+ bound: _Pinned_Operation = _Composing_Pinned_Operation(function, state)
566
+ else:
567
+ bound = _Pinned_Operation(function)
568
+
569
+ if name in state.secrets:
570
+ _namespace_of(tag).pop(name, None)
571
+ state.secret_values[name] = bound
572
+ else:
573
+ _namespace_of(tag)[name] = bound
574
+
575
+
576
+ def _rebind_all(
577
+ agent: object,
578
+ state: _State,
579
+ ) -> None:
580
+ for name in state.actions:
581
+ if not _is_dunder(name):
582
+ _bind_to(
583
+ agent,
584
+ state,
585
+ name,
586
+ )
587
+
588
+
589
+ # ------------------------------------------------------------------
590
+ # Descriptors installed on the runtime type
591
+ # ------------------------------------------------------------------
592
+
593
+
594
+ class _Deleted:
595
+ """A name a Tag deleted. Reads fail until something stores it again."""
596
+
597
+ __slots__ = ("name",)
598
+
599
+ def __init__(
600
+ gate,
601
+ name: str,
602
+ ) -> None:
603
+ gate.name = name
604
+
605
+ def __get__(
606
+ gate,
607
+ agent: object,
608
+ owner: type | None = None,
609
+ ) -> Any:
610
+ if agent is None:
611
+ return gate
612
+
613
+ value = _namespace_of(agent).get(
614
+ gate.name,
615
+ _MISSING,
616
+ )
617
+
618
+ if value is _MISSING:
619
+ raise AttributeError(
620
+ f"{type(agent).__name__} has no visible member"
621
+ f" {gate.name!r} (deleted by a Tag)"
622
+ )
623
+
624
+ return value
625
+
626
+ def __set__(
627
+ gate,
628
+ agent: object,
629
+ value: Any,
630
+ ) -> None:
631
+ _namespace_of(agent)[gate.name] = value
632
+
633
+ def __delete__(
634
+ gate,
635
+ agent: object,
636
+ ) -> None:
637
+ _namespace_of(agent).pop(
638
+ gate.name,
639
+ None,
640
+ )
641
+
642
+
643
+ class _Secret_Gate:
644
+ """A @Secret name. Resolves only while the Agent is composing."""
645
+
646
+ __slots__ = ("name",)
647
+
648
+ def __init__(
649
+ gate,
650
+ name: str,
651
+ ) -> None:
652
+ gate.name = name
653
+
654
+ def _open(
655
+ gate,
656
+ agent: object,
657
+ ) -> dict[str, Any]:
658
+ namespace = _namespace_of(agent)
659
+
660
+ if namespace[STATE].composing == 0:
661
+ raise AttributeError(
662
+ f"{gate.name!r} is a secret member of"
663
+ f" {type(agent).__name__}; it is reachable only from"
664
+ " its Tags' own Actions and protocols"
665
+ )
666
+
667
+ return namespace
668
+
669
+ def __get__(
670
+ gate,
671
+ agent: object,
672
+ owner: type | None = None,
673
+ ) -> Any:
674
+ if agent is None:
675
+ return gate
676
+
677
+ namespace = gate._open(agent)
678
+ value = namespace.get(
679
+ gate.name,
680
+ _MISSING,
681
+ )
682
+
683
+ if value is _MISSING:
684
+ raise AttributeError(
685
+ f"{type(agent).__name__} has no visible member"
686
+ f" {gate.name!r}"
687
+ )
688
+
689
+ return value
690
+
691
+ def __set__(
692
+ gate,
693
+ agent: object,
694
+ value: Any,
695
+ ) -> None:
696
+ gate._open(agent)[gate.name] = value
697
+
698
+ def __delete__(
699
+ gate,
700
+ agent: object,
701
+ ) -> None:
702
+ gate._open(agent).pop(
703
+ gate.name,
704
+ None,
705
+ )
706
+
707
+
708
+ class _Published:
709
+ """A published Report: reads the Tag-scope value, read-only on the Agent."""
710
+
711
+ __slots__ = ("name",)
712
+
713
+ def __init__(
714
+ gate,
715
+ name: str,
716
+ ) -> None:
717
+ gate.name = name
718
+
719
+ def __get__(
720
+ gate,
721
+ agent: object,
722
+ owner: type | None = None,
723
+ ) -> Any:
724
+ if agent is None:
725
+ return gate
726
+
727
+ from .overlay import _require_membership
728
+
729
+ state = _namespace_of(agent)[STATE]
730
+ origin, _declared = state.reports[gate.name]
731
+ _require_membership(
732
+ agent,
733
+ origin,
734
+ gate.name,
735
+ "Report",
736
+ )
737
+
738
+ return getattr(
739
+ origin,
740
+ gate.name,
741
+ )
742
+
743
+ def __set__(
744
+ gate,
745
+ agent: object,
746
+ value: Any,
747
+ ) -> None:
748
+ raise AttributeError(
749
+ f"{gate.name!r} is a published Report; it is read-only on"
750
+ " the Agent and lives on the Tag"
751
+ )
752
+
753
+
754
+ # ------------------------------------------------------------------
755
+ # Runtime types
756
+ # ------------------------------------------------------------------
757
+
758
+
759
+ class Tagged:
760
+ """Marker base every Agent runtime type ends with."""
761
+
762
+ __slots__ = ()
763
+
764
+
765
+ _type_cache: "WeakValueDictionary[tuple, type]" = WeakValueDictionary()
766
+
767
+
768
+ def _dunder_actions(
769
+ state: _State,
770
+ ) -> dict[str, Function]:
771
+ return {
772
+ name: function
773
+ for name, function in state.actions.items()
774
+ if _is_dunder(name)
775
+ }
776
+
777
+
778
+ def _type_key_of(
779
+ state: _State,
780
+ ) -> tuple:
781
+ return (
782
+ state.host_type,
783
+ frozenset(state.deleted),
784
+ frozenset(state.secrets),
785
+ frozenset(state.published),
786
+ bool(state.postconditions),
787
+ any(_is_flag(tag) for tag in state.active),
788
+ tuple(
789
+ sorted(
790
+ (name, id(function))
791
+ for name, function in _dunder_actions(state).items()
792
+ )
793
+ ),
794
+ )
795
+
796
+
797
+ def _runtime_type_for(
798
+ state: _State,
799
+ ) -> type:
800
+ """The runtime type for a composition, shared across Agents that need
801
+ the same type-level behaviour."""
802
+
803
+ from .access import _hooks_for
804
+
805
+ key = _type_key_of(state)
806
+ shared = _type_cache.get(key)
807
+
808
+ if shared is not None:
809
+ return shared
810
+
811
+ host_type = state.host_type
812
+ dunders = _dunder_actions(state)
813
+ deleted = key[1]
814
+ secrets = key[2]
815
+ published = key[3]
816
+ has_posts = key[4]
817
+ has_flags = key[5]
818
+
819
+ namespace: dict[str, Any] = _hooks_for(
820
+ host_type,
821
+ has_posts,
822
+ has_flags,
823
+ )
824
+ namespace.update(dunders)
825
+
826
+ if not issubclass(host_type, type):
827
+ # A Tag's gates are answered on its miss path, never by a data
828
+ # descriptor on its metaclass (which would also intercept the
829
+ # class-attribute writes the kernel makes).
830
+ for name in deleted:
831
+ namespace[name] = _Deleted(name)
832
+
833
+ for name in secrets:
834
+ namespace[name] = _Secret_Gate(name)
835
+
836
+ for name in published:
837
+ namespace[name] = _Published(name)
838
+
839
+ if issubclass(host_type, Tagged):
840
+ bases: tuple[type, ...] = (host_type,)
841
+ else:
842
+ bases = (
843
+ host_type,
844
+ Tagged,
845
+ )
846
+
847
+ try:
848
+ runtime_type = type(
849
+ host_type.__name__,
850
+ bases,
851
+ namespace,
852
+ )
853
+ except TypeError as error:
854
+ raise TagCompositionError(
855
+ f"{host_type.__name__} cannot be actualized as an Agent"
856
+ ) from error
857
+
858
+ runtime_type.__qualname__ = host_type.__qualname__
859
+ runtime_type.__module__ = host_type.__module__
860
+ _type_cache[key] = runtime_type
861
+
862
+ return runtime_type