weft-kernel 0.1.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.
- weft_kernel/__init__.py +178 -0
- weft_kernel/blocking.py +370 -0
- weft_kernel/context.py +250 -0
- weft_kernel/discovery.py +1181 -0
- weft_kernel/errors.py +73 -0
- weft_kernel/fallback.py +159 -0
- weft_kernel/payload/__init__.py +43 -0
- weft_kernel/payload/applicability.py +298 -0
- weft_kernel/payload/ext.py +172 -0
- weft_kernel/payload/ids.py +22 -0
- weft_kernel/payload/lineage.py +90 -0
- weft_kernel/payload/media_type.py +18 -0
- weft_kernel/payload/node.py +260 -0
- weft_kernel/payload/outcome.py +40 -0
- weft_kernel/payload/property.py +60 -0
- weft_kernel/payload/vector.py +28 -0
- weft_kernel/pipeline.py +748 -0
- weft_kernel/py.typed +0 -0
- weft_kernel/registry.py +692 -0
- weft_kernel/resolution.py +1582 -0
- weft_kernel/runner.py +1436 -0
- weft_kernel/seam.py +725 -0
- weft_kernel-0.1.0.dist-info/METADATA +88 -0
- weft_kernel-0.1.0.dist-info/RECORD +27 -0
- weft_kernel-0.1.0.dist-info/WHEEL +4 -0
- weft_kernel-0.1.0.dist-info/licenses/LICENSE +21 -0
- weft_kernel-0.1.0.dist-info/licenses/NOTICE +77 -0
weft_kernel/__init__.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""The Weft kernel.
|
|
2
|
+
|
|
3
|
+
What belongs here is settled in G1 and specified in `docs/01-high-level-plan.md`
|
|
4
|
+
under *The kernel boundary*: the kernel is what is required to express, load and
|
|
5
|
+
run contracts it knows nothing about, plus the domain types those signatures
|
|
6
|
+
unavoidably name — and nothing in the kernel performs RAG work.
|
|
7
|
+
|
|
8
|
+
Its two dependencies, `pydantic` and `opentelemetry-api`, are declared and
|
|
9
|
+
enforced by fitness function 1.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from weft_kernel.blocking import BlockingCallError
|
|
13
|
+
from weft_kernel.context import (
|
|
14
|
+
Context,
|
|
15
|
+
DuplicateServiceError,
|
|
16
|
+
ServiceRegistry,
|
|
17
|
+
ServiceRole,
|
|
18
|
+
UnresolvedServiceError,
|
|
19
|
+
)
|
|
20
|
+
from weft_kernel.discovery import (
|
|
21
|
+
ENTRY_POINT_GROUP,
|
|
22
|
+
Disclosure,
|
|
23
|
+
EntryPointLike,
|
|
24
|
+
EnvInterpolationError,
|
|
25
|
+
PackRegistrar,
|
|
26
|
+
PackReport,
|
|
27
|
+
PackSettingsError,
|
|
28
|
+
PackStatus,
|
|
29
|
+
PipelineResource,
|
|
30
|
+
allow_list_from_config,
|
|
31
|
+
discover,
|
|
32
|
+
interpolate_env,
|
|
33
|
+
)
|
|
34
|
+
from weft_kernel.errors import UnresolvedNameError, WeftError
|
|
35
|
+
from weft_kernel.fallback import Attempt, try_in_order
|
|
36
|
+
from weft_kernel.payload import (
|
|
37
|
+
SCHEMA_VERSION_KEY,
|
|
38
|
+
ExtMap,
|
|
39
|
+
ExtModel,
|
|
40
|
+
Failed,
|
|
41
|
+
Lineage,
|
|
42
|
+
MediaType,
|
|
43
|
+
Node,
|
|
44
|
+
NodeId,
|
|
45
|
+
NothingToProduce,
|
|
46
|
+
Outcome,
|
|
47
|
+
Produced,
|
|
48
|
+
Property,
|
|
49
|
+
SchemaVersionRefusedError,
|
|
50
|
+
SourceId,
|
|
51
|
+
SyntheticOrigin,
|
|
52
|
+
Vector,
|
|
53
|
+
)
|
|
54
|
+
from weft_kernel.pipeline import (
|
|
55
|
+
InsertOperator,
|
|
56
|
+
Pipeline,
|
|
57
|
+
SetOperator,
|
|
58
|
+
SlotDeclaration,
|
|
59
|
+
StageDeclaration,
|
|
60
|
+
)
|
|
61
|
+
from weft_kernel.registry import (
|
|
62
|
+
DuplicateRegistrationError,
|
|
63
|
+
MissingDestroysDeclarationError,
|
|
64
|
+
MissingRequiredDeclarationError,
|
|
65
|
+
Registry,
|
|
66
|
+
RegistryEntry,
|
|
67
|
+
UnknownPluginError,
|
|
68
|
+
)
|
|
69
|
+
from weft_kernel.resolution import (
|
|
70
|
+
Contribution,
|
|
71
|
+
InvalidStageConfigError,
|
|
72
|
+
OperatorIdCollisionError,
|
|
73
|
+
PipelineCycleError,
|
|
74
|
+
ResolvedPipeline,
|
|
75
|
+
ResolvedStage,
|
|
76
|
+
SlotOrderConflictError,
|
|
77
|
+
StageNotConfigurableError,
|
|
78
|
+
StaleOperatorTargetError,
|
|
79
|
+
UndefinedVarError,
|
|
80
|
+
UnknownParentPipelineError,
|
|
81
|
+
resolve,
|
|
82
|
+
)
|
|
83
|
+
from weft_kernel.runner import (
|
|
84
|
+
FlushError,
|
|
85
|
+
IntactViolationError,
|
|
86
|
+
Lifetime,
|
|
87
|
+
PipelineResolutionError,
|
|
88
|
+
RunnablePipeline,
|
|
89
|
+
Runner,
|
|
90
|
+
RunSummary,
|
|
91
|
+
Stage,
|
|
92
|
+
StageCompositionError,
|
|
93
|
+
StageSpec,
|
|
94
|
+
TenantMismatchError,
|
|
95
|
+
UnknownFallbackError,
|
|
96
|
+
UnmetRequiresError,
|
|
97
|
+
)
|
|
98
|
+
from weft_kernel.seam import Deprecation, wrap, wrap_flush
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"ENTRY_POINT_GROUP",
|
|
102
|
+
"SCHEMA_VERSION_KEY",
|
|
103
|
+
"Attempt",
|
|
104
|
+
"BlockingCallError",
|
|
105
|
+
"Context",
|
|
106
|
+
"Contribution",
|
|
107
|
+
"Deprecation",
|
|
108
|
+
"Disclosure",
|
|
109
|
+
"DuplicateRegistrationError",
|
|
110
|
+
"DuplicateServiceError",
|
|
111
|
+
"EntryPointLike",
|
|
112
|
+
"EnvInterpolationError",
|
|
113
|
+
"ExtMap",
|
|
114
|
+
"ExtModel",
|
|
115
|
+
"Failed",
|
|
116
|
+
"FlushError",
|
|
117
|
+
"InsertOperator",
|
|
118
|
+
"IntactViolationError",
|
|
119
|
+
"InvalidStageConfigError",
|
|
120
|
+
"Lifetime",
|
|
121
|
+
"Lineage",
|
|
122
|
+
"MediaType",
|
|
123
|
+
"MissingDestroysDeclarationError",
|
|
124
|
+
"MissingRequiredDeclarationError",
|
|
125
|
+
"Node",
|
|
126
|
+
"NodeId",
|
|
127
|
+
"NothingToProduce",
|
|
128
|
+
"OperatorIdCollisionError",
|
|
129
|
+
"Outcome",
|
|
130
|
+
"PackRegistrar",
|
|
131
|
+
"PackReport",
|
|
132
|
+
"PackSettingsError",
|
|
133
|
+
"PackStatus",
|
|
134
|
+
"Pipeline",
|
|
135
|
+
"PipelineCycleError",
|
|
136
|
+
"PipelineResolutionError",
|
|
137
|
+
"PipelineResource",
|
|
138
|
+
"Produced",
|
|
139
|
+
"Property",
|
|
140
|
+
"Registry",
|
|
141
|
+
"RegistryEntry",
|
|
142
|
+
"ResolvedPipeline",
|
|
143
|
+
"ResolvedStage",
|
|
144
|
+
"RunnablePipeline",
|
|
145
|
+
"Runner",
|
|
146
|
+
"RunSummary",
|
|
147
|
+
"SchemaVersionRefusedError",
|
|
148
|
+
"ServiceRegistry",
|
|
149
|
+
"ServiceRole",
|
|
150
|
+
"SetOperator",
|
|
151
|
+
"SlotDeclaration",
|
|
152
|
+
"SlotOrderConflictError",
|
|
153
|
+
"SourceId",
|
|
154
|
+
"Stage",
|
|
155
|
+
"StageCompositionError",
|
|
156
|
+
"StageDeclaration",
|
|
157
|
+
"StageNotConfigurableError",
|
|
158
|
+
"StageSpec",
|
|
159
|
+
"StaleOperatorTargetError",
|
|
160
|
+
"SyntheticOrigin",
|
|
161
|
+
"TenantMismatchError",
|
|
162
|
+
"UndefinedVarError",
|
|
163
|
+
"UnknownFallbackError",
|
|
164
|
+
"UnknownParentPipelineError",
|
|
165
|
+
"UnknownPluginError",
|
|
166
|
+
"UnmetRequiresError",
|
|
167
|
+
"UnresolvedNameError",
|
|
168
|
+
"UnresolvedServiceError",
|
|
169
|
+
"Vector",
|
|
170
|
+
"WeftError",
|
|
171
|
+
"allow_list_from_config",
|
|
172
|
+
"discover",
|
|
173
|
+
"interpolate_env",
|
|
174
|
+
"resolve",
|
|
175
|
+
"try_in_order",
|
|
176
|
+
"wrap",
|
|
177
|
+
"wrap_flush",
|
|
178
|
+
]
|
weft_kernel/blocking.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""The categorical blocking-call detector — fitness function 7(b).
|
|
2
|
+
|
|
3
|
+
Specified in `docs/01-high-level-plan.md` → *Fitness functions*, clause 7(b),
|
|
4
|
+
and `docs/02-extension-model.md` → *Colour*: while a stage runs, a detector
|
|
5
|
+
installed at the registration seam fails the build on a blocking call made on
|
|
6
|
+
the event loop thread — file IO, sockets, `time.sleep`, `subprocess`, and (for
|
|
7
|
+
free, because most of them go through a socket to do it) a synchronous
|
|
8
|
+
database driver. **Categorical, not a threshold.** The rejected alternative
|
|
9
|
+
was a slow-callback duration constant; it was rejected because its correct
|
|
10
|
+
value is machine-dependent and changes legitimately with every runner and
|
|
11
|
+
dependency bump, so it cannot be ratcheted the way the kernel budget can.
|
|
12
|
+
There is nothing to tune here: a call either happened on the loop thread
|
|
13
|
+
during a stage's `run`, or it did not.
|
|
14
|
+
|
|
15
|
+
**Detecting the operation, not the object.** A `socket.socket()` call
|
|
16
|
+
constructs an object; it does not block. What blocks is a *use* of that
|
|
17
|
+
object in blocking mode — `connect`, `recv`, `recv_into`, `send`, `sendall`
|
|
18
|
+
or `accept` called on a socket whose timeout is not `0`. `asyncio`'s own
|
|
19
|
+
`BaseEventLoop._connect_sock` builds every outbound connection's socket via
|
|
20
|
+
this same constructor and then immediately calls `sock.setblocking(False)`
|
|
21
|
+
(equivalent to `settimeout(0)`) before touching it — so a detector that
|
|
22
|
+
fired on construction would flag every legitimate async HTTP call, embedding
|
|
23
|
+
call and vector-store round trip a stage makes, which is exactly the
|
|
24
|
+
traffic `01`'s *Colour* section describes as "invisible to it by
|
|
25
|
+
construction". Checking the socket's own `gettimeout()` at the moment of the
|
|
26
|
+
operation keeps the detector categorical rather than reintroducing a
|
|
27
|
+
threshold: a socket is either in blocking mode or it is not, and Python
|
|
28
|
+
already tracks which.
|
|
29
|
+
|
|
30
|
+
The same six operations, plus `accept`, are patched a second time on
|
|
31
|
+
`ssl.SSLSocket`. `ssl.SSLSocket` overrides `connect`, `recv`, `recv_into`,
|
|
32
|
+
`send`, `sendall` and `accept` rather than inheriting `socket.socket`'s —
|
|
33
|
+
its read/write path runs through the TLS layer (`self._sslobj`), not the
|
|
34
|
+
plain-socket implementation the patches above replace — so a synchronous
|
|
35
|
+
HTTPS client (`requests`, a sync `httpx.Client`, most sync vendor SDKs)
|
|
36
|
+
would otherwise be caught only at `connect` (which does route through
|
|
37
|
+
`socket.socket.connect`) and never at the read or write that actually
|
|
38
|
+
stalls the loop. `ssl` is imported lazily, inside `_install`, so the kernel
|
|
39
|
+
does not pay the interpreter's TLS/OpenSSL binding cost at import time for
|
|
40
|
+
a stage that never makes a network call.
|
|
41
|
+
|
|
42
|
+
The same distinction holds for `subprocess`. `subprocess.Popen(...)`
|
|
43
|
+
constructs an object; it does not block — it forks/execs and returns. What
|
|
44
|
+
blocks is a *use* of that object: `Popen.wait()` or `Popen.communicate()`.
|
|
45
|
+
`asyncio`'s own `_UnixSubprocessTransport._start` (the machinery behind
|
|
46
|
+
`await asyncio.create_subprocess_exec(...)`, the only correct way to run a
|
|
47
|
+
subprocess from async code) builds the child via this same constructor on
|
|
48
|
+
the event loop thread — so a detector that fired on construction would
|
|
49
|
+
raise inside every correct, awaited subprocess call a stage makes. Patching
|
|
50
|
+
`wait`/`communicate` instead loses no real detection: `subprocess.run`,
|
|
51
|
+
`check_output` and `check_call` all route through `communicate` then `wait`,
|
|
52
|
+
so every synchronous convenience wrapper is still caught unconditionally —
|
|
53
|
+
there is no timeout or blocking-mode flag to check first, because these two
|
|
54
|
+
calls block by definition. `asyncio.subprocess.Process.wait` goes through
|
|
55
|
+
the transport and its child watcher, never through `Popen.wait`, so the
|
|
56
|
+
awaited path never reaches the patch.
|
|
57
|
+
|
|
58
|
+
**Scope, and why it does not false-positive on the sanctioned escape hatch.**
|
|
59
|
+
`guard()` arms a `ContextVar` for the duration of one stage's `run` call,
|
|
60
|
+
recording the stage's label and the identifier of the thread running the
|
|
61
|
+
event loop at that moment. Several stdlib entry points are patched, once, to
|
|
62
|
+
consult it:
|
|
63
|
+
|
|
64
|
+
- `ContextVar` is copied per `asyncio.Task`, never shared, so a *second*,
|
|
65
|
+
unrelated pipeline running concurrently on the same loop carries its own
|
|
66
|
+
(unarmed) copy and is untouched — this is what "scoped to stage execution"
|
|
67
|
+
means at run time, not merely at the source level.
|
|
68
|
+
- `asyncio.to_thread` propagates the current `Context` into its worker thread
|
|
69
|
+
by design, so the var *is* still armed there — but the thread id captured
|
|
70
|
+
at `guard()`'s entry no longer matches `threading.get_ident()` inside the
|
|
71
|
+
worker, so a call a stage deliberately offloaded is not flagged. That is
|
|
72
|
+
the sanctioned way to make a blocking call, per `01` → *Colour*, and it is
|
|
73
|
+
excluded by construction rather than by an author remembering to mark it.
|
|
74
|
+
- Fixtures, imports and model downloads run before `guard()` is entered or
|
|
75
|
+
after it exits, so they were never in scope to begin with — `01` states
|
|
76
|
+
this as a design property of the seam, not a claim this module has to
|
|
77
|
+
enforce.
|
|
78
|
+
|
|
79
|
+
**What this does not, and must not, become.** `docs/02-extension-model.md`
|
|
80
|
+
names the trap directly: this detector sees only *blocking* calls on the loop
|
|
81
|
+
thread. An async HTTP client, or anything already behind `to_thread`, is
|
|
82
|
+
invisible to it by construction — sound for colour, unsound for security. It
|
|
83
|
+
must never be reused as a network- or filesystem-access observer.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
from __future__ import annotations
|
|
87
|
+
|
|
88
|
+
import builtins
|
|
89
|
+
import io
|
|
90
|
+
import socket
|
|
91
|
+
import subprocess
|
|
92
|
+
import threading
|
|
93
|
+
import time
|
|
94
|
+
from collections.abc import Callable, Generator
|
|
95
|
+
from contextlib import contextmanager
|
|
96
|
+
from contextvars import ContextVar
|
|
97
|
+
from dataclasses import dataclass
|
|
98
|
+
from typing import TYPE_CHECKING, Any
|
|
99
|
+
|
|
100
|
+
from weft_kernel.errors import WeftError
|
|
101
|
+
|
|
102
|
+
if TYPE_CHECKING:
|
|
103
|
+
import ssl
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class BlockingCallError(WeftError):
|
|
107
|
+
"""A stage made a blocking call on the event loop thread. Fitness function 7(b).
|
|
108
|
+
|
|
109
|
+
Raised from inside the blocking call itself — `open`, a socket operation,
|
|
110
|
+
`time.sleep`, or `Popen.wait`/`Popen.communicate` — so the traceback
|
|
111
|
+
points at the exact call site, not merely at the stage that eventually
|
|
112
|
+
made it.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class _Watch:
|
|
118
|
+
"""What one armed `guard()` window remembers, for the patched functions to consult."""
|
|
119
|
+
|
|
120
|
+
stage: str
|
|
121
|
+
loop_thread_id: int
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
_watch: ContextVar[_Watch | None] = ContextVar("weft_kernel_blocking_watch", default=None)
|
|
125
|
+
|
|
126
|
+
_installed = False
|
|
127
|
+
|
|
128
|
+
_real_open: Callable[..., Any] = builtins.open
|
|
129
|
+
_real_io_open: Callable[..., Any] = io.open
|
|
130
|
+
_real_sleep: Callable[..., None] = time.sleep
|
|
131
|
+
_real_popen_wait: Callable[..., int] = subprocess.Popen[Any].wait
|
|
132
|
+
_real_popen_communicate: Callable[..., tuple[Any, Any]] = subprocess.Popen[Any].communicate
|
|
133
|
+
_real_socket_connect: Callable[..., Any] = socket.socket.connect
|
|
134
|
+
_real_socket_recv: Callable[..., Any] = socket.socket.recv
|
|
135
|
+
_real_socket_recv_into: Callable[..., Any] = socket.socket.recv_into
|
|
136
|
+
_real_socket_send: Callable[..., Any] = socket.socket.send
|
|
137
|
+
_real_socket_sendall: Callable[..., Any] = socket.socket.sendall
|
|
138
|
+
_real_socket_accept: Callable[..., Any] = socket.socket.accept
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _ssl_patch_not_installed(*_args: Any, **_kwargs: Any) -> Any:
|
|
142
|
+
msg = "ssl.SSLSocket patch not installed"
|
|
143
|
+
raise RuntimeError(msg)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
_real_ssl_socket_connect: Callable[..., Any] = _ssl_patch_not_installed
|
|
147
|
+
_real_ssl_socket_recv: Callable[..., Any] = _ssl_patch_not_installed
|
|
148
|
+
_real_ssl_socket_recv_into: Callable[..., Any] = _ssl_patch_not_installed
|
|
149
|
+
_real_ssl_socket_send: Callable[..., Any] = _ssl_patch_not_installed
|
|
150
|
+
_real_ssl_socket_sendall: Callable[..., Any] = _ssl_patch_not_installed
|
|
151
|
+
_real_ssl_socket_accept: Callable[..., Any] = _ssl_patch_not_installed
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@contextmanager
|
|
155
|
+
def guard(stage: str) -> Generator[None]:
|
|
156
|
+
"""Scope the detector to `stage`'s execution, for the lifetime of this `with` block.
|
|
157
|
+
|
|
158
|
+
Installs the stdlib patches on first use (idempotent — a second `guard()`
|
|
159
|
+
call, nested or sequential, does not re-patch), then arms the watch for
|
|
160
|
+
this task on this thread. Reset on the way out, including on an
|
|
161
|
+
exception: `guard` never decides whether a stage failed, it only decides
|
|
162
|
+
whether a call it made was blocking.
|
|
163
|
+
"""
|
|
164
|
+
_install()
|
|
165
|
+
token = _watch.set(_Watch(stage=stage, loop_thread_id=threading.get_ident()))
|
|
166
|
+
try:
|
|
167
|
+
yield
|
|
168
|
+
finally:
|
|
169
|
+
_watch.reset(token)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _install() -> None:
|
|
173
|
+
"""Patch the stdlib's blocking entry points, once per process.
|
|
174
|
+
|
|
175
|
+
`builtins.open` and `io.open` are patched separately because they are
|
|
176
|
+
distinct callables — `pathlib.Path.read_text`/`read_bytes` route through
|
|
177
|
+
`io.open`, not `builtins.open`, and this repository's own ruff `PTH`
|
|
178
|
+
rules push authors toward `Path`, so the file-IO clause of 7(b) would be
|
|
179
|
+
inert for the idiom this codebase mandates if only `builtins.open` were
|
|
180
|
+
covered.
|
|
181
|
+
|
|
182
|
+
The socket entry points are the six *operations* that can block —
|
|
183
|
+
`connect`, `recv`, `recv_into`, `send`, `sendall`, `accept` — patched as
|
|
184
|
+
bound methods on `socket.socket` itself, not by substituting the class.
|
|
185
|
+
Substituting the class would make `isinstance(x, socket.socket)` false
|
|
186
|
+
for objects built through the real constructor, which `asyncio` and
|
|
187
|
+
`ssl` both rely on; patching methods in place leaves class identity, and
|
|
188
|
+
therefore `isinstance`, untouched.
|
|
189
|
+
|
|
190
|
+
The subprocess entry points are, by the same reasoning, the two
|
|
191
|
+
*operations* that can block — `Popen.wait`, `Popen.communicate` — not
|
|
192
|
+
`Popen.__init__`. Construction only forks/execs; `await
|
|
193
|
+
asyncio.create_subprocess_exec(...)` calls it on the event loop thread by
|
|
194
|
+
design (`_UnixSubprocessTransport._start`), so patching the constructor
|
|
195
|
+
would flag every correct, awaited subprocess call a stage makes. `wait`
|
|
196
|
+
and `communicate` block unconditionally when called synchronously — there
|
|
197
|
+
is no non-blocking mode to check for a `Popen`, unlike a socket's
|
|
198
|
+
`gettimeout()` — so both patches call `_check` on every invocation while
|
|
199
|
+
armed.
|
|
200
|
+
|
|
201
|
+
`ssl` is imported here, lazily, and its `SSLSocket` gets the same six
|
|
202
|
+
operations patched a second time — `connect`, `recv`, `recv_into`,
|
|
203
|
+
`send`, `sendall`, `accept` — because `SSLSocket` overrides all six
|
|
204
|
+
rather than inheriting `socket.socket`'s, so the patches above never see
|
|
205
|
+
a TLS socket's read or write. Guarded by the same `gettimeout() != 0`
|
|
206
|
+
check: a non-blocking TLS socket is exactly as exempt as a non-blocking
|
|
207
|
+
plain one.
|
|
208
|
+
|
|
209
|
+
Every patch is a pass-through outside an armed `guard()` window, and for
|
|
210
|
+
a socket, also a pass-through whenever the socket is not in blocking mode
|
|
211
|
+
(`gettimeout() == 0`) — nothing about their behaviour changes for code
|
|
212
|
+
that never runs as a stage, or for a socket already opted into async use.
|
|
213
|
+
This is what makes eager, process-wide installation safe: the gate this
|
|
214
|
+
function installs defaults to open.
|
|
215
|
+
"""
|
|
216
|
+
global _installed
|
|
217
|
+
global _real_ssl_socket_connect, _real_ssl_socket_recv, _real_ssl_socket_recv_into
|
|
218
|
+
global _real_ssl_socket_send, _real_ssl_socket_sendall, _real_ssl_socket_accept
|
|
219
|
+
if _installed:
|
|
220
|
+
return
|
|
221
|
+
builtins.open = _guarded_open
|
|
222
|
+
io.open = _guarded_io_open
|
|
223
|
+
time.sleep = _guarded_sleep
|
|
224
|
+
subprocess.Popen.wait = _guarded_popen_wait
|
|
225
|
+
subprocess.Popen.communicate = _guarded_popen_communicate
|
|
226
|
+
socket.socket.connect = _guarded_socket_connect
|
|
227
|
+
socket.socket.recv = _guarded_socket_recv
|
|
228
|
+
socket.socket.recv_into = _guarded_socket_recv_into
|
|
229
|
+
socket.socket.send = _guarded_socket_send
|
|
230
|
+
socket.socket.sendall = _guarded_socket_sendall
|
|
231
|
+
socket.socket.accept = _guarded_socket_accept
|
|
232
|
+
|
|
233
|
+
import ssl
|
|
234
|
+
|
|
235
|
+
_real_ssl_socket_connect = ssl.SSLSocket.connect
|
|
236
|
+
_real_ssl_socket_recv = ssl.SSLSocket.recv
|
|
237
|
+
_real_ssl_socket_recv_into = ssl.SSLSocket.recv_into
|
|
238
|
+
_real_ssl_socket_send = ssl.SSLSocket.send
|
|
239
|
+
_real_ssl_socket_sendall = ssl.SSLSocket.sendall
|
|
240
|
+
_real_ssl_socket_accept = ssl.SSLSocket.accept
|
|
241
|
+
ssl.SSLSocket.connect = _guarded_ssl_socket_connect
|
|
242
|
+
ssl.SSLSocket.recv = _guarded_ssl_socket_recv
|
|
243
|
+
ssl.SSLSocket.recv_into = _guarded_ssl_socket_recv_into
|
|
244
|
+
ssl.SSLSocket.send = _guarded_ssl_socket_send
|
|
245
|
+
ssl.SSLSocket.sendall = _guarded_ssl_socket_sendall
|
|
246
|
+
ssl.SSLSocket.accept = _guarded_ssl_socket_accept
|
|
247
|
+
|
|
248
|
+
_installed = True
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _check(call: str) -> None:
|
|
252
|
+
watch = _watch.get()
|
|
253
|
+
if watch is None or watch.loop_thread_id != threading.get_ident():
|
|
254
|
+
return
|
|
255
|
+
raise BlockingCallError(
|
|
256
|
+
f"stage '{watch.stage}' made a blocking call ({call}) on the event loop thread. "
|
|
257
|
+
f"Offload it — `await asyncio.to_thread(...)` — or use an async client instead. "
|
|
258
|
+
f"See fitness function 7(b), docs/01-high-level-plan.md.",
|
|
259
|
+
stage=watch.stage,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _guarded_open(*args: Any, **kwargs: Any) -> Any:
|
|
264
|
+
_check("open()")
|
|
265
|
+
return _real_open(*args, **kwargs)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _guarded_io_open(*args: Any, **kwargs: Any) -> Any:
|
|
269
|
+
_check("open()")
|
|
270
|
+
return _real_io_open(*args, **kwargs)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _guarded_sleep(*args: Any, **kwargs: Any) -> None:
|
|
274
|
+
_check("time.sleep()")
|
|
275
|
+
_real_sleep(*args, **kwargs)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _guarded_popen_wait(self: subprocess.Popen[Any], *args: Any, **kwargs: Any) -> int:
|
|
279
|
+
_check("subprocess.wait()")
|
|
280
|
+
return _real_popen_wait(self, *args, **kwargs)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _guarded_popen_communicate(
|
|
284
|
+
self: subprocess.Popen[Any], *args: Any, **kwargs: Any
|
|
285
|
+
) -> tuple[Any, Any]:
|
|
286
|
+
_check("subprocess.communicate()")
|
|
287
|
+
return _real_popen_communicate(self, *args, **kwargs)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _is_blocking(sock: socket.socket) -> bool:
|
|
291
|
+
"""A socket is in blocking mode unless it opted out with `settimeout(0)`.
|
|
292
|
+
|
|
293
|
+
`gettimeout()` returns `None` for the default blocking mode and a
|
|
294
|
+
`float` timeout for both the "blocking with a deadline" and the
|
|
295
|
+
non-blocking (`0`) modes — so the only value that means "this socket
|
|
296
|
+
will not block the loop thread" is exactly `0`.
|
|
297
|
+
"""
|
|
298
|
+
return sock.gettimeout() != 0
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _guarded_socket_connect(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
|
|
302
|
+
if _is_blocking(self):
|
|
303
|
+
_check("socket.connect()")
|
|
304
|
+
return _real_socket_connect(self, *args, **kwargs)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _guarded_socket_recv(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
|
|
308
|
+
if _is_blocking(self):
|
|
309
|
+
_check("socket.recv()")
|
|
310
|
+
return _real_socket_recv(self, *args, **kwargs)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _guarded_socket_recv_into(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
|
|
314
|
+
if _is_blocking(self):
|
|
315
|
+
_check("socket.recv_into()")
|
|
316
|
+
return _real_socket_recv_into(self, *args, **kwargs)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _guarded_socket_send(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
|
|
320
|
+
if _is_blocking(self):
|
|
321
|
+
_check("socket.send()")
|
|
322
|
+
return _real_socket_send(self, *args, **kwargs)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _guarded_socket_sendall(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
|
|
326
|
+
if _is_blocking(self):
|
|
327
|
+
_check("socket.sendall()")
|
|
328
|
+
return _real_socket_sendall(self, *args, **kwargs)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _guarded_socket_accept(self: socket.socket, *args: Any, **kwargs: Any) -> Any:
|
|
332
|
+
if _is_blocking(self):
|
|
333
|
+
_check("socket.accept()")
|
|
334
|
+
return _real_socket_accept(self, *args, **kwargs)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _guarded_ssl_socket_connect(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
|
|
338
|
+
if _is_blocking(self):
|
|
339
|
+
_check("ssl.SSLSocket.connect()")
|
|
340
|
+
return _real_ssl_socket_connect(self, *args, **kwargs)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _guarded_ssl_socket_recv(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
|
|
344
|
+
if _is_blocking(self):
|
|
345
|
+
_check("ssl.SSLSocket.recv()")
|
|
346
|
+
return _real_ssl_socket_recv(self, *args, **kwargs)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _guarded_ssl_socket_recv_into(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
|
|
350
|
+
if _is_blocking(self):
|
|
351
|
+
_check("ssl.SSLSocket.recv_into()")
|
|
352
|
+
return _real_ssl_socket_recv_into(self, *args, **kwargs)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _guarded_ssl_socket_send(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
|
|
356
|
+
if _is_blocking(self):
|
|
357
|
+
_check("ssl.SSLSocket.send()")
|
|
358
|
+
return _real_ssl_socket_send(self, *args, **kwargs)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _guarded_ssl_socket_sendall(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
|
|
362
|
+
if _is_blocking(self):
|
|
363
|
+
_check("ssl.SSLSocket.sendall()")
|
|
364
|
+
return _real_ssl_socket_sendall(self, *args, **kwargs)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _guarded_ssl_socket_accept(self: ssl.SSLSocket, *args: Any, **kwargs: Any) -> Any:
|
|
368
|
+
if _is_blocking(self):
|
|
369
|
+
_check("ssl.SSLSocket.accept()")
|
|
370
|
+
return _real_ssl_socket_accept(self, *args, **kwargs)
|