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.
@@ -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)