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/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)
|