sayfirst-cli 0.2.0__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.
- sayfirst_cli/__init__.py +2 -0
- sayfirst_cli/approvals.py +127 -0
- sayfirst_cli/ask.py +167 -0
- sayfirst_cli/evidence.py +668 -0
- sayfirst_cli/exit_codes.py +88 -0
- sayfirst_cli/explain.py +35 -0
- sayfirst_cli/instrument/__init__.py +14 -0
- sayfirst_cli/instrument/commands.py +191 -0
- sayfirst_cli/instrument/engine.py +396 -0
- sayfirst_cli/instrument/harness.py +1162 -0
- sayfirst_cli/instrument/launch.py +417 -0
- sayfirst_cli/instrument/manifest.py +409 -0
- sayfirst_cli/instrument/verify.py +553 -0
- sayfirst_cli/main.py +78 -0
- sayfirst_cli/packs/__init__.py +10 -0
- sayfirst_cli/packs/database/NOTE.md +4 -0
- sayfirst_cli/packs/database/interpose.py +85 -0
- sayfirst_cli/packs/database/pack.toml +13 -0
- sayfirst_cli/packs/http-client/NOTE.md +4 -0
- sayfirst_cli/packs/http-client/interpose.py +72 -0
- sayfirst_cli/packs/http-client/pack.toml +13 -0
- sayfirst_cli/packs/subprocess/NOTE.md +4 -0
- sayfirst_cli/packs/subprocess/interpose.py +90 -0
- sayfirst_cli/packs/subprocess/pack.toml +13 -0
- sayfirst_cli/packs_cmd.py +115 -0
- sayfirst_cli/pages.py +87 -0
- sayfirst_cli/reads.py +254 -0
- sayfirst_cli/render.py +138 -0
- sayfirst_cli/trace.py +88 -0
- sayfirst_cli-0.2.0.dist-info/METADATA +297 -0
- sayfirst_cli-0.2.0.dist-info/RECORD +35 -0
- sayfirst_cli-0.2.0.dist-info/WHEEL +4 -0
- sayfirst_cli-0.2.0.dist-info/entry_points.txt +2 -0
- sayfirst_cli-0.2.0.dist-info/licenses/LICENSE +202 -0
- sayfirst_cli-0.2.0.dist-info/licenses/NOTICE +13 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The interposition engine: it installs what a pack declares, and knows no library.
|
|
3
|
+
|
|
4
|
+
Layer 1 of the instrumentation chain. It reads no policy, holds no cache and
|
|
5
|
+
learns nothing about why a decision came back as it did: it puts the boundary's
|
|
6
|
+
one shape in front of a named attribute and gets out of the way. A program can
|
|
7
|
+
use the boundary by hand with no engine at all, and that is a supported way to
|
|
8
|
+
use it rather than a workaround.
|
|
9
|
+
|
|
10
|
+
Two installation paths, because a process has exactly two states for any given
|
|
11
|
+
module. One already in the interpreter is replaced where it sits. One not yet
|
|
12
|
+
loaded is replaced by a single entry at the head of the import path, which asks
|
|
13
|
+
the entries behind it for the real answer and wraps only the loader — so the
|
|
14
|
+
module is found, built and executed exactly as it would have been, and the
|
|
15
|
+
declared attributes are replaced after its body has run. That is also why
|
|
16
|
+
`from … import …` receives the replacement: the name is bound after the module
|
|
17
|
+
is complete.
|
|
18
|
+
|
|
19
|
+
**The hole this mechanism has, stated rather than hidden.** A program that bound
|
|
20
|
+
a reference before this ran, or that was started by anything other than the
|
|
21
|
+
launcher, is not instrumented. Nothing here detects that, and nothing here
|
|
22
|
+
reports coverage it did not have — finding that gap is the verifier's job, and
|
|
23
|
+
the two deliberately share no bookkeeping, because a proof that trusts the thing
|
|
24
|
+
it is proving is not a proof.
|
|
25
|
+
|
|
26
|
+
**All or nothing, and one installation per engine.** Every point is resolved and
|
|
27
|
+
every replacement made before the first attribute is replaced, so an install
|
|
28
|
+
that refuses leaves the process exactly as it found it — a half-installed set of
|
|
29
|
+
points is a program running partly governed with nothing saying which part. And
|
|
30
|
+
an engine installs once: a second install would leave a point still waiting for
|
|
31
|
+
its module to be wrapped with a boundary and a scope it was not installed with.
|
|
32
|
+
|
|
33
|
+
**One refusal surfaces after the hand-off, and it cannot surface sooner.** A
|
|
34
|
+
point naming an attribute of a module that is not loaded yet cannot be checked
|
|
35
|
+
until that module has run: the attribute does not exist to be absent. So that
|
|
36
|
+
one arrives as `EngineMisuse` out of the program's own import, where the
|
|
37
|
+
launcher no longer rewrites anything (`launch.py` says why). Every other refusal
|
|
38
|
+
is found before the program starts.
|
|
39
|
+
|
|
40
|
+
Reversible by construction, which is what article 9 asks of the primary mode:
|
|
41
|
+
nothing is written anywhere — the pack's own directory included, whose
|
|
42
|
+
execution module is loaded through a loader with the bytecode write taken out
|
|
43
|
+
(`_NoBytecode` says how) — and `uninstall` puts back the very objects that were
|
|
44
|
+
taken away, not equal ones.
|
|
45
|
+
|
|
46
|
+
**This file names no library, and a guard walks its source to say so.** Not as
|
|
47
|
+
an identifier, not in a string, not in a docstring. An engine is exactly where
|
|
48
|
+
a special case for a popular library is cheapest to add and hardest to see, so
|
|
49
|
+
the rule is mechanical rather than remembered.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import importlib.util
|
|
55
|
+
import sys
|
|
56
|
+
from collections.abc import Mapping, Sequence
|
|
57
|
+
from importlib.machinery import ModuleSpec, SourceFileLoader
|
|
58
|
+
from types import ModuleType
|
|
59
|
+
from typing import Protocol
|
|
60
|
+
|
|
61
|
+
from .manifest import Pack, Point
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class EngineMisuse(RuntimeError):
|
|
65
|
+
"""The engine was asked for something it must refuse, and says which and why."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Wrapper(Protocol):
|
|
69
|
+
"""What a pack's execution module provides: the replacement for one attribute."""
|
|
70
|
+
|
|
71
|
+
def __call__(self, original: object, capability: str, boundary: object) -> object: ...
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Asking(Protocol):
|
|
75
|
+
"""The whole of what the engine needs from a boundary, and no more of it."""
|
|
76
|
+
|
|
77
|
+
def request(
|
|
78
|
+
self, capability: str, arguments: Mapping[str, object], *, scope: str
|
|
79
|
+
) -> object: ...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _InScope:
|
|
83
|
+
"""The boundary, with the scope this installation was made in already supplied.
|
|
84
|
+
|
|
85
|
+
The seam is « the engine calls only `request(capability, arguments)` and the
|
|
86
|
+
handle's `record_outcome` », and the call itself is made by a pack's wrapper.
|
|
87
|
+
The scope is not the pack's to know and not the pack's to choose — it is the
|
|
88
|
+
invocation's, named on the command line — so the engine binds it here rather
|
|
89
|
+
than widening the signature every pack implements. What a pack is handed is
|
|
90
|
+
still a thing with `request` and nothing else, called with a capability and
|
|
91
|
+
arguments, which is the seam unchanged.
|
|
92
|
+
|
|
93
|
+
Nothing else is forwarded. A pack that tried to choose a scope of its own
|
|
94
|
+
would find no parameter to choose it with, which is the point.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, boundary: Asking, scope: str) -> None:
|
|
98
|
+
self._boundary = boundary
|
|
99
|
+
self._scope = scope
|
|
100
|
+
|
|
101
|
+
def request(self, capability: str, arguments: Mapping[str, object]) -> object:
|
|
102
|
+
"""Ask in the scope the invocation named, about what the pack declared."""
|
|
103
|
+
return self._boundary.request(capability, arguments, scope=self._scope)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Engine:
|
|
107
|
+
"""One installation, plus the record needed to take it back exactly."""
|
|
108
|
+
|
|
109
|
+
def __init__(self) -> None:
|
|
110
|
+
self._taken: list[tuple[ModuleType, str, object]] = []
|
|
111
|
+
self._awaited: dict[str, list[tuple[Point, Wrapper]]] = {}
|
|
112
|
+
self._boundary: _InScope | None = None
|
|
113
|
+
self._finder: _Finder | None = None
|
|
114
|
+
self._installed = False
|
|
115
|
+
|
|
116
|
+
def install(self, packs: Sequence[Pack], boundary: Asking, *, scope: str) -> None:
|
|
117
|
+
"""Put the boundary in front of every point every pack declares.
|
|
118
|
+
|
|
119
|
+
Each pack's execution module is loaded first, so a pack that cannot
|
|
120
|
+
produce a wrapper is refused before anything at all is replaced: a
|
|
121
|
+
half-installed set of points is a program running partly governed with
|
|
122
|
+
nothing saying which part.
|
|
123
|
+
|
|
124
|
+
`scope` is required and has no default. An engine that picked one would
|
|
125
|
+
be choosing where a question is asked, which is the invocation's to
|
|
126
|
+
choose and nobody else's — and a default that quietly matched the
|
|
127
|
+
contract's would make a scope somebody typed indistinguishable from a
|
|
128
|
+
scope nobody did.
|
|
129
|
+
"""
|
|
130
|
+
if self._installed:
|
|
131
|
+
raise EngineMisuse(
|
|
132
|
+
"this engine is already installed; uninstall first. One installation per "
|
|
133
|
+
"engine, because a point still waiting for its module would otherwise be "
|
|
134
|
+
"wrapped with a boundary and a scope it was not installed with"
|
|
135
|
+
)
|
|
136
|
+
arriving = _planned(packs)
|
|
137
|
+
asking = _InScope(boundary, scope)
|
|
138
|
+
# Resolved and made in full before the first attribute is replaced, so an
|
|
139
|
+
# install that refuses leaves this process exactly as it found it.
|
|
140
|
+
prepared: list[tuple[ModuleType, Point, object, object]] = []
|
|
141
|
+
for name in sorted(arriving):
|
|
142
|
+
if name not in sys.modules:
|
|
143
|
+
continue
|
|
144
|
+
loaded = sys.modules[name]
|
|
145
|
+
for point, wrapper in arriving[name]:
|
|
146
|
+
original = _original_of(loaded, point)
|
|
147
|
+
prepared.append((loaded, point, original, _made(wrapper, original, point, asking)))
|
|
148
|
+
self._boundary = asking
|
|
149
|
+
for loaded, point, original, replacement in prepared:
|
|
150
|
+
setattr(loaded, point.attribute, replacement)
|
|
151
|
+
self._taken.append((loaded, point.attribute, original))
|
|
152
|
+
for name in sorted(arriving):
|
|
153
|
+
if name not in sys.modules:
|
|
154
|
+
self._awaited[name] = list(arriving[name])
|
|
155
|
+
if self._awaited:
|
|
156
|
+
self._finder = _Finder(self)
|
|
157
|
+
sys.meta_path.insert(0, self._finder)
|
|
158
|
+
self._installed = True
|
|
159
|
+
|
|
160
|
+
def uninstall(self) -> None:
|
|
161
|
+
"""Give every original back, and take the entry out of the import path.
|
|
162
|
+
|
|
163
|
+
Idempotent, and ordered last-first, so a point wrapped over another
|
|
164
|
+
engine's work restores the object that engine had left in place.
|
|
165
|
+
"""
|
|
166
|
+
while self._taken:
|
|
167
|
+
loaded, attribute, original = self._taken.pop()
|
|
168
|
+
setattr(loaded, attribute, original)
|
|
169
|
+
if self._finder is not None and self._finder in sys.meta_path:
|
|
170
|
+
sys.meta_path.remove(self._finder)
|
|
171
|
+
self._finder = None
|
|
172
|
+
self._awaited.clear()
|
|
173
|
+
self._boundary = None
|
|
174
|
+
self._installed = False
|
|
175
|
+
|
|
176
|
+
def _replace(self, loaded: ModuleType, point: Point, wrapper: Wrapper) -> None:
|
|
177
|
+
"""Replace one attribute of a module that has just finished loading.
|
|
178
|
+
|
|
179
|
+
The one path that can refuse after the program has started, because the
|
|
180
|
+
attribute did not exist to be absent until now.
|
|
181
|
+
"""
|
|
182
|
+
original = _original_of(loaded, point)
|
|
183
|
+
setattr(loaded, point.attribute, _made(wrapper, original, point, self._boundary))
|
|
184
|
+
self._taken.append((loaded, point.attribute, original))
|
|
185
|
+
|
|
186
|
+
def _awaiting(self, name: str) -> bool:
|
|
187
|
+
"""Whether any point is waiting for this module to be loaded."""
|
|
188
|
+
return name in self._awaited
|
|
189
|
+
|
|
190
|
+
def _arrived(self, name: str, loaded: ModuleType) -> None:
|
|
191
|
+
"""Called by the wrapped loader, once, after the module's own body has run."""
|
|
192
|
+
for point, wrapper in self._awaited.pop(name, []):
|
|
193
|
+
self._replace(loaded, point, wrapper)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class _Finder:
|
|
197
|
+
"""One entry at the head of the import path, for the modules a pack awaits.
|
|
198
|
+
|
|
199
|
+
It finds nothing itself. It asks the entries behind it for the real answer
|
|
200
|
+
and replaces the loader on it, so a module is located and executed by
|
|
201
|
+
whatever would have located and executed it — a finder of its own would be
|
|
202
|
+
this engine deciding how somebody's code is loaded, which is not its
|
|
203
|
+
business and would break the moment that code is packaged differently.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def __init__(self, engine: Engine) -> None:
|
|
207
|
+
self._engine = engine
|
|
208
|
+
self._asking: set[str] = set()
|
|
209
|
+
|
|
210
|
+
def find_spec(
|
|
211
|
+
self, fullname: str, path: object = None, target: ModuleType | None = None
|
|
212
|
+
) -> ModuleSpec | None:
|
|
213
|
+
if not self._engine._awaiting(fullname) or fullname in self._asking:
|
|
214
|
+
# Re-entrant because the entries behind may import on the way to an
|
|
215
|
+
# answer; answering `None` there lets the real answer through.
|
|
216
|
+
return None
|
|
217
|
+
self._asking.add(fullname)
|
|
218
|
+
try:
|
|
219
|
+
found = _spec_from_the_others(self, fullname, path, target)
|
|
220
|
+
finally:
|
|
221
|
+
self._asking.discard(fullname)
|
|
222
|
+
if found is None or found.loader is None:
|
|
223
|
+
return None
|
|
224
|
+
found.loader = _Loader(found.loader, self._engine, fullname)
|
|
225
|
+
return found
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class _Loader:
|
|
229
|
+
"""The real loader, with the pack's replacements applied after the body has run."""
|
|
230
|
+
|
|
231
|
+
def __init__(self, original: object, engine: Engine, fullname: str) -> None:
|
|
232
|
+
self._original = original
|
|
233
|
+
self._engine = engine
|
|
234
|
+
self._fullname = fullname
|
|
235
|
+
|
|
236
|
+
def create_module(self, spec: ModuleSpec) -> ModuleType | None:
|
|
237
|
+
make = getattr(self._original, "create_module", None)
|
|
238
|
+
return None if make is None else make(spec)
|
|
239
|
+
|
|
240
|
+
def exec_module(self, module: ModuleType) -> None:
|
|
241
|
+
self._original.exec_module(module) # type: ignore[attr-defined]
|
|
242
|
+
self._engine._arrived(self._fullname, module)
|
|
243
|
+
|
|
244
|
+
def __getattr__(self, name: str) -> object:
|
|
245
|
+
"""Everything else the import system may ask, answered by the real loader."""
|
|
246
|
+
return getattr(self._original, name)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _spec_from_the_others(
|
|
250
|
+
mine: _Finder, fullname: str, path: object, target: ModuleType | None
|
|
251
|
+
) -> ModuleSpec | None:
|
|
252
|
+
"""Ask every other entry of the import path, in order, as the interpreter would."""
|
|
253
|
+
for entry in list(sys.meta_path):
|
|
254
|
+
if entry is mine:
|
|
255
|
+
continue
|
|
256
|
+
ask = getattr(entry, "find_spec", None)
|
|
257
|
+
if ask is None:
|
|
258
|
+
continue
|
|
259
|
+
found = ask(fullname, path, target)
|
|
260
|
+
if found is not None:
|
|
261
|
+
return found
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class _NoBytecode(SourceFileLoader):
|
|
266
|
+
"""The interpreter's own source loader, with the bytecode write taken out.
|
|
267
|
+
|
|
268
|
+
One method writes that cache, and this is it. `SourceLoader.get_code`
|
|
269
|
+
compiles the source and hands the result to `_cache_bytecode`, which
|
|
270
|
+
`SourceFileLoader` adapts to `set_data` — and `get_code` treats a
|
|
271
|
+
`NotImplementedError` from it as a loader that does not cache, which is the
|
|
272
|
+
import system's own way of saying so. Overriding `set_data` to write
|
|
273
|
+
nothing therefore removes the write instead of moving it: there is no other
|
|
274
|
+
route from a load to a file. The spec's `cached` is not that route — the
|
|
275
|
+
path written is computed from the source path, not from the spec — which is
|
|
276
|
+
why it is not what this uses.
|
|
277
|
+
|
|
278
|
+
Why the engine bothers: a pack is a directory somebody else owns, and may
|
|
279
|
+
be one nothing may be written into — a system path, a container layer, a
|
|
280
|
+
signed bundle. This file claims the primary mode writes nothing anywhere,
|
|
281
|
+
and a pack's own directory is somewhere.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
def set_data(self, path: str, data: bytes, *, _mode: int = 0o666) -> None:
|
|
285
|
+
"""Write nothing: the one method a source loader writes its cache through."""
|
|
286
|
+
|
|
287
|
+
def get_code(self, fullname: str) -> object:
|
|
288
|
+
"""Compile the source, always: a cache found beside it is never read.
|
|
289
|
+
|
|
290
|
+
The ordinary loader prefers a bytecode file whose recorded size and
|
|
291
|
+
mtime match the source, so a `.pyc` planted beside `interpose.py`
|
|
292
|
+
would run in place of the file `sayfirst packs check` read and a person
|
|
293
|
+
audited. The execution module is the audited text and nothing else.
|
|
294
|
+
"""
|
|
295
|
+
path = self.get_filename(fullname)
|
|
296
|
+
return self.source_to_code(self.get_data(path), path)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _wrapper_of(pack: Pack) -> Wrapper:
|
|
300
|
+
"""Load the pack's own execution module by path, and take its `wrap`.
|
|
301
|
+
|
|
302
|
+
By path and never by name: a pack is a directory a person named on the
|
|
303
|
+
command line, not a distribution on the import path, and resolving one from
|
|
304
|
+
a name is a registry wearing another hat (article 9).
|
|
305
|
+
|
|
306
|
+
Through `_NoBytecode`, so the load leaves the pack's directory exactly as it
|
|
307
|
+
was found.
|
|
308
|
+
"""
|
|
309
|
+
location = pack.execution_module
|
|
310
|
+
name = f"sayfirst_pack_{pack.name}"
|
|
311
|
+
spec = importlib.util.spec_from_file_location(
|
|
312
|
+
name, location, loader=_NoBytecode(name, str(location))
|
|
313
|
+
)
|
|
314
|
+
if spec is None or spec.loader is None:
|
|
315
|
+
# Not reachable while the loader is supplied above, and kept: the
|
|
316
|
+
# published contract of `spec_from_file_location` is « a spec or
|
|
317
|
+
# nothing », and an engine that read attributes off `None` would answer
|
|
318
|
+
# a pack's problem with a traceback of its own. A file that is not a
|
|
319
|
+
# module this interpreter can load arrives below instead, out of
|
|
320
|
+
# `exec_module`, as the pack's failure said as one.
|
|
321
|
+
raise EngineMisuse(f"{location} is not a module this interpreter can load")
|
|
322
|
+
loaded = importlib.util.module_from_spec(spec)
|
|
323
|
+
try:
|
|
324
|
+
spec.loader.exec_module(loaded)
|
|
325
|
+
except Exception as failed:
|
|
326
|
+
# A pack is code the person running it chose, exactly like a dependency,
|
|
327
|
+
# so a pack that will not load is their pack to fix — never an answer
|
|
328
|
+
# about their program, and never a traceback out of this engine.
|
|
329
|
+
raise EngineMisuse(f"{location} did not load: {failed!r}") from failed
|
|
330
|
+
made = getattr(loaded, "wrap", None)
|
|
331
|
+
if not callable(made):
|
|
332
|
+
raise EngineMisuse(
|
|
333
|
+
f"{location} declares no callable wrap: the execution module's whole job is "
|
|
334
|
+
f"to produce the replacement, given the original attribute, the capability "
|
|
335
|
+
f"and the boundary"
|
|
336
|
+
)
|
|
337
|
+
return made
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _made(wrapper: Wrapper, original: object, point: Point, boundary: object) -> object:
|
|
341
|
+
"""The replacement the pack produces, or the pack's own failure said as one.
|
|
342
|
+
|
|
343
|
+
A wrapper factory that raises has not produced a wrapper, so there is
|
|
344
|
+
nothing to install and nothing was installed. Whatever it raised is carried
|
|
345
|
+
in the sentence rather than out of the engine, because the caller has to be
|
|
346
|
+
able to tell « your pack is wrong » from « the control plane said no ».
|
|
347
|
+
"""
|
|
348
|
+
try:
|
|
349
|
+
return wrapper(original, point.capability, boundary)
|
|
350
|
+
except EngineMisuse:
|
|
351
|
+
raise
|
|
352
|
+
except Exception as failed:
|
|
353
|
+
raise EngineMisuse(
|
|
354
|
+
f"the wrapper for {point.attribute} of {point.module} could not be made: {failed!r}"
|
|
355
|
+
) from failed
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _planned(packs: Sequence[Pack]) -> dict[str, list[tuple[Point, Wrapper]]]:
|
|
359
|
+
"""Every point every pack declares, by module, refusing a point claimed twice.
|
|
360
|
+
|
|
361
|
+
The execution modules are loaded here, which is the earliest anything can
|
|
362
|
+
refuse: a pack that will not load, or that produces no wrapper, is found
|
|
363
|
+
before a single attribute has been looked up.
|
|
364
|
+
"""
|
|
365
|
+
arriving: dict[str, list[tuple[Point, Wrapper]]] = {}
|
|
366
|
+
claimed: set[tuple[str, str]] = set()
|
|
367
|
+
for pack in packs:
|
|
368
|
+
wrapper = _wrapper_of(pack)
|
|
369
|
+
for point in pack.points:
|
|
370
|
+
claim = (point.module, point.attribute)
|
|
371
|
+
if claim in claimed:
|
|
372
|
+
raise EngineMisuse(
|
|
373
|
+
f"{point.attribute} of {point.module} is already wrapped: a second "
|
|
374
|
+
f"wrapper around the first would ask twice for one effect, and "
|
|
375
|
+
f"nothing afterwards could say which pack a caller meant"
|
|
376
|
+
)
|
|
377
|
+
claimed.add(claim)
|
|
378
|
+
arriving.setdefault(point.module, []).append((point, wrapper))
|
|
379
|
+
return arriving
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _original_of(loaded: ModuleType, point: Point) -> object:
|
|
383
|
+
"""The attribute a point names, or the refusal that it is not there.
|
|
384
|
+
|
|
385
|
+
The engine wraps what a pack names and invents nothing, so a point naming an
|
|
386
|
+
absent attribute is refused rather than passed over in silence (article 2):
|
|
387
|
+
a pack that appears to govern an effect it never wrapped is exactly the false
|
|
388
|
+
all-clear this chain exists to prevent.
|
|
389
|
+
"""
|
|
390
|
+
if not hasattr(loaded, point.attribute):
|
|
391
|
+
raise EngineMisuse(
|
|
392
|
+
f"{point.module} has no {point.attribute}: the engine wraps what a pack "
|
|
393
|
+
f"names and invents nothing, so a point naming an absent attribute is "
|
|
394
|
+
f"refused rather than passed over in silence (article 2)"
|
|
395
|
+
)
|
|
396
|
+
return getattr(loaded, point.attribute)
|