small-python 0.1.0__py3-none-macosx_11_0_arm64.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,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}")
@@ -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}"