small-python 0.1.0__py3-none-musllinux_1_2_aarch64.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.
- small_python/__init__.py +35 -0
- small_python/_runtime.py +642 -0
- small_python/_runtime.pyi +71 -0
- small_python/_wire.py +178 -0
- small_python/py.typed +0 -0
- small_python/spy-worker +0 -0
- small_python-0.1.0.dist-info/METADATA +81 -0
- small_python-0.1.0.dist-info/RECORD +13 -0
- small_python-0.1.0.dist-info/WHEEL +5 -0
- small_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- small_python-0.1.0.dist-info/licenses/csrc/licenses/python.txt +277 -0
- small_python-0.1.0.dist-info/licenses/csrc/licenses/unicode.txt +39 -0
- small_python-0.1.0.dist-info/top_level.txt +1 -0
small_python/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Run Small Python, a small embeddable Python engine, from CPython.
|
|
2
|
+
|
|
3
|
+
Each Runtime runs the engine in its own worker process, so nothing the engine
|
|
4
|
+
does can crash or take over this one.
|
|
5
|
+
|
|
6
|
+
import small_python
|
|
7
|
+
|
|
8
|
+
with small_python.Runtime(timeout=5) as rt:
|
|
9
|
+
@rt.function
|
|
10
|
+
def greet(name):
|
|
11
|
+
return f"hello {name}"
|
|
12
|
+
|
|
13
|
+
rt.eval("greet('spy')") # 'hello spy'
|
|
14
|
+
"""
|
|
15
|
+
from importlib import metadata as _metadata
|
|
16
|
+
|
|
17
|
+
from ._runtime import WORKER as _WORKER, Error, Object, Runtime, Timeout, WorkerError
|
|
18
|
+
|
|
19
|
+
__all__ = ["Error", "Object", "Runtime", "SPY_VERSION", "Timeout", "WorkerError"]
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
__version__ = _metadata.version("small-python")
|
|
23
|
+
except _metadata.PackageNotFoundError: # imported from a build tree, not installed
|
|
24
|
+
__version__ = "0+unknown"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __getattr__(name):
|
|
28
|
+
# The engine version, asked of the bundled worker on first use.
|
|
29
|
+
if name == "SPY_VERSION":
|
|
30
|
+
import subprocess
|
|
31
|
+
global SPY_VERSION
|
|
32
|
+
SPY_VERSION = subprocess.run([_WORKER, "--version"], capture_output=True, text=True,
|
|
33
|
+
check=True, env={}).stdout.strip()
|
|
34
|
+
return SPY_VERSION
|
|
35
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
small_python/_runtime.py
ADDED
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
"""The parent side of the worker protocol: a Runtime owns one spy-worker
|
|
2
|
+
process, takes turns with it, runs host callbacks, and proxies engine values.
|
|
3
|
+
See docs/packaging/python.md."""
|
|
4
|
+
import os
|
|
5
|
+
import select
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from . import _wire as wire
|
|
13
|
+
from ._wire import Violation
|
|
14
|
+
|
|
15
|
+
WORKER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "spy-worker")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Error(Exception):
|
|
19
|
+
"""An engine exception that escaped: type, message, exception, filename, lineno."""
|
|
20
|
+
|
|
21
|
+
filename = None
|
|
22
|
+
lineno = None
|
|
23
|
+
|
|
24
|
+
def __init__(self, type=None, message=None, exception=None):
|
|
25
|
+
super().__init__(type, message, exception)
|
|
26
|
+
|
|
27
|
+
type = property(lambda self: self.args[0] if self.args else None)
|
|
28
|
+
message = property(lambda self: self.args[1] if len(self.args) > 1 else None)
|
|
29
|
+
exception = property(lambda self: self.args[2] if len(self.args) > 2 else None)
|
|
30
|
+
|
|
31
|
+
def __str__(self):
|
|
32
|
+
return f"{self.type}: {self.message}" if self.message else str(self.type)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class WorkerError(RuntimeError):
|
|
36
|
+
"""The worker was lost: it crashed, broke the protocol, or was terminated."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message, returncode=None):
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
self.returncode = returncode
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Timeout(WorkerError, TimeoutError):
|
|
44
|
+
"""An operation ran past the runtime's timeout; the worker was terminated."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Engine exceptions that Python protocols expect as their own (Object operations).
|
|
48
|
+
_PROTOCOL = {"TypeError": TypeError, "KeyError": KeyError, "IndexError": IndexError,
|
|
49
|
+
"AttributeError": AttributeError, "StopIteration": StopIteration}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Runtime:
|
|
53
|
+
"""An independent Small Python engine running in its own worker process.
|
|
54
|
+
|
|
55
|
+
Keyword arguments: ``std`` registers the standard modules (sys, gc,
|
|
56
|
+
weakref, ...); ``step_limit`` bounds evaluation steps (0 is unlimited);
|
|
57
|
+
``memory_limit`` bounds the engine's memory in bytes (0 is unlimited);
|
|
58
|
+
``timeout`` bounds the seconds one operation may run (None is unlimited);
|
|
59
|
+
``output`` receives printed text (default ``sys.stdout.write``);
|
|
60
|
+
``loader`` maps a module name to its source, or None when not found;
|
|
61
|
+
``message_limit`` bounds a protocol message in bytes.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, *, std=True, step_limit=0, memory_limit=0, timeout=None,
|
|
65
|
+
output=None, loader=None, message_limit=64 << 20):
|
|
66
|
+
if hasattr(self, "_process"):
|
|
67
|
+
raise RuntimeError("the runtime is already initialized")
|
|
68
|
+
for name, limit in (("step_limit", step_limit), ("memory_limit", memory_limit)):
|
|
69
|
+
if not isinstance(limit, int) or limit < 0:
|
|
70
|
+
raise ValueError(f"{name} must be an int >= 0")
|
|
71
|
+
if not isinstance(message_limit, int) or not 1 <= message_limit <= 0xFFFFFFFF:
|
|
72
|
+
raise ValueError("message_limit must be an int from 1 to 2**32 - 1")
|
|
73
|
+
if timeout is not None and not timeout > 0:
|
|
74
|
+
raise ValueError("timeout must be positive or None")
|
|
75
|
+
if (output is not None and not callable(output)) or (loader is not None and not callable(loader)):
|
|
76
|
+
raise TypeError("output and loader must be callable or None")
|
|
77
|
+
self._process = None
|
|
78
|
+
self._lost = None
|
|
79
|
+
self._closed = False
|
|
80
|
+
self._timeout = timeout
|
|
81
|
+
self._output = output
|
|
82
|
+
self._loader = loader
|
|
83
|
+
self._message_limit = message_limit
|
|
84
|
+
self._functions = {} # function id -> callable
|
|
85
|
+
self._function_ids = {} # id(callable) -> function id
|
|
86
|
+
self._released = [] # handle ids of collected Objects, sent with the next message
|
|
87
|
+
self._failure = None # (token, exception) last raised into the engine
|
|
88
|
+
self._tokens = 0
|
|
89
|
+
self._mutex = threading.Lock()
|
|
90
|
+
self._owner = None
|
|
91
|
+
self._depth = 0
|
|
92
|
+
self._callbacks = 0
|
|
93
|
+
self._deadline = None
|
|
94
|
+
try:
|
|
95
|
+
self._process = subprocess.Popen(
|
|
96
|
+
[WORKER], stdin=subprocess.PIPE, stdout=subprocess.PIPE, bufsize=0,
|
|
97
|
+
env={}, cwd="/", close_fds=True, start_new_session=True)
|
|
98
|
+
except OSError as e:
|
|
99
|
+
raise WorkerError(f"cannot start the worker: {e}") from e
|
|
100
|
+
self._to = self._process.stdin.fileno()
|
|
101
|
+
self._from = self._process.stdout.fileno()
|
|
102
|
+
options = {"std": bool(std), "step_limit": step_limit, "memory_limit": memory_limit,
|
|
103
|
+
"message_limit": message_limit}
|
|
104
|
+
self.version = self._run(wire.INIT, options)
|
|
105
|
+
|
|
106
|
+
# Ownership: one thread at a time, and one top-level operation at a time.
|
|
107
|
+
|
|
108
|
+
def _run(self, kind, *fields):
|
|
109
|
+
if self._lost:
|
|
110
|
+
raise WorkerError(str(self._lost), self._lost.returncode)
|
|
111
|
+
if self._closed:
|
|
112
|
+
raise RuntimeError("the runtime is closed")
|
|
113
|
+
me = threading.get_ident()
|
|
114
|
+
with self._mutex:
|
|
115
|
+
if self._owner not in (None, me):
|
|
116
|
+
raise RuntimeError("the runtime is in use by another thread")
|
|
117
|
+
self._owner = me
|
|
118
|
+
self._depth += 1
|
|
119
|
+
try:
|
|
120
|
+
if self._depth == 1 and self._timeout is not None:
|
|
121
|
+
self._deadline = time.monotonic() + self._timeout
|
|
122
|
+
return self._request(kind, fields)
|
|
123
|
+
finally:
|
|
124
|
+
self._depth -= 1
|
|
125
|
+
if not self._depth:
|
|
126
|
+
self._owner = None
|
|
127
|
+
self._deadline = None
|
|
128
|
+
|
|
129
|
+
def _request(self, kind, fields):
|
|
130
|
+
data = wire.message(kind, fields, self._reference)
|
|
131
|
+
if len(data) - 4 > self._message_limit:
|
|
132
|
+
raise ValueError("message exceeds the limit")
|
|
133
|
+
# Anything that interrupts the exchange before the reply has arrived
|
|
134
|
+
# (KeyboardInterrupt, a timeout) leaves the worker mid-turn: lose it.
|
|
135
|
+
reader = None
|
|
136
|
+
try:
|
|
137
|
+
self._send(data)
|
|
138
|
+
reader = self._await_reply()
|
|
139
|
+
finally:
|
|
140
|
+
if reader is None:
|
|
141
|
+
self._lose(WorkerError("the worker was terminated while it was running"))
|
|
142
|
+
value = self._decode(reader, 1)[0]
|
|
143
|
+
if reader.data[0] == wire.RAISE:
|
|
144
|
+
self._check_error(value)
|
|
145
|
+
raise self._engine_error(value)
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
def _await_reply(self):
|
|
149
|
+
while True:
|
|
150
|
+
reader = self._receive()
|
|
151
|
+
kind = reader.data[0]
|
|
152
|
+
if kind in (wire.RESULT, wire.RAISE):
|
|
153
|
+
return reader
|
|
154
|
+
if kind not in (wire.HOST, wire.LOAD, wire.OUTPUT):
|
|
155
|
+
raise self._violation(f"unexpected message kind {kind:#x}")
|
|
156
|
+
self._serve(kind, reader)
|
|
157
|
+
|
|
158
|
+
# The stream.
|
|
159
|
+
|
|
160
|
+
def _send(self, data):
|
|
161
|
+
if self._released:
|
|
162
|
+
ids = []
|
|
163
|
+
while self._released: # pop is atomic; Objects may be collected meanwhile
|
|
164
|
+
ids.append(self._released.pop())
|
|
165
|
+
self._write(wire.message(wire.RELEASE, (tuple(ids),), self._reference))
|
|
166
|
+
self._write(data)
|
|
167
|
+
|
|
168
|
+
def _write(self, data):
|
|
169
|
+
view = memoryview(data)
|
|
170
|
+
while view:
|
|
171
|
+
self._wait(self._to, write=True)
|
|
172
|
+
try:
|
|
173
|
+
view = view[os.write(self._to, view):]
|
|
174
|
+
except BrokenPipeError:
|
|
175
|
+
raise self._exited() from None
|
|
176
|
+
|
|
177
|
+
def _read(self, n):
|
|
178
|
+
chunks = []
|
|
179
|
+
while n:
|
|
180
|
+
self._wait(self._from, write=False)
|
|
181
|
+
chunk = os.read(self._from, min(n, 1 << 20))
|
|
182
|
+
if not chunk:
|
|
183
|
+
raise self._exited()
|
|
184
|
+
chunks.append(chunk)
|
|
185
|
+
n -= len(chunk)
|
|
186
|
+
return b"".join(chunks)
|
|
187
|
+
|
|
188
|
+
def _wait(self, fd, write):
|
|
189
|
+
if self._deadline is None:
|
|
190
|
+
return
|
|
191
|
+
remaining = self._deadline - time.monotonic()
|
|
192
|
+
poll = select.poll()
|
|
193
|
+
poll.register(fd, select.POLLOUT if write else select.POLLIN)
|
|
194
|
+
if not poll.poll(max(remaining, 0) * 1000):
|
|
195
|
+
raise self._lose(Timeout(f"the operation exceeded the timeout of {self._timeout} seconds"))
|
|
196
|
+
|
|
197
|
+
def _receive(self):
|
|
198
|
+
n = int.from_bytes(self._read(4), "little")
|
|
199
|
+
if not 1 <= n <= self._message_limit:
|
|
200
|
+
raise self._violation("message length out of range")
|
|
201
|
+
return wire.Reader(self._read(n), self._obtain)
|
|
202
|
+
|
|
203
|
+
def _decode(self, reader, count):
|
|
204
|
+
try:
|
|
205
|
+
values = [reader.value() for _ in range(count)]
|
|
206
|
+
if reader.at != len(reader.data):
|
|
207
|
+
raise Violation("trailing bytes in message")
|
|
208
|
+
except Violation as e:
|
|
209
|
+
raise self._violation(str(e)) from None
|
|
210
|
+
reader.finish()
|
|
211
|
+
return values
|
|
212
|
+
|
|
213
|
+
# Losing the worker (protocol.md 8).
|
|
214
|
+
|
|
215
|
+
def _violation(self, what):
|
|
216
|
+
return self._lose(WorkerError(f"the worker broke the protocol: {what}"))
|
|
217
|
+
|
|
218
|
+
def _exited(self):
|
|
219
|
+
code = self._process.wait()
|
|
220
|
+
how = f"was killed by {_signal_name(-code)}" if code < 0 else f"exited with status {code}"
|
|
221
|
+
return self._lose(WorkerError(f"the worker {how}", code))
|
|
222
|
+
|
|
223
|
+
def _lose(self, error):
|
|
224
|
+
if self._lost is None:
|
|
225
|
+
self._lost = error
|
|
226
|
+
self._end()
|
|
227
|
+
return self._lost
|
|
228
|
+
|
|
229
|
+
def _end(self):
|
|
230
|
+
process = self._process
|
|
231
|
+
if process.poll() is None:
|
|
232
|
+
process.kill()
|
|
233
|
+
process.wait()
|
|
234
|
+
process.stdin.close()
|
|
235
|
+
process.stdout.close()
|
|
236
|
+
self._functions.clear()
|
|
237
|
+
self._function_ids.clear()
|
|
238
|
+
self._released.clear()
|
|
239
|
+
self._failure = None
|
|
240
|
+
self._output = self._loader = None
|
|
241
|
+
|
|
242
|
+
# Values.
|
|
243
|
+
|
|
244
|
+
def _reference(self, value):
|
|
245
|
+
if type(value) is Object:
|
|
246
|
+
if object.__getattribute__(value, "_runtime") is not self:
|
|
247
|
+
raise ValueError("the object belongs to another runtime")
|
|
248
|
+
return wire.handle(object.__getattribute__(value, "_id"))
|
|
249
|
+
if type(value) is _Named:
|
|
250
|
+
return wire.function(self._function_id(value.function), value.name)
|
|
251
|
+
if callable(value):
|
|
252
|
+
name = getattr(value, "__name__", None)
|
|
253
|
+
return wire.function(self._function_id(value), name if isinstance(name, str) else type(value).__name__)
|
|
254
|
+
raise TypeError(f"cannot convert '{type(value).__name__}' to an engine value")
|
|
255
|
+
|
|
256
|
+
def _function_id(self, function):
|
|
257
|
+
key = id(function)
|
|
258
|
+
fid = self._function_ids.get(key)
|
|
259
|
+
if fid is None:
|
|
260
|
+
fid = self._function_ids[key] = len(self._functions) + 1
|
|
261
|
+
self._functions[fid] = function
|
|
262
|
+
return fid
|
|
263
|
+
|
|
264
|
+
def _obtain(self, id):
|
|
265
|
+
return Object._new(self, id)
|
|
266
|
+
|
|
267
|
+
# Errors from the engine.
|
|
268
|
+
|
|
269
|
+
def _check_error(self, error):
|
|
270
|
+
keys = {"type": str, "message": str, "filename": str, "line": int, "exception": (Object, type(None)),
|
|
271
|
+
"token": int}
|
|
272
|
+
if type(error) is not dict or error.keys() != keys.keys() or \
|
|
273
|
+
not all(issubclass(type(error[k]), t) for k, t in keys.items()):
|
|
274
|
+
raise self._violation("malformed error")
|
|
275
|
+
|
|
276
|
+
def _engine_error(self, error):
|
|
277
|
+
cause = None
|
|
278
|
+
if self._failure and error["token"] == self._failure[0]:
|
|
279
|
+
cause = self._failure[1]
|
|
280
|
+
self._failure = None
|
|
281
|
+
if cause is not None and not isinstance(cause, Exception):
|
|
282
|
+
return cause
|
|
283
|
+
result = Error(error["type"], error["message"], error["exception"])
|
|
284
|
+
result.filename, result.lineno = error["filename"], error["line"]
|
|
285
|
+
result.__cause__ = cause
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
def _protocol_error(self, error, key=None):
|
|
289
|
+
"""For Object operations: the builtin exception Python protocols expect."""
|
|
290
|
+
cls = _PROTOCOL.get(error.type) if isinstance(error, Error) else None
|
|
291
|
+
if cls is None:
|
|
292
|
+
return error
|
|
293
|
+
if cls is KeyError and key is not None:
|
|
294
|
+
return KeyError(key)
|
|
295
|
+
return cls(error.message) if error.message else cls()
|
|
296
|
+
|
|
297
|
+
# Host callbacks (worker requests).
|
|
298
|
+
|
|
299
|
+
def _serve(self, kind, reader):
|
|
300
|
+
try:
|
|
301
|
+
self._dispatch(kind, reader)
|
|
302
|
+
except _Unconvertible as e:
|
|
303
|
+
# The request was read whole; the engine gets the conversion error.
|
|
304
|
+
self._send(wire.message(wire.RAISE, (self._failure_of(e.__cause__),), self._reference))
|
|
305
|
+
|
|
306
|
+
def _dispatch(self, kind, reader):
|
|
307
|
+
if kind == wire.HOST:
|
|
308
|
+
fid, args, kwargs = self._decode_fields(reader, 3)
|
|
309
|
+
function = self._functions.get(fid) if type(fid) is int else None
|
|
310
|
+
if function is None or type(args) is not tuple or type(kwargs) is not dict or \
|
|
311
|
+
not all(type(k) is str for k in kwargs):
|
|
312
|
+
raise self._violation("malformed host call")
|
|
313
|
+
run = lambda: function(*args, **kwargs)
|
|
314
|
+
elif kind == wire.LOAD:
|
|
315
|
+
(name,) = self._decode_fields(reader, 1)
|
|
316
|
+
if type(name) is not str:
|
|
317
|
+
raise self._violation("malformed load")
|
|
318
|
+
run = lambda: self._load(name)
|
|
319
|
+
else:
|
|
320
|
+
(text,) = self._decode_fields(reader, 1)
|
|
321
|
+
if type(text) is not str:
|
|
322
|
+
raise self._violation("malformed output")
|
|
323
|
+
run = lambda: self._write_output(text)
|
|
324
|
+
started = time.monotonic()
|
|
325
|
+
self._callbacks += 1
|
|
326
|
+
try:
|
|
327
|
+
result = run()
|
|
328
|
+
reply = wire.message(wire.RESULT, (result,), self._reference)
|
|
329
|
+
except BaseException as e:
|
|
330
|
+
if self._lost:
|
|
331
|
+
raise
|
|
332
|
+
reply = wire.message(wire.RAISE, (self._failure_of(e),), self._reference)
|
|
333
|
+
finally:
|
|
334
|
+
self._callbacks -= 1
|
|
335
|
+
if self._deadline is not None:
|
|
336
|
+
self._deadline += time.monotonic() - started
|
|
337
|
+
if len(reply) - 4 > self._message_limit:
|
|
338
|
+
reply = wire.message(wire.RAISE, (self._failure_of(ValueError("message exceeds the limit")),),
|
|
339
|
+
self._reference)
|
|
340
|
+
self._send(reply)
|
|
341
|
+
|
|
342
|
+
def _decode_fields(self, reader, count):
|
|
343
|
+
try:
|
|
344
|
+
return self._decode(reader, count)
|
|
345
|
+
except WorkerError:
|
|
346
|
+
raise
|
|
347
|
+
except Exception as e:
|
|
348
|
+
raise _Unconvertible from e
|
|
349
|
+
|
|
350
|
+
def _load(self, name):
|
|
351
|
+
if self._loader is None:
|
|
352
|
+
return None
|
|
353
|
+
source = self._loader(name)
|
|
354
|
+
if source is not None and not isinstance(source, str):
|
|
355
|
+
raise TypeError("a loader must return str or None")
|
|
356
|
+
return source
|
|
357
|
+
|
|
358
|
+
def _write_output(self, text):
|
|
359
|
+
# A failure is reported here; the engine's print raises its own OSError.
|
|
360
|
+
try:
|
|
361
|
+
if self._output is not None:
|
|
362
|
+
self._output(text)
|
|
363
|
+
elif sys.stdout is not None:
|
|
364
|
+
sys.stdout.write(text)
|
|
365
|
+
except Exception as e:
|
|
366
|
+
sys.unraisablehook(_Unraisable(e, self._output))
|
|
367
|
+
raise
|
|
368
|
+
|
|
369
|
+
def _failure_of(self, exception):
|
|
370
|
+
self._tokens += 1
|
|
371
|
+
self._failure = (self._tokens, exception)
|
|
372
|
+
if isinstance(exception, Error) and type(exception.exception) is Object and \
|
|
373
|
+
object.__getattribute__(exception.exception, "_runtime") is self:
|
|
374
|
+
return {"raise": exception.exception, "token": self._tokens}
|
|
375
|
+
try:
|
|
376
|
+
wire.message(wire.RESULT, (exception.args,), self._reference)
|
|
377
|
+
args = exception.args
|
|
378
|
+
except Exception:
|
|
379
|
+
args = None
|
|
380
|
+
return {"classes": tuple(c.__name__ for c in type(exception).__mro__), "args": args,
|
|
381
|
+
"message": str(exception), "token": self._tokens}
|
|
382
|
+
|
|
383
|
+
# The public API.
|
|
384
|
+
|
|
385
|
+
def _no_callback(self, what):
|
|
386
|
+
if self._callbacks and self._owner == threading.get_ident():
|
|
387
|
+
raise RuntimeError(f"cannot {what} while a host function of this runtime is running")
|
|
388
|
+
|
|
389
|
+
def eval(self, source, filename="<string>"):
|
|
390
|
+
"""Run source and return the value of its last expression statement."""
|
|
391
|
+
self._no_callback("evaluate source")
|
|
392
|
+
return self._run(wire.EVAL, _source(source), _str(filename))
|
|
393
|
+
|
|
394
|
+
def exec(self, source, filename="<string>"):
|
|
395
|
+
"""Run source for its effects."""
|
|
396
|
+
self._no_callback("evaluate source")
|
|
397
|
+
self._run(wire.EXEC, _source(source), _str(filename))
|
|
398
|
+
|
|
399
|
+
def call(self, function, /, *args, **kwargs):
|
|
400
|
+
"""Call a global name, an Object, or a callable, and return the result."""
|
|
401
|
+
return self._run(wire.CALL, function, args, kwargs)
|
|
402
|
+
|
|
403
|
+
def __getitem__(self, name):
|
|
404
|
+
try:
|
|
405
|
+
return self._run(wire.GET, _name(name))
|
|
406
|
+
except Error as e:
|
|
407
|
+
if e.type == "NameError":
|
|
408
|
+
raise KeyError(name) from None
|
|
409
|
+
raise
|
|
410
|
+
|
|
411
|
+
def __setitem__(self, name, value):
|
|
412
|
+
self._run(wire.SET, _name(name), value)
|
|
413
|
+
|
|
414
|
+
def __delitem__(self, name):
|
|
415
|
+
try:
|
|
416
|
+
self._run(wire.DELETE, _name(name))
|
|
417
|
+
except Error as e:
|
|
418
|
+
if e.type == "KeyError":
|
|
419
|
+
raise KeyError(name) from None
|
|
420
|
+
raise
|
|
421
|
+
|
|
422
|
+
def define(self, name, function):
|
|
423
|
+
"""Bind a Python callable as the global ``name``; returns it."""
|
|
424
|
+
if not callable(function):
|
|
425
|
+
raise TypeError("define() needs a callable")
|
|
426
|
+
self._run(wire.SET, _name(name), _Named(function, name))
|
|
427
|
+
return function
|
|
428
|
+
|
|
429
|
+
def function(self, func=None, *, name=None):
|
|
430
|
+
"""Decorator binding a Python callable as a global host function."""
|
|
431
|
+
def bind(func):
|
|
432
|
+
return self.define(name or func.__name__, func)
|
|
433
|
+
return bind(func) if func is not None else bind
|
|
434
|
+
|
|
435
|
+
def register_module(self, name, members=None, source=None):
|
|
436
|
+
"""Make ``import name`` create a module from members, then source."""
|
|
437
|
+
if members is not None and (not isinstance(members, dict) or not all(isinstance(k, str) for k in members)):
|
|
438
|
+
raise TypeError("members must be a dict with str keys")
|
|
439
|
+
if source is not None and not isinstance(source, str):
|
|
440
|
+
raise TypeError("source must be a str")
|
|
441
|
+
self._run(wire.MODULE, _name(name), members, source)
|
|
442
|
+
|
|
443
|
+
def mount(self, directory, path=None, *, read_only=False):
|
|
444
|
+
"""Let the engine's open() reach a host directory at an engine path."""
|
|
445
|
+
directory = os.path.realpath(os.fsdecode(directory))
|
|
446
|
+
if not os.path.isdir(directory):
|
|
447
|
+
raise Error("OSError", f"cannot mount '{directory}': Not a directory")
|
|
448
|
+
if path is None:
|
|
449
|
+
path = directory
|
|
450
|
+
self._run(wire.MOUNT, directory, _str(path), bool(read_only))
|
|
451
|
+
|
|
452
|
+
def collect(self):
|
|
453
|
+
"""Run the engine's cycle collector; returns the number of values freed."""
|
|
454
|
+
return self._run(wire.COLLECT)
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def closed(self):
|
|
458
|
+
return self._closed or self._lost is not None
|
|
459
|
+
|
|
460
|
+
def close(self):
|
|
461
|
+
"""End the worker and drop every host reference. Idempotent."""
|
|
462
|
+
if self._closed or self._process is None:
|
|
463
|
+
self._closed = True
|
|
464
|
+
return
|
|
465
|
+
self._no_callback("close the runtime")
|
|
466
|
+
if self._owner not in (None, threading.get_ident()):
|
|
467
|
+
raise RuntimeError("the runtime is in use by another thread")
|
|
468
|
+
self._closed = True
|
|
469
|
+
if self._lost is None:
|
|
470
|
+
process = self._process
|
|
471
|
+
process.stdin.close()
|
|
472
|
+
try:
|
|
473
|
+
process.wait(timeout=1)
|
|
474
|
+
except subprocess.TimeoutExpired:
|
|
475
|
+
pass
|
|
476
|
+
self._end()
|
|
477
|
+
|
|
478
|
+
def __enter__(self):
|
|
479
|
+
return self
|
|
480
|
+
|
|
481
|
+
def __exit__(self, *exc):
|
|
482
|
+
self.close()
|
|
483
|
+
|
|
484
|
+
def __del__(self):
|
|
485
|
+
if getattr(self, "_process", None) is not None and not self._closed and self._lost is None:
|
|
486
|
+
self._closed = True
|
|
487
|
+
self._end()
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
class Object:
|
|
491
|
+
"""An engine value held by the worker, operated on by message."""
|
|
492
|
+
|
|
493
|
+
__slots__ = ("_runtime", "_id")
|
|
494
|
+
|
|
495
|
+
@classmethod
|
|
496
|
+
def _new(cls, runtime, id):
|
|
497
|
+
self = object.__new__(cls)
|
|
498
|
+
object.__setattr__(self, "_runtime", runtime)
|
|
499
|
+
object.__setattr__(self, "_id", id)
|
|
500
|
+
return self
|
|
501
|
+
|
|
502
|
+
def __init__(self, *args, **kwargs):
|
|
503
|
+
raise TypeError("Object values come from a Runtime")
|
|
504
|
+
|
|
505
|
+
def __del__(self):
|
|
506
|
+
try:
|
|
507
|
+
runtime = object.__getattribute__(self, "_runtime")
|
|
508
|
+
if not runtime.closed:
|
|
509
|
+
runtime._released.append(object.__getattribute__(self, "_id"))
|
|
510
|
+
except Exception: # a half-built Object, or interpreter shutdown
|
|
511
|
+
pass
|
|
512
|
+
|
|
513
|
+
def _op(self, op, *operands, key=None):
|
|
514
|
+
runtime = object.__getattribute__(self, "_runtime")
|
|
515
|
+
try:
|
|
516
|
+
return runtime._run(wire.OP, op, self, *operands)
|
|
517
|
+
except Error as e:
|
|
518
|
+
raise runtime._protocol_error(e, key) from None
|
|
519
|
+
|
|
520
|
+
def __getattribute__(self, name):
|
|
521
|
+
try:
|
|
522
|
+
return Object._op(self, 1, name)
|
|
523
|
+
except AttributeError:
|
|
524
|
+
if name.startswith("__") or name in Object.__dict__:
|
|
525
|
+
try:
|
|
526
|
+
return object.__getattribute__(self, name)
|
|
527
|
+
except AttributeError:
|
|
528
|
+
pass
|
|
529
|
+
raise
|
|
530
|
+
|
|
531
|
+
def __setattr__(self, name, value):
|
|
532
|
+
Object._op(self, 2, name, value)
|
|
533
|
+
|
|
534
|
+
def __delattr__(self, name):
|
|
535
|
+
raise TypeError("deleting attributes of engine values is not supported")
|
|
536
|
+
|
|
537
|
+
def __getitem__(self, key):
|
|
538
|
+
return Object._op(self, 3, key, key=key)
|
|
539
|
+
|
|
540
|
+
def __setitem__(self, key, value):
|
|
541
|
+
Object._op(self, 4, key, value, key=key)
|
|
542
|
+
|
|
543
|
+
def __delitem__(self, key):
|
|
544
|
+
raise TypeError("deleting items of engine values is not supported")
|
|
545
|
+
|
|
546
|
+
def __len__(self):
|
|
547
|
+
return Object._op(self, 5)
|
|
548
|
+
|
|
549
|
+
def __contains__(self, item):
|
|
550
|
+
return Object._op(self, 6, item)
|
|
551
|
+
|
|
552
|
+
def __iter__(self):
|
|
553
|
+
return Object._op(self, 7)
|
|
554
|
+
|
|
555
|
+
def __next__(self):
|
|
556
|
+
return Object._op(self, 8)
|
|
557
|
+
|
|
558
|
+
def __bool__(self):
|
|
559
|
+
return Object._op(self, 9)
|
|
560
|
+
|
|
561
|
+
def __hash__(self):
|
|
562
|
+
return Object._op(self, 10)
|
|
563
|
+
|
|
564
|
+
def _compare(self, op, other, equality):
|
|
565
|
+
runtime = object.__getattribute__(self, "_runtime")
|
|
566
|
+
if type(other) is Object and object.__getattribute__(other, "_runtime") is not runtime:
|
|
567
|
+
if equality:
|
|
568
|
+
return op == 14
|
|
569
|
+
return NotImplemented
|
|
570
|
+
try:
|
|
571
|
+
wire.message(wire.OP, (other,), runtime._reference)
|
|
572
|
+
except (TypeError, ValueError):
|
|
573
|
+
if equality:
|
|
574
|
+
return op == 14
|
|
575
|
+
return NotImplemented
|
|
576
|
+
return Object._op(self, op, other)
|
|
577
|
+
|
|
578
|
+
__lt__ = lambda self, other: Object._compare(self, 11, other, False)
|
|
579
|
+
__le__ = lambda self, other: Object._compare(self, 12, other, False)
|
|
580
|
+
__eq__ = lambda self, other: Object._compare(self, 13, other, True)
|
|
581
|
+
__ne__ = lambda self, other: Object._compare(self, 14, other, True)
|
|
582
|
+
__gt__ = lambda self, other: Object._compare(self, 15, other, False)
|
|
583
|
+
__ge__ = lambda self, other: Object._compare(self, 16, other, False)
|
|
584
|
+
|
|
585
|
+
def __repr__(self):
|
|
586
|
+
runtime = object.__getattribute__(self, "_runtime")
|
|
587
|
+
if runtime.closed:
|
|
588
|
+
return "<small_python.Object (closed)>"
|
|
589
|
+
return Object._op(self, 17)
|
|
590
|
+
|
|
591
|
+
def __str__(self):
|
|
592
|
+
return Object._op(self, 18)
|
|
593
|
+
|
|
594
|
+
def __dir__(self):
|
|
595
|
+
return Object._op(self, 19)
|
|
596
|
+
|
|
597
|
+
def __call__(self, *args, **kwargs):
|
|
598
|
+
runtime = object.__getattribute__(self, "_runtime")
|
|
599
|
+
return runtime._run(wire.CALL, self, args, kwargs)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class _Unconvertible(Exception):
|
|
603
|
+
"""A request whose values decoded but could not be built."""
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
class _Named:
|
|
607
|
+
"""A callable passed in under a chosen name, as define() does."""
|
|
608
|
+
|
|
609
|
+
def __init__(self, function, name):
|
|
610
|
+
self.function, self.name = function, name
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
class _Unraisable:
|
|
614
|
+
def __init__(self, exception, obj):
|
|
615
|
+
self.exc_type, self.exc_value, self.exc_traceback = type(exception), exception, exception.__traceback__
|
|
616
|
+
self.err_msg = "Exception ignored in the runtime's output callback"
|
|
617
|
+
self.object = obj
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _str(value):
|
|
621
|
+
if not isinstance(value, str):
|
|
622
|
+
raise TypeError(f"expected str, not '{type(value).__name__}'")
|
|
623
|
+
return value
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _source(source):
|
|
627
|
+
if "\0" in _str(source):
|
|
628
|
+
raise ValueError("source code string cannot contain null bytes")
|
|
629
|
+
return source
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _name(name):
|
|
633
|
+
if not isinstance(name, str):
|
|
634
|
+
raise TypeError(f"global names must be str, not '{type(name).__name__}'")
|
|
635
|
+
return name
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _signal_name(number):
|
|
639
|
+
try:
|
|
640
|
+
return signal.Signals(number).name
|
|
641
|
+
except ValueError:
|
|
642
|
+
return f"signal {number}"
|