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