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/errors.py ADDED
@@ -0,0 +1,138 @@
1
+ """Failures and diagnostics.
2
+
3
+ Every failure names the semantic law it violates. A language profile may
4
+ map these to its own exception types, but must keep them distinct.
5
+
6
+ Three failures also carry the name of the check that failed, as a
7
+ subclass: ``TagPreconditionError.Is_A_Caster`` is raised when the
8
+ Precondition declared as ``Is_A_Caster`` refuses, so a program writes
9
+ ``except Precondition.Is_A_Caster:`` and reads its own words back.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+
15
+ class _Named(type):
16
+ """Metaclass for failures that name the check that failed.
17
+
18
+ ``Failure.Name`` is the subclass raised when the check called ``Name``
19
+ fails. A subclass exists only for a name that was declared, so a
20
+ misspelt handler is an AttributeError at the ``except``, not a handler
21
+ that never fires.
22
+ """
23
+
24
+ def __getattr__(
25
+ cls,
26
+ name: str,
27
+ ) -> type:
28
+ names = cls.__dict__.get("_names")
29
+
30
+ if names is None or name.startswith("_"):
31
+ raise AttributeError(name)
32
+
33
+ try:
34
+ return names[name]
35
+ except KeyError:
36
+ raise AttributeError(
37
+ f"{cls.__name__} has no {name!r}: no {cls._kind} called"
38
+ f" {name!r} has been declared"
39
+ ) from None
40
+
41
+ def Named(
42
+ cls,
43
+ name: str,
44
+ ) -> type:
45
+ """The subclass raised when the check called ``name`` fails.
46
+
47
+ Made once per name, when the check is declared.
48
+ """
49
+
50
+ names = cls.__dict__["_names"]
51
+
52
+ if name not in names:
53
+ names[name] = type(
54
+ name,
55
+ (cls,),
56
+ {
57
+ "__module__": cls.__module__,
58
+ "__qualname__": f"{cls.__qualname__}.{name}",
59
+ "name": name,
60
+ },
61
+ )
62
+
63
+ return names[name]
64
+
65
+
66
+ class TagError(Exception):
67
+ """Base failure for TopKit."""
68
+
69
+
70
+ class TagDeclarationError(TagError):
71
+ """A Tag declaration is invalid (bad mark combination, bad signature)."""
72
+
73
+
74
+ class TagCompositionError(TagError):
75
+ """Contributions cannot form a coherent Overlay, or a Record cannot
76
+ be materialized, or a teardown failed."""
77
+
78
+
79
+ class TagResolutionError(TagError):
80
+ """A required Underlay, Tag view, or contribution is unavailable."""
81
+
82
+
83
+ class TagRogueAccessError(TagResolutionError):
84
+ """A Rogue Agent reached a published member of a Tag it has left.
85
+
86
+ A published member answers members only (STEP-SPEC-10). The failure
87
+ says what happened in TOP's own words: an access, from a Rogue Agent.
88
+ It is a TOP failure and nothing else, so it is never swallowed by a
89
+ lower layer asking a different question.
90
+ """
91
+
92
+
93
+ class TagPreconditionError(TagError, metaclass=_Named):
94
+ """A Precondition refused the Tagging. Nothing committed.
95
+
96
+ ``TagPreconditionError.Name`` is the subclass raised for the
97
+ Precondition declared as ``Name``; ``error.name`` is that name.
98
+ """
99
+
100
+ _kind = "Precondition"
101
+ _names: dict[str, type] = {}
102
+ name: str | None = None
103
+
104
+
105
+ class TagImprintError(TagError, metaclass=_Named):
106
+ """An Imprint failed after the Tag applied. The Tag stays.
107
+
108
+ ``TagImprintError.Name`` is the subclass raised for the Imprint
109
+ declared as ``Name``; ``error.name`` is that name.
110
+ """
111
+
112
+ _kind = "Imprint"
113
+ _names: dict[str, type] = {}
114
+ name: str | None = None
115
+
116
+
117
+ class TagPostconditionError(TagError, metaclass=_Named):
118
+ """A Postcondition found the finished Tagging defective. The Tag stays.
119
+
120
+ ``TagPostconditionError.Name`` is the subclass raised for the
121
+ Postcondition declared as ``Name``; ``error.name`` is that name.
122
+ """
123
+
124
+ _kind = "Postcondition"
125
+ _names: dict[str, type] = {}
126
+ name: str | None = None
127
+
128
+
129
+ class TagContractError(TagError):
130
+ """A condition yielded a non-boolean (truthy/falsy) value."""
131
+
132
+
133
+ class TagOverwriteWarning(UserWarning):
134
+ """An independent Tag replaced a visible contribution."""
135
+
136
+
137
+ class TagContractWarning(UserWarning):
138
+ """A Shape weakened a Base Postcondition without @Underlay."""
TopKit/fields.py ADDED
@@ -0,0 +1,165 @@
1
+ """Fields: the population of Agents carrying a Tag.
2
+
3
+ A Field never keeps an Agent alive. Membership is indexed by identity so
4
+ registration and removal are constant-time. Iterating a Tag gives the
5
+ sound population (every visible Postcondition holds), ``~Tag`` the
6
+ defective one, ``Tag[:]`` everyone.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Callable
12
+ from typing import Iterator
13
+ import weakref
14
+
15
+ from .errors import TagCompositionError
16
+
17
+
18
+ class _Field:
19
+ """Whole population of one Tag, weakly held, in application order."""
20
+
21
+ def __init__(
22
+ field,
23
+ ) -> None:
24
+ field._members: dict[int, weakref.ReferenceType[object]] = {}
25
+
26
+ def Add(
27
+ field,
28
+ agent: object,
29
+ ) -> None:
30
+ key = id(agent)
31
+
32
+ if key in field._members:
33
+ return
34
+
35
+ try:
36
+ reference = weakref.ref(
37
+ agent,
38
+ lambda expired, key=key: field._Forget(key, expired),
39
+ )
40
+ except TypeError as error:
41
+ raise TagCompositionError(
42
+ "Tagged Agents must support weak references for Fields"
43
+ ) from error
44
+
45
+ field._members[key] = reference
46
+
47
+ def Remove(
48
+ field,
49
+ agent: object,
50
+ ) -> None:
51
+ field._members.pop(id(agent), None)
52
+
53
+ def _Forget(
54
+ field,
55
+ key: int,
56
+ expired: weakref.ReferenceType[object],
57
+ ) -> None:
58
+ if field._members.get(key) is expired:
59
+ del field._members[key]
60
+
61
+ def __contains__(
62
+ field,
63
+ agent: object,
64
+ ) -> bool:
65
+ reference = field._members.get(id(agent))
66
+
67
+ return (
68
+ reference is not None
69
+ and reference() is agent
70
+ )
71
+
72
+ def __iter__(
73
+ field,
74
+ ) -> Iterator[object]:
75
+ live = [
76
+ agent
77
+ for agent in (
78
+ reference()
79
+ for reference in list(field._members.values())
80
+ )
81
+ if agent is not None
82
+ ]
83
+
84
+ return iter(live)
85
+
86
+ def __len__(
87
+ field,
88
+ ) -> int:
89
+ return sum(
90
+ 1
91
+ for reference in list(field._members.values())
92
+ if reference() is not None
93
+ )
94
+
95
+ def __bool__(
96
+ field,
97
+ ) -> bool:
98
+ return any(
99
+ reference() is not None
100
+ for reference in list(field._members.values())
101
+ )
102
+
103
+
104
+ class _Partition:
105
+ """One half of a Field: the Agents for which ``holds`` is True."""
106
+
107
+ def __init__(
108
+ partition,
109
+ field: _Field,
110
+ holds: Callable[[object], bool],
111
+ label: str,
112
+ ) -> None:
113
+ partition._field = field
114
+ partition._holds = holds
115
+ partition._label = label
116
+
117
+ def __iter__(
118
+ partition,
119
+ ) -> Iterator[object]:
120
+ return (
121
+ agent
122
+ for agent in partition._field
123
+ if partition._holds(agent)
124
+ )
125
+
126
+ def __contains__(
127
+ partition,
128
+ agent: object,
129
+ ) -> bool:
130
+ return (
131
+ agent in partition._field
132
+ and partition._holds(agent)
133
+ )
134
+
135
+ def __len__(
136
+ partition,
137
+ ) -> int:
138
+ return sum(
139
+ 1
140
+ for _ in partition
141
+ )
142
+
143
+ def __bool__(
144
+ partition,
145
+ ) -> bool:
146
+ return any(
147
+ True
148
+ for _ in partition
149
+ )
150
+
151
+ def __invert__(
152
+ partition,
153
+ ) -> "_Partition":
154
+ holds = partition._holds
155
+
156
+ return _Partition(
157
+ partition._field,
158
+ lambda agent: not holds(agent),
159
+ "defective" if partition._label == "sound" else "sound",
160
+ )
161
+
162
+ def __repr__(
163
+ partition,
164
+ ) -> str:
165
+ return f"<{partition._label} Field>"
TopKit/geometry.py ADDED
@@ -0,0 +1,120 @@
1
+ """Geometry: how Tags relate. A Base forms into a Shape.
2
+
3
+ The Form of a Tag is its ordered, duplicate-free, Base-first closure,
4
+ ending with the Tag itself. Applying a Tag follows its Form.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Iterable
10
+ from weakref import WeakKeyDictionary
11
+
12
+
13
+ _tag_types: tuple[type, type] | None = None
14
+ _form_cache: "WeakKeyDictionary[type, tuple[type, ...]]" = WeakKeyDictionary()
15
+
16
+
17
+ def _is_tag(
18
+ candidate: object,
19
+ ) -> bool:
20
+ global _tag_types
21
+
22
+ if _tag_types is None:
23
+ from .tags import MetaTag
24
+ from .tags import Tag
25
+
26
+ _tag_types = (MetaTag, Tag)
27
+
28
+ meta, root = _tag_types
29
+
30
+ return (
31
+ isinstance(candidate, meta)
32
+ and candidate is not root
33
+ )
34
+
35
+
36
+ def _direct_bases(
37
+ tag: type,
38
+ ) -> tuple[type, ...]:
39
+ """The Bases a Tag declares directly, in declaration order."""
40
+
41
+ return tuple(
42
+ base
43
+ for base in tag.__bases__
44
+ if _is_tag(base)
45
+ )
46
+
47
+
48
+ def _form_of(
49
+ tag: type,
50
+ ) -> tuple[type, ...]:
51
+ """Base-first closure of one Tag: every required Base once, then the Tag."""
52
+
53
+ cached = _form_cache.get(tag)
54
+
55
+ if cached is not None:
56
+ return cached
57
+
58
+ form: list[type] = []
59
+
60
+ def Visit(
61
+ candidate: type,
62
+ ) -> None:
63
+ for base in _direct_bases(candidate):
64
+ Visit(base)
65
+
66
+ if candidate not in form:
67
+ form.append(candidate)
68
+
69
+ Visit(tag)
70
+
71
+ result = tuple(form)
72
+ _form_cache[tag] = result
73
+
74
+ return result
75
+
76
+
77
+ def _leaves(
78
+ active: Iterable[type],
79
+ ) -> tuple[type, ...]:
80
+ """Active Tags that no other active Tag specializes."""
81
+
82
+ active = tuple(active)
83
+
84
+ return tuple(
85
+ candidate
86
+ for candidate in active
87
+ if not any(
88
+ other is not candidate
89
+ and issubclass(other, candidate)
90
+ for other in active
91
+ )
92
+ )
93
+
94
+
95
+ def _requiring_shapes(
96
+ tag: type,
97
+ active: Iterable[type],
98
+ ) -> tuple[type, ...]:
99
+ """Active Shapes that still require ``tag`` as a Base."""
100
+
101
+ return tuple(
102
+ other
103
+ for other in active
104
+ if (
105
+ other is not tag
106
+ and issubclass(other, tag)
107
+ )
108
+ )
109
+
110
+
111
+ def _related(
112
+ one: type,
113
+ other: type,
114
+ ) -> bool:
115
+ """True when one Tag is a Base or Shape of the other."""
116
+
117
+ return (
118
+ issubclass(one, other)
119
+ or issubclass(other, one)
120
+ )
TopKit/lifecycle.py ADDED
@@ -0,0 +1,220 @@
1
+ """Lifecycle: Rip, teardown, Scope, and exit protocols.
2
+
3
+ Rip ends active membership. Contributions are sticky: Actions and Records
4
+ stay on the Agent (a Rogue Agent) unless the Tag's @Rip teardowns change
5
+ them. Ripping a Base is refused while an active Shape still requires it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from contextlib import contextmanager
11
+ from typing import Any
12
+ from typing import Iterator
13
+ import atexit
14
+ import weakref
15
+
16
+ from .declarations import _parameters_of
17
+ from .declarations import _takes_underlay
18
+ from .errors import TagCompositionError
19
+ from .errors import TagError
20
+ from .errors import TagResolutionError
21
+ from .geometry import _requiring_shapes
22
+ from .state import _Originals
23
+ from .state import _State
24
+ from .state import _state_of
25
+
26
+
27
+ def _rip(
28
+ agent: object,
29
+ tag: type,
30
+ ) -> object:
31
+ state = _state_of(agent)
32
+
33
+ if state is None or tag not in state.active:
34
+ raise TagResolutionError(
35
+ f"{tag.__name__} is not active on this Agent"
36
+ )
37
+
38
+ blocking = _requiring_shapes(
39
+ tag,
40
+ state.active,
41
+ )
42
+
43
+ if blocking:
44
+ names = ", ".join(
45
+ shape.__name__
46
+ for shape in blocking
47
+ )
48
+
49
+ raise TagCompositionError(
50
+ f"{tag.__name__} is required by active Shape(s): {names}"
51
+ )
52
+
53
+ state.active.remove(tag)
54
+ tag._topkit_field.Remove(agent)
55
+
56
+ _teardown(
57
+ agent,
58
+ state,
59
+ tag,
60
+ )
61
+
62
+ return agent
63
+
64
+
65
+ def _teardown(
66
+ agent: object,
67
+ state: _State,
68
+ tag: type,
69
+ ) -> None:
70
+ """Run every @Rip teardown of ``tag`` once, then report failures."""
71
+
72
+ teardowns = state.rips.pop(tag, ())
73
+ failures: list[tuple[str, Exception]] = []
74
+ state.composing += 1
75
+
76
+ try:
77
+ for teardown in teardowns:
78
+ try:
79
+ _call_teardown(
80
+ teardown,
81
+ agent,
82
+ state,
83
+ )
84
+ except Exception as error:
85
+ failures.append(
86
+ (
87
+ teardown.__name__,
88
+ error,
89
+ )
90
+ )
91
+ finally:
92
+ state.composing -= 1
93
+
94
+ if failures:
95
+ names = ", ".join(
96
+ name
97
+ for name, _error in failures
98
+ )
99
+
100
+ raise TagCompositionError(
101
+ f"{tag.__name__} teardown failed in: {names}"
102
+ ) from failures[0][1]
103
+
104
+
105
+ def _teardown_all(
106
+ agent: object,
107
+ ) -> None:
108
+ """Best-effort teardown of every still-active Tag (finalizer, exit)."""
109
+
110
+ state = _state_of(agent)
111
+
112
+ if state is None:
113
+ return
114
+
115
+ for tag in reversed(list(state.active)):
116
+ for teardown in state.rips.pop(tag, ()):
117
+ try:
118
+ _call_teardown(
119
+ teardown,
120
+ agent,
121
+ state,
122
+ )
123
+ except Exception:
124
+ pass
125
+
126
+
127
+ def _call_teardown(
128
+ teardown: Any,
129
+ agent: object,
130
+ state: _State,
131
+ ) -> None:
132
+ """Run one @Rip teardown. On a pinned Tag, a teardown that declares a
133
+ seat after the receiver (and after its Underlay, if it takes one)
134
+ receives the Tag's original declarations, so un-patching is
135
+ ``tag.Control = original.Control``."""
136
+
137
+ if state.pinned is None:
138
+ teardown(agent)
139
+ return
140
+
141
+ declared = getattr(
142
+ teardown,
143
+ "__wrapped__",
144
+ teardown,
145
+ )
146
+ seats = _parameters_of(declared).positional
147
+ receiver_seats = 2 if _takes_underlay(declared) else 1
148
+
149
+ if seats > receiver_seats:
150
+ teardown(
151
+ agent,
152
+ _Originals(state.originals),
153
+ )
154
+ else:
155
+ teardown(agent)
156
+
157
+
158
+ @contextmanager
159
+ def Scope(
160
+ agent: object,
161
+ *tags: type,
162
+ **inputs: Any,
163
+ ) -> Iterator[object]:
164
+ """Apply Tags for a block and Rip them, in reverse, on exit, even if
165
+ the block raises. The guaranteed teardown path."""
166
+
167
+ applied: list[type] = []
168
+
169
+ try:
170
+ for tag in tags:
171
+ tag(
172
+ agent,
173
+ **inputs,
174
+ )
175
+ applied.append(tag)
176
+
177
+ yield agent
178
+ finally:
179
+ for tag in reversed(applied):
180
+ try:
181
+ _rip(
182
+ agent,
183
+ tag,
184
+ )
185
+ except TagError:
186
+ pass
187
+
188
+
189
+ _exit_registry: list[weakref.ReferenceType[object]] = []
190
+
191
+
192
+ def At_Exit(
193
+ agent: object,
194
+ ) -> object:
195
+ """Also run the Agent's teardowns at normal interpreter exit.
196
+
197
+ Registration is weak: it never keeps the Agent alive.
198
+ """
199
+
200
+ _exit_registry[:] = [
201
+ reference
202
+ for reference in _exit_registry
203
+ if reference() is not None
204
+ ]
205
+ _exit_registry.append(
206
+ weakref.ref(agent)
207
+ )
208
+
209
+ return agent
210
+
211
+
212
+ def _run_exit_protocols() -> None:
213
+ for reference in _exit_registry:
214
+ agent = reference()
215
+
216
+ if agent is not None:
217
+ _teardown_all(agent)
218
+
219
+
220
+ atexit.register(_run_exit_protocols)