tywrap 0.7.0 → 0.9.0
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.
- package/README.md +36 -11
- package/SECURITY.md +39 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/core/annotation-parser.d.ts +2 -1
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +6 -3
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/emit-call.d.ts.map +1 -1
- package/dist/core/emit-call.js +1 -1
- package/dist/core/emit-call.js.map +1 -1
- package/dist/core/generator.d.ts +23 -4
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +243 -170
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts +3 -2
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +5 -5
- package/dist/core/mapper.js.map +1 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +1 -3
- package/dist/dev.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/base-bridge.d.ts +3 -7
- package/dist/runtime/base-bridge.d.ts.map +1 -1
- package/dist/runtime/base-bridge.js +4 -17
- package/dist/runtime/base-bridge.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +3 -22
- package/dist/runtime/bounded-context.d.ts.map +1 -1
- package/dist/runtime/bounded-context.js +13 -53
- package/dist/runtime/bounded-context.js.map +1 -1
- package/dist/runtime/bridge-codec.d.ts +1 -1
- package/dist/runtime/bridge-codec.d.ts.map +1 -1
- package/dist/runtime/bridge-codec.js +78 -55
- package/dist/runtime/bridge-codec.js.map +1 -1
- package/dist/runtime/errors.d.ts +16 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +17 -0
- package/dist/runtime/errors.js.map +1 -1
- package/dist/runtime/frame-codec.d.ts +111 -0
- package/dist/runtime/frame-codec.d.ts.map +1 -0
- package/dist/runtime/frame-codec.js +352 -0
- package/dist/runtime/frame-codec.js.map +1 -0
- package/dist/runtime/http-transport.d.ts +1 -1
- package/dist/runtime/http-transport.d.ts.map +1 -1
- package/dist/runtime/http-transport.js +1 -1
- package/dist/runtime/http-transport.js.map +1 -1
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +7 -23
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +5 -78
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +120 -59
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +345 -78
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/pyodide-transport.d.ts +1 -1
- package/dist/runtime/pyodide-transport.d.ts.map +1 -1
- package/dist/runtime/pyodide-transport.js +2 -3
- package/dist/runtime/pyodide-transport.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +7 -36
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +37 -72
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/subprocess-transport.d.ts +134 -8
- package/dist/runtime/subprocess-transport.d.ts.map +1 -1
- package/dist/runtime/subprocess-transport.js +490 -65
- package/dist/runtime/subprocess-transport.js.map +1 -1
- package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
- package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
- package/dist/runtime/transport.d.ts +90 -18
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +21 -1
- package/dist/runtime/transport.js.map +1 -1
- package/dist/runtime/validators.d.ts +49 -0
- package/dist/runtime/validators.d.ts.map +1 -1
- package/dist/runtime/validators.js +152 -0
- package/dist/runtime/validators.js.map +1 -1
- package/dist/types/index.d.ts +14 -15
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts +3 -2
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +140 -21
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +3 -16
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/codec.d.ts +1 -0
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +165 -17
- package/dist/utils/codec.js.map +1 -1
- package/dist/utils/ir-cache.d.ts +2 -1
- package/dist/utils/ir-cache.d.ts.map +1 -1
- package/dist/utils/runtime.d.ts +0 -29
- package/dist/utils/runtime.d.ts.map +1 -1
- package/dist/utils/runtime.js +16 -107
- package/dist/utils/runtime.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/runtime/frame_codec.py +430 -0
- package/runtime/python_bridge.py +213 -54
- package/runtime/tywrap_bridge_core.py +217 -147
- package/src/config/index.ts +11 -0
- package/src/core/annotation-parser.ts +9 -5
- package/src/core/emit-call.ts +1 -7
- package/src/core/generator.ts +315 -205
- package/src/core/mapper.ts +8 -8
- package/src/dev.ts +1 -3
- package/src/index.ts +7 -3
- package/src/runtime/base-bridge.ts +5 -30
- package/src/runtime/bounded-context.ts +12 -67
- package/src/runtime/bridge-codec.ts +94 -65
- package/src/runtime/errors.ts +21 -0
- package/src/runtime/frame-codec.ts +469 -0
- package/src/runtime/http-transport.ts +6 -1
- package/src/runtime/index.ts +7 -6
- package/src/runtime/node.ts +17 -104
- package/src/runtime/pooled-transport.ts +424 -90
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/pyodide-transport.ts +7 -3
- package/src/runtime/rpc-client.ts +58 -93
- package/src/runtime/subprocess-transport.ts +585 -80
- package/src/runtime/timed-out-request-tracker.ts +1 -1
- package/src/runtime/transport.ts +112 -22
- package/src/runtime/validators.ts +204 -0
- package/src/types/index.ts +21 -35
- package/src/tywrap.ts +157 -30
- package/src/utils/cache.ts +3 -3
- package/src/utils/codec.ts +205 -16
- package/src/utils/ir-cache.ts +1 -1
- package/src/utils/runtime.ts +17 -128
- package/src/version.ts +1 -1
- package/dist/core/discovery.d.ts +0 -103
- package/dist/core/discovery.d.ts.map +0 -1
- package/dist/core/discovery.js +0 -380
- package/dist/core/discovery.js.map +0 -1
- package/dist/core/validation.d.ts +0 -102
- package/dist/core/validation.d.ts.map +0 -1
- package/dist/core/validation.js +0 -490
- package/dist/core/validation.js.map +0 -1
- package/dist/runtime/base.d.ts +0 -22
- package/dist/runtime/base.d.ts.map +0 -1
- package/dist/runtime/base.js +0 -23
- package/dist/runtime/base.js.map +0 -1
- package/dist/runtime/transport-pool.d.ts +0 -196
- package/dist/runtime/transport-pool.d.ts.map +0 -1
- package/dist/runtime/transport-pool.js +0 -418
- package/dist/runtime/transport-pool.js.map +0 -1
- package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/safe_codec.py +0 -352
- package/src/core/discovery.ts +0 -477
- package/src/core/validation.ts +0 -729
- package/src/runtime/base.ts +0 -24
- package/src/runtime/transport-pool.ts +0 -538
|
@@ -43,12 +43,11 @@ derived from TYWRAP_CODEC_FALLBACK=json so that "Node in json-fallback mode" and
|
|
|
43
43
|
import base64
|
|
44
44
|
import datetime as dt
|
|
45
45
|
import decimal
|
|
46
|
-
import functools
|
|
47
46
|
import importlib
|
|
48
47
|
import importlib.util
|
|
49
|
-
import inspect
|
|
50
48
|
import json
|
|
51
49
|
import math
|
|
50
|
+
import sys
|
|
52
51
|
import traceback
|
|
53
52
|
import uuid
|
|
54
53
|
from pathlib import Path, PurePath
|
|
@@ -64,10 +63,6 @@ class ProtocolError(Exception):
|
|
|
64
63
|
"""Raised for malformed requests (bad protocol/id/method/params)."""
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
class InstanceHandleError(ValueError):
|
|
68
|
-
"""Raised when an instance handle is unknown or no longer valid."""
|
|
69
|
-
|
|
70
|
-
|
|
71
66
|
class ImportNotAllowedError(PermissionError):
|
|
72
67
|
"""Raised when a requested module import is not on the active allowlist."""
|
|
73
68
|
|
|
@@ -96,8 +91,8 @@ class AttributeNotAllowedError(PermissionError):
|
|
|
96
91
|
# IMPORT / ATTRIBUTE ALLOWLIST (trust boundary enforcement)
|
|
97
92
|
# =============================================================================
|
|
98
93
|
#
|
|
99
|
-
# The bridge dispatches call
|
|
100
|
-
#
|
|
94
|
+
# The bridge dispatches call requests by importing the requested module and
|
|
95
|
+
# getattr-ing the requested function/class method. That is an
|
|
101
96
|
# arbitrary import+getattr+call surface, so two complementary guards live here.
|
|
102
97
|
# Both are PURE (no env reads) so the rules behave identically under the
|
|
103
98
|
# subprocess server and the in-WASM Pyodide server; the subprocess server derives
|
|
@@ -204,22 +199,6 @@ def resolve_allowed_attr_path(root, dotted_name, *, allow_private_attrs):
|
|
|
204
199
|
return obj
|
|
205
200
|
|
|
206
201
|
|
|
207
|
-
def is_accessor_attr(obj, attr_name):
|
|
208
|
-
"""
|
|
209
|
-
True when attr_name resolves to a @property or functools.cached_property on
|
|
210
|
-
obj's type — i.e. it is read by attribute access, not called.
|
|
211
|
-
|
|
212
|
-
Inspects type(obj)'s MRO via getattr_static (which never triggers the
|
|
213
|
-
descriptor protocol), NOT the instance dict. That matters for
|
|
214
|
-
cached_property: after the first read it stores its value in the instance
|
|
215
|
-
__dict__, so an instance-level static lookup would return the cached value
|
|
216
|
-
rather than the descriptor and misclassify it as a method on the next read.
|
|
217
|
-
Reading from the type keeps the classification stable across repeated reads.
|
|
218
|
-
"""
|
|
219
|
-
descriptor = inspect.getattr_static(type(obj), attr_name, None)
|
|
220
|
-
return isinstance(descriptor, (property, functools.cached_property))
|
|
221
|
-
|
|
222
|
-
|
|
223
202
|
class CodecError(Exception):
|
|
224
203
|
"""Raised when value encoding fails (e.g. NaN/Infinity not allowed)."""
|
|
225
204
|
|
|
@@ -269,22 +248,24 @@ def _deserialize_bytes_envelope(value):
|
|
|
269
248
|
return _NO_DESERIALIZE
|
|
270
249
|
|
|
271
250
|
|
|
272
|
-
def deserialize(value):
|
|
251
|
+
def deserialize(value, *, has_envelope_markers=True):
|
|
273
252
|
"""
|
|
274
253
|
Recursively deserialize request values into Python-native types.
|
|
275
254
|
|
|
276
255
|
Why: requests are JSON-only; we need a small set of explicit decoders
|
|
277
256
|
(currently bytes) to restore Python semantics at the boundary.
|
|
278
257
|
"""
|
|
258
|
+
if not has_envelope_markers:
|
|
259
|
+
return value
|
|
279
260
|
decoded = _deserialize_bytes_envelope(value)
|
|
280
261
|
if decoded is not _NO_DESERIALIZE:
|
|
281
262
|
return decoded
|
|
282
263
|
|
|
283
264
|
if isinstance(value, list):
|
|
284
|
-
return [deserialize(item) for item in value]
|
|
265
|
+
return [deserialize(item, has_envelope_markers=True) for item in value]
|
|
285
266
|
if isinstance(value, dict):
|
|
286
267
|
# Preserve dict shape while decoding nested values.
|
|
287
|
-
return {k: deserialize(v) for k, v in value.items()}
|
|
268
|
+
return {k: deserialize(v, has_envelope_markers=True) for k, v in value.items()}
|
|
288
269
|
return value
|
|
289
270
|
|
|
290
271
|
|
|
@@ -314,33 +295,33 @@ def module_available(module_name):
|
|
|
314
295
|
|
|
315
296
|
|
|
316
297
|
def is_numpy_array(obj):
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
298
|
+
"""Return whether obj is an ndarray without importing NumPy."""
|
|
299
|
+
np = sys.modules.get('numpy')
|
|
300
|
+
if np is None:
|
|
320
301
|
return False
|
|
321
302
|
return isinstance(obj, np.ndarray)
|
|
322
303
|
|
|
323
304
|
|
|
324
305
|
def is_pandas_dataframe(obj):
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
306
|
+
"""Return whether obj is a DataFrame without importing pandas."""
|
|
307
|
+
pd = sys.modules.get('pandas')
|
|
308
|
+
if pd is None:
|
|
328
309
|
return False
|
|
329
310
|
return isinstance(obj, pd.DataFrame)
|
|
330
311
|
|
|
331
312
|
|
|
332
313
|
def is_pandas_series(obj):
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
314
|
+
"""Return whether obj is a Series without importing pandas."""
|
|
315
|
+
pd = sys.modules.get('pandas')
|
|
316
|
+
if pd is None:
|
|
336
317
|
return False
|
|
337
318
|
return isinstance(obj, pd.Series)
|
|
338
319
|
|
|
339
320
|
|
|
340
321
|
def is_scipy_sparse(obj):
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
322
|
+
"""Return whether obj is sparse without importing SciPy."""
|
|
323
|
+
sp = sys.modules.get('scipy.sparse')
|
|
324
|
+
if sp is None:
|
|
344
325
|
return False
|
|
345
326
|
try:
|
|
346
327
|
return sp.issparse(obj)
|
|
@@ -349,9 +330,9 @@ def is_scipy_sparse(obj):
|
|
|
349
330
|
|
|
350
331
|
|
|
351
332
|
def is_torch_tensor(obj):
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
333
|
+
"""Return whether obj is a Tensor without importing PyTorch."""
|
|
334
|
+
torch = sys.modules.get('torch')
|
|
335
|
+
if torch is None:
|
|
355
336
|
return False
|
|
356
337
|
try:
|
|
357
338
|
return torch.is_tensor(obj)
|
|
@@ -360,11 +341,11 @@ def is_torch_tensor(obj):
|
|
|
360
341
|
|
|
361
342
|
|
|
362
343
|
def is_sklearn_estimator(obj):
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
344
|
+
"""Return whether obj is an estimator without importing scikit-learn."""
|
|
345
|
+
sklearn_base = sys.modules.get('sklearn.base')
|
|
346
|
+
if sklearn_base is None:
|
|
366
347
|
return False
|
|
367
|
-
return isinstance(obj, BaseEstimator)
|
|
348
|
+
return isinstance(obj, sklearn_base.BaseEstimator)
|
|
368
349
|
|
|
369
350
|
|
|
370
351
|
# =============================================================================
|
|
@@ -536,7 +517,10 @@ def serialize_sparse_matrix(obj):
|
|
|
536
517
|
raise RuntimeError('Failed to inspect scipy sparse matrix format') from exc
|
|
537
518
|
|
|
538
519
|
if fmt not in ('csr', 'csc', 'coo'):
|
|
539
|
-
raise RuntimeError(
|
|
520
|
+
raise RuntimeError(
|
|
521
|
+
f'Unsupported scipy sparse format: {fmt}; only csr/csc/coo are supported. '
|
|
522
|
+
'Convert explicitly (e.g. matrix.tocsr()) before returning'
|
|
523
|
+
)
|
|
540
524
|
|
|
541
525
|
dtype = None
|
|
542
526
|
try:
|
|
@@ -544,7 +528,10 @@ def serialize_sparse_matrix(obj):
|
|
|
544
528
|
except Exception:
|
|
545
529
|
dtype = None
|
|
546
530
|
if getattr(obj.dtype, 'kind', None) == 'c':
|
|
547
|
-
raise RuntimeError(
|
|
531
|
+
raise RuntimeError(
|
|
532
|
+
'Complex scipy sparse matrices are not supported by the JSON codec; '
|
|
533
|
+
'split into real/imag components explicitly before returning'
|
|
534
|
+
)
|
|
548
535
|
|
|
549
536
|
if fmt in ('csr', 'csc'):
|
|
550
537
|
data = obj.data.tolist()
|
|
@@ -584,8 +571,58 @@ def serialize_torch_tensor(obj, *, force_json_markers, torch_allow_copy=False):
|
|
|
584
571
|
Serialize torch.Tensor values via the nested ndarray envelope. CPU-only by
|
|
585
572
|
default; device/copy behavior is explicit. force_json_markers is threaded
|
|
586
573
|
into the nested ndarray serialization so Pyodide gets a JSON ndarray value.
|
|
574
|
+
|
|
575
|
+
Rejection order is significant: the categorical rejections (sparse / quantized
|
|
576
|
+
/ meta / complex) are checked BEFORE the device/contiguous opt-in branch so
|
|
577
|
+
they fail with a clear, specific message and are NOT bypassable by
|
|
578
|
+
TYWRAP_TORCH_ALLOW_COPY. The opt-in only governs the lossy-but-lossless device
|
|
579
|
+
transfer and contiguous copy, never an unrepresentable layout/dtype.
|
|
587
580
|
"""
|
|
581
|
+
import torch # already importable: is_torch_tensor() gated the dispatch
|
|
582
|
+
|
|
588
583
|
tensor = obj.detach()
|
|
584
|
+
|
|
585
|
+
# Sparse tensors (COO/CSR/CSC/BSR/BSC -> any non-strided layout) have no dense
|
|
586
|
+
# numpy representation without a densify step, which is not the round-trip this
|
|
587
|
+
# envelope promises. Reject explicitly rather than emitting a misleading
|
|
588
|
+
# "not contiguous" error or silently densifying.
|
|
589
|
+
layout = getattr(tensor, 'layout', None)
|
|
590
|
+
if getattr(tensor, 'is_sparse', False) or (
|
|
591
|
+
layout is not None and layout != torch.strided
|
|
592
|
+
):
|
|
593
|
+
raise RuntimeError(
|
|
594
|
+
f'Torch sparse tensors are not supported (layout={layout}); '
|
|
595
|
+
'convert to a dense CPU tensor explicitly (e.g. tensor.to_dense()) before returning'
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
# Quantized tensors carry a qscheme/scale/zero_point that numpy() cannot
|
|
599
|
+
# represent; .numpy() raises an opaque "unsupported ScalarType" deep in torch.
|
|
600
|
+
# Reject up front with an actionable message.
|
|
601
|
+
if getattr(tensor, 'is_quantized', False):
|
|
602
|
+
raise RuntimeError(
|
|
603
|
+
'Torch quantized tensors are not supported; dequantize explicitly '
|
|
604
|
+
'(e.g. tensor.dequantize()) before returning'
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# Meta tensors have shape/dtype but NO storage; copying to CPU yields garbage,
|
|
608
|
+
# so this is never a lossy-but-honest transfer the opt-in could authorize.
|
|
609
|
+
if getattr(tensor, 'is_meta', False) or (
|
|
610
|
+
getattr(tensor, 'device', None) is not None and tensor.device.type == 'meta'
|
|
611
|
+
):
|
|
612
|
+
raise RuntimeError(
|
|
613
|
+
'Torch meta tensors carry no data and cannot be serialized; '
|
|
614
|
+
'materialize the tensor on a real device before returning'
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Complex tensors round-trip to numpy complex arrays, which are not
|
|
618
|
+
# JSON-serializable and have no codec envelope. Reject explicitly instead of
|
|
619
|
+
# emitting Python complex tuples that the JS decoder cannot parse.
|
|
620
|
+
if torch.is_complex(tensor):
|
|
621
|
+
raise RuntimeError(
|
|
622
|
+
f'Torch complex tensors are not supported (dtype={tensor.dtype}); '
|
|
623
|
+
'split into real/imag components explicitly before returning'
|
|
624
|
+
)
|
|
625
|
+
|
|
589
626
|
if getattr(tensor, 'device', None) is not None and tensor.device.type != 'cpu':
|
|
590
627
|
if not torch_allow_copy:
|
|
591
628
|
raise RuntimeError(
|
|
@@ -622,12 +659,22 @@ def serialize_sklearn_estimator(obj):
|
|
|
622
659
|
raise RuntimeError('scikit-learn is not available') from exc
|
|
623
660
|
|
|
624
661
|
params = obj.get_params(deep=False)
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
662
|
+
|
|
663
|
+
# Metadata-only: NEVER pickle/joblib. Every param value must be plain JSON
|
|
664
|
+
# (no callables, nested estimators, numpy arrays, or other objects). Probe
|
|
665
|
+
# each value individually so the error names the offending param instead of
|
|
666
|
+
# failing opaquely on the whole dict. allow_nan=False also rejects NaN/Inf
|
|
667
|
+
# params here for parity with the response codec.
|
|
668
|
+
for key, value in params.items():
|
|
669
|
+
try:
|
|
670
|
+
json.dumps(value, allow_nan=False)
|
|
671
|
+
except (TypeError, ValueError) as exc:
|
|
672
|
+
raise RuntimeError(
|
|
673
|
+
f'scikit-learn estimator param {key!r} is not JSON-serializable '
|
|
674
|
+
f'(got {type(value).__name__}); estimators are serialized as metadata only '
|
|
675
|
+
'(no pickle/joblib), so every param must be a plain JSON value. '
|
|
676
|
+
'Sanitize or drop the param before returning'
|
|
677
|
+
) from exc
|
|
631
678
|
|
|
632
679
|
return {
|
|
633
680
|
'__tywrap__': 'sklearn.estimator',
|
|
@@ -682,26 +729,45 @@ def serialize_stdlib(obj):
|
|
|
682
729
|
|
|
683
730
|
def serialize(obj, *, force_json_markers, torch_allow_copy=False):
|
|
684
731
|
"""
|
|
685
|
-
Top-level result serializer.
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
732
|
+
Top-level result serializer.
|
|
733
|
+
|
|
734
|
+
Scientific codecs are type-first and only inspect packages that the value can
|
|
735
|
+
belong to. A value from an optional package implies that package is already in
|
|
736
|
+
sys.modules, so these checks never cold-import the scientific stack. The
|
|
737
|
+
package dispatch deliberately precedes the JSON-native fast path: e.g. a
|
|
738
|
+
package-defined subclass of dict still receives its relevant codec check.
|
|
739
|
+
The remaining BridgeCodec value behaviors (numpy/pandas scalars, bytes, sets,
|
|
740
|
+
complex rejection, NaN/Infinity) are applied later during JSON encoding by
|
|
741
|
+
default_encoder.
|
|
690
742
|
"""
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
)
|
|
703
|
-
|
|
743
|
+
package = type(obj).__module__.split('.', 1)[0]
|
|
744
|
+
|
|
745
|
+
if package == 'numpy' and 'numpy' in sys.modules:
|
|
746
|
+
if is_numpy_array(obj):
|
|
747
|
+
return serialize_ndarray(obj, force_json_markers=force_json_markers)
|
|
748
|
+
elif package == 'pandas' and 'pandas' in sys.modules:
|
|
749
|
+
if is_pandas_dataframe(obj):
|
|
750
|
+
return serialize_dataframe(obj, force_json_markers=force_json_markers)
|
|
751
|
+
if is_pandas_series(obj):
|
|
752
|
+
return serialize_series(obj, force_json_markers=force_json_markers)
|
|
753
|
+
elif package == 'scipy' and 'scipy.sparse' in sys.modules:
|
|
754
|
+
if is_scipy_sparse(obj):
|
|
755
|
+
return serialize_sparse_matrix(obj)
|
|
756
|
+
elif package == 'torch' and 'torch' in sys.modules:
|
|
757
|
+
if is_torch_tensor(obj):
|
|
758
|
+
return serialize_torch_tensor(
|
|
759
|
+
obj, force_json_markers=force_json_markers, torch_allow_copy=torch_allow_copy
|
|
760
|
+
)
|
|
761
|
+
elif 'sklearn.base' in sys.modules and is_sklearn_estimator(obj):
|
|
762
|
+
# No package gate here, unlike the branches above: subclassing
|
|
763
|
+
# BaseEstimator is sklearn's documented extension point, so user-defined
|
|
764
|
+
# estimators live outside the 'sklearn' package and must still get the
|
|
765
|
+
# estimator serializer (and its param-naming errors).
|
|
704
766
|
return serialize_sklearn_estimator(obj)
|
|
767
|
+
|
|
768
|
+
if isinstance(obj, (type(None), bool, int, float, str, dict, list, tuple)):
|
|
769
|
+
return obj
|
|
770
|
+
|
|
705
771
|
pydantic_value = serialize_pydantic(obj)
|
|
706
772
|
if pydantic_value is not _NO_PYDANTIC:
|
|
707
773
|
return pydantic_value
|
|
@@ -712,14 +778,12 @@ def serialize(obj, *, force_json_markers, torch_allow_copy=False):
|
|
|
712
778
|
|
|
713
779
|
|
|
714
780
|
# =============================================================================
|
|
715
|
-
# JSON
|
|
781
|
+
# JSON CODEC: value handling and size-limited encode/decode
|
|
716
782
|
# =============================================================================
|
|
717
783
|
#
|
|
718
|
-
# This
|
|
719
|
-
#
|
|
720
|
-
#
|
|
721
|
-
# core encoder exists so the Pyodide server gets identical value handling without
|
|
722
|
-
# depending on safe_codec.py. The conformance suite asserts these behaviors match.
|
|
784
|
+
# This is the single Python implementation used by both the subprocess bridge and
|
|
785
|
+
# the embedded Pyodide core. BridgeCodec adds payload-size enforcement around the
|
|
786
|
+
# shared encoder; encode_value is the unbounded form used by the in-memory bridge.
|
|
723
787
|
|
|
724
788
|
def _is_nan_or_inf(value):
|
|
725
789
|
if not isinstance(value, (int, float)):
|
|
@@ -844,6 +908,56 @@ def encode_value(value, *, allow_nan):
|
|
|
844
908
|
raise CodecError(f'JSON encoding failed: {exc}') from exc
|
|
845
909
|
|
|
846
910
|
|
|
911
|
+
class BridgeCodec:
|
|
912
|
+
"""Safe JSON codec with explicit value handling and payload-size limits."""
|
|
913
|
+
|
|
914
|
+
def __init__(self, allow_nan=False, max_payload_bytes=10 * 1024 * 1024):
|
|
915
|
+
self.allow_nan = allow_nan
|
|
916
|
+
self.max_payload_bytes = max_payload_bytes
|
|
917
|
+
self._encoder = make_default_encoder(allow_nan=allow_nan)
|
|
918
|
+
|
|
919
|
+
def encode(self, value):
|
|
920
|
+
result = encode_value(value, allow_nan=self.allow_nan)
|
|
921
|
+
if len(result.encode('utf-8')) > self.max_payload_bytes:
|
|
922
|
+
raise CodecError(f'Payload exceeds {self.max_payload_bytes} bytes')
|
|
923
|
+
return result
|
|
924
|
+
|
|
925
|
+
def decode(self, payload):
|
|
926
|
+
if len(payload.encode('utf-8')) > self.max_payload_bytes:
|
|
927
|
+
raise CodecError(f'Payload exceeds {self.max_payload_bytes} bytes')
|
|
928
|
+
try:
|
|
929
|
+
return json.loads(payload)
|
|
930
|
+
except json.JSONDecodeError as exc:
|
|
931
|
+
raise CodecError(f'JSON decoding failed: {exc}') from exc
|
|
932
|
+
|
|
933
|
+
def _default_encoder(self, obj):
|
|
934
|
+
"""Compatibility hook for callers that used the codec's JSON encoder."""
|
|
935
|
+
return self._encoder(obj)
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
_default_codec = None
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def get_default_codec():
|
|
942
|
+
"""Return the lazily-created default BridgeCodec instance."""
|
|
943
|
+
global _default_codec
|
|
944
|
+
if _default_codec is None:
|
|
945
|
+
_default_codec = BridgeCodec()
|
|
946
|
+
return _default_codec
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def encode(value, *, allow_nan=False):
|
|
950
|
+
"""Encode a value with the default codec settings."""
|
|
951
|
+
if allow_nan:
|
|
952
|
+
return BridgeCodec(allow_nan=True).encode(value)
|
|
953
|
+
return get_default_codec().encode(value)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def decode(payload):
|
|
957
|
+
"""Decode a JSON payload with the default codec settings."""
|
|
958
|
+
return get_default_codec().decode(payload)
|
|
959
|
+
|
|
960
|
+
|
|
847
961
|
# =============================================================================
|
|
848
962
|
# REQUEST VALIDATION + HANDLERS + DISPATCH
|
|
849
963
|
# =============================================================================
|
|
@@ -883,11 +997,19 @@ def coerce_dict(value, key):
|
|
|
883
997
|
return value
|
|
884
998
|
|
|
885
999
|
|
|
886
|
-
def handle_call(
|
|
1000
|
+
def handle_call(
|
|
1001
|
+
params,
|
|
1002
|
+
*,
|
|
1003
|
+
force_json_markers,
|
|
1004
|
+
torch_allow_copy,
|
|
1005
|
+
allowed_modules,
|
|
1006
|
+
allow_private_attrs,
|
|
1007
|
+
has_envelope_markers,
|
|
1008
|
+
):
|
|
887
1009
|
module_name = require_str(params, 'module')
|
|
888
1010
|
function_name = require_str(params, 'functionName')
|
|
889
|
-
args = deserialize(coerce_list(params.get('args'), 'args'))
|
|
890
|
-
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'))
|
|
1011
|
+
args = deserialize(coerce_list(params.get('args'), 'args'), has_envelope_markers=has_envelope_markers)
|
|
1012
|
+
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'), has_envelope_markers=has_envelope_markers)
|
|
891
1013
|
mod = import_allowed_module(module_name, allowed_modules)
|
|
892
1014
|
# function_name may be dotted ('Class.method') for @classmethod/@staticmethod
|
|
893
1015
|
# calls, which the generated wrapper routes through call() rather than an
|
|
@@ -897,53 +1019,14 @@ def handle_call(params, *, force_json_markers, torch_allow_copy, allowed_modules
|
|
|
897
1019
|
return serialize(res, force_json_markers=force_json_markers, torch_allow_copy=torch_allow_copy)
|
|
898
1020
|
|
|
899
1021
|
|
|
900
|
-
def
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
handle_id = str(id(obj))
|
|
909
|
-
instances[handle_id] = obj
|
|
910
|
-
return handle_id
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
def handle_call_method(params, instances, *, force_json_markers, torch_allow_copy, allow_private_attrs):
|
|
914
|
-
handle_id = require_str(params, 'handle')
|
|
915
|
-
method_name = require_str(params, 'methodName')
|
|
916
|
-
args = deserialize(coerce_list(params.get('args'), 'args'))
|
|
917
|
-
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'))
|
|
918
|
-
if handle_id not in instances:
|
|
919
|
-
raise InstanceHandleError(f'Unknown instance handle: {handle_id}')
|
|
920
|
-
obj = instances[handle_id]
|
|
921
|
-
# A @property / functools.cached_property is read, not called: the generated
|
|
922
|
-
# `get prop()` accessor emits callMethod(handle, name, []). Classify before
|
|
923
|
-
# touching the value (so cached_property is detected on its first read) and
|
|
924
|
-
# return the attribute directly; everything else is a bound method to call.
|
|
925
|
-
if is_accessor_attr(obj, method_name):
|
|
926
|
-
# An accessor is read, never called: a generated `get prop()` always
|
|
927
|
-
# sends empty args. Reject a malformed request that supplies any so it
|
|
928
|
-
# fails loudly instead of silently dropping the arguments.
|
|
929
|
-
if args or kwargs:
|
|
930
|
-
raise ProtocolError(f'Accessor {method_name!r} does not accept arguments')
|
|
931
|
-
res = get_allowed_attr(obj, method_name, allow_private_attrs=allow_private_attrs)
|
|
932
|
-
else:
|
|
933
|
-
func = get_allowed_attr(obj, method_name, allow_private_attrs=allow_private_attrs)
|
|
934
|
-
res = func(*args, **kwargs)
|
|
935
|
-
return serialize(res, force_json_markers=force_json_markers, torch_allow_copy=torch_allow_copy)
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
def handle_dispose_instance(params, instances):
|
|
939
|
-
handle_id = require_str(params, 'handle')
|
|
940
|
-
if handle_id not in instances:
|
|
941
|
-
return False
|
|
942
|
-
del instances[handle_id]
|
|
943
|
-
return True
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
def build_meta(instances, *, bridge, pid, python_version, codec_fallback, arrow_available_override=None):
|
|
1022
|
+
def build_meta(
|
|
1023
|
+
*,
|
|
1024
|
+
bridge,
|
|
1025
|
+
pid,
|
|
1026
|
+
python_version,
|
|
1027
|
+
codec_fallback,
|
|
1028
|
+
arrow_available_override=None,
|
|
1029
|
+
):
|
|
947
1030
|
"""
|
|
948
1031
|
Build the bridge metadata payload.
|
|
949
1032
|
|
|
@@ -958,7 +1041,7 @@ def build_meta(instances, *, bridge, pid, python_version, codec_fallback, arrow_
|
|
|
958
1041
|
pyarrow happens to be importable in the WASM environment.
|
|
959
1042
|
"""
|
|
960
1043
|
arrow = arrow_available() if arrow_available_override is None else arrow_available_override
|
|
961
|
-
|
|
1044
|
+
meta = {
|
|
962
1045
|
'protocol': PROTOCOL,
|
|
963
1046
|
'protocolVersion': PROTOCOL_VERSION,
|
|
964
1047
|
'bridge': bridge,
|
|
@@ -969,13 +1052,13 @@ def build_meta(instances, *, bridge, pid, python_version, codec_fallback, arrow_
|
|
|
969
1052
|
'scipyAvailable': module_available('scipy'),
|
|
970
1053
|
'torchAvailable': module_available('torch'),
|
|
971
1054
|
'sklearnAvailable': module_available('sklearn'),
|
|
972
|
-
'instances':
|
|
1055
|
+
'instances': 0,
|
|
973
1056
|
}
|
|
1057
|
+
return meta
|
|
974
1058
|
|
|
975
1059
|
|
|
976
1060
|
def dispatch_request(
|
|
977
1061
|
msg,
|
|
978
|
-
instances,
|
|
979
1062
|
*,
|
|
980
1063
|
bridge,
|
|
981
1064
|
pid,
|
|
@@ -986,6 +1069,7 @@ def dispatch_request(
|
|
|
986
1069
|
arrow_available_override=None,
|
|
987
1070
|
allowed_modules=None,
|
|
988
1071
|
allow_private_attrs=False,
|
|
1072
|
+
has_envelope_markers=True,
|
|
989
1073
|
):
|
|
990
1074
|
"""
|
|
991
1075
|
Validate and route a request, returning the fully-serialized response dict
|
|
@@ -997,7 +1081,7 @@ def dispatch_request(
|
|
|
997
1081
|
the final encode_value() call, which the caller performs.
|
|
998
1082
|
|
|
999
1083
|
allowed_modules: None (default) disables the import allowlist so existing
|
|
1000
|
-
behavior is preserved. Supplying a set restricts call
|
|
1084
|
+
behavior is preserved. Supplying a set restricts call imports to
|
|
1001
1085
|
those modules (plus the stdlib the bridge itself needs) and raises
|
|
1002
1086
|
ImportNotAllowedError otherwise. allow_private_attrs=False (default) blocks
|
|
1003
1087
|
getattr of underscore-prefixed names; True restores unrestricted access. See
|
|
@@ -1015,28 +1099,14 @@ def dispatch_request(
|
|
|
1015
1099
|
torch_allow_copy=torch_allow_copy,
|
|
1016
1100
|
allowed_modules=allowed_modules,
|
|
1017
1101
|
allow_private_attrs=allow_private_attrs,
|
|
1102
|
+
has_envelope_markers=has_envelope_markers,
|
|
1018
1103
|
)
|
|
1019
|
-
elif method == 'instantiate':
|
|
1020
|
-
result = handle_instantiate(
|
|
1021
|
-
params, instances, allowed_modules=allowed_modules, allow_private_attrs=allow_private_attrs
|
|
1022
|
-
)
|
|
1023
|
-
elif method == 'call_method':
|
|
1024
|
-
result = handle_call_method(
|
|
1025
|
-
params,
|
|
1026
|
-
instances,
|
|
1027
|
-
force_json_markers=force_json_markers,
|
|
1028
|
-
torch_allow_copy=torch_allow_copy,
|
|
1029
|
-
allow_private_attrs=allow_private_attrs,
|
|
1030
|
-
)
|
|
1031
|
-
elif method == 'dispose_instance':
|
|
1032
|
-
result = handle_dispose_instance(params, instances)
|
|
1033
1104
|
elif method == 'meta':
|
|
1034
1105
|
if python_version is None:
|
|
1035
1106
|
import sys
|
|
1036
1107
|
python_version = sys.version.split()[0]
|
|
1037
1108
|
codec_fallback = 'json' if force_json_markers else 'none'
|
|
1038
1109
|
result = build_meta(
|
|
1039
|
-
instances,
|
|
1040
1110
|
bridge=bridge,
|
|
1041
1111
|
pid=pid,
|
|
1042
1112
|
python_version=python_version,
|
package/src/config/index.ts
CHANGED
|
@@ -118,6 +118,7 @@ function detectLegacyFields(config: TywrapConfig): void {
|
|
|
118
118
|
const ALLOWED_TOP_LEVEL = new Set([
|
|
119
119
|
'pythonModules',
|
|
120
120
|
'pythonImportPath',
|
|
121
|
+
'contractInput',
|
|
121
122
|
'output',
|
|
122
123
|
'runtime',
|
|
123
124
|
'performance',
|
|
@@ -129,6 +130,7 @@ const VALID_OUTPUT_FORMATS = ['esm', 'cjs', 'both'];
|
|
|
129
130
|
const VALID_COMPRESSION = ['auto', 'gzip', 'brotli', 'none'];
|
|
130
131
|
const VALID_TYPE_HINTS = ['strict', 'loose', 'ignore'];
|
|
131
132
|
const VALID_TYPE_PRESETS = new Set([
|
|
133
|
+
// Accepted as an explicit no-op in 0.9; ndarray shape typing depends on #268.
|
|
132
134
|
'numpy',
|
|
133
135
|
'pandas',
|
|
134
136
|
'pydantic',
|
|
@@ -276,6 +278,15 @@ function validateConfig(config: ResolvedTywrapConfig): void {
|
|
|
276
278
|
throw new Error('pythonImportPath must be an array of strings');
|
|
277
279
|
}
|
|
278
280
|
|
|
281
|
+
if (
|
|
282
|
+
config.contractInput !== undefined &&
|
|
283
|
+
typeof config.contractInput !== 'string' &&
|
|
284
|
+
(!isPlainObject(config.contractInput) ||
|
|
285
|
+
!Object.values(config.contractInput).every(value => typeof value === 'string'))
|
|
286
|
+
) {
|
|
287
|
+
throw new Error('contractInput must be a path string or a record of module paths');
|
|
288
|
+
}
|
|
289
|
+
|
|
279
290
|
validateOutput(config.output);
|
|
280
291
|
validateRuntime(config.runtime);
|
|
281
292
|
validatePerformance(config.performance);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PythonGenericParameter, PythonType } from '../types/index.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface AnnotationParserOptions {
|
|
4
4
|
onUnknownTypeName?: (name: string) => void;
|
|
5
5
|
knownTypeVarNames?: Iterable<string>;
|
|
6
6
|
typeParameters?: readonly PythonGenericParameter[];
|
|
@@ -253,7 +253,8 @@ export function parseAnnotationToPythonType(
|
|
|
253
253
|
// Returns the content between the first `[` and the last `]` of a special-form
|
|
254
254
|
// annotation (e.g. the `int, str` of `Union[int, str]`). Callers gate this on a
|
|
255
255
|
// prefix check, so the brackets are known to be present.
|
|
256
|
-
const bracketInner = (raw: string): string =>
|
|
256
|
+
const bracketInner = (raw: string): string =>
|
|
257
|
+
raw.slice(raw.indexOf('[') + 1, raw.lastIndexOf(']'));
|
|
257
258
|
|
|
258
259
|
// True when `raw` opens with one of the supported module prefixes followed by
|
|
259
260
|
// `name[`. Mirrors the inlined `raw.startsWith('typing.Name[') || ...` checks so
|
|
@@ -406,8 +407,7 @@ export function parseAnnotationToPythonType(
|
|
|
406
407
|
const builtInClassMatch = raw.match(/^<class ['"][^'"]+['"]>$/);
|
|
407
408
|
if (builtInClassMatch) {
|
|
408
409
|
const inner = (raw.match(/^<class ['"]([^'"]+)['"]>$/) ?? [])[1] ?? '';
|
|
409
|
-
|
|
410
|
-
return mapSimpleName(name);
|
|
410
|
+
return mapSimpleName(inner);
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
if (raw.includes('|')) {
|
|
@@ -505,7 +505,11 @@ export function parseAnnotationToPythonType(
|
|
|
505
505
|
};
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
-
|
|
508
|
+
const qualified = splitQualifiedName(raw);
|
|
509
|
+
if (qualified.module) {
|
|
510
|
+
return { kind: 'custom', name: qualified.name, module: qualified.module };
|
|
511
|
+
}
|
|
512
|
+
return mapSimpleName(qualified.name);
|
|
509
513
|
};
|
|
510
514
|
|
|
511
515
|
return parse(annotation, 0);
|
package/src/core/emit-call.ts
CHANGED
|
@@ -132,13 +132,7 @@ export function emitCallPrelude(desc: CallDescriptor, helpers: CallEmitHelpers):
|
|
|
132
132
|
* keywords and enforce required keyword-only arguments.
|
|
133
133
|
*/
|
|
134
134
|
export function emitArgGuards(desc: CallDescriptor): string[] {
|
|
135
|
-
const {
|
|
136
|
-
hasKwArgs,
|
|
137
|
-
positionalOnlyNames,
|
|
138
|
-
requiredKwOnlyNames,
|
|
139
|
-
indent: i,
|
|
140
|
-
errorLabel,
|
|
141
|
-
} = desc;
|
|
135
|
+
const { hasKwArgs, positionalOnlyNames, requiredKwOnlyNames, indent: i, errorLabel } = desc;
|
|
142
136
|
const i2 = `${i} `;
|
|
143
137
|
const i3 = `${i2} `;
|
|
144
138
|
|