Skip to content

Commit 2acf270

Browse files
authored
Merge pull request #295 from bbopen/feat/0.9-remove-instance-api
feat!: remove the stateful instance API — server, client/protocol, generator
2 parents 6801836 + 1b64a2a commit 2acf270

30 files changed

Lines changed: 195 additions & 994 deletions

docs/examples/index.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,15 @@ import { sin, pi } from './generated/math.generated.js';
5757
console.log(await sin(pi / 4));
5858
```
5959

60-
## Generated Classes
60+
## Value-returning APIs
6161

62-
Generated classes have an async `create(...)` constructor and an explicit `disposeHandle()`:
62+
v0.9 generated wrappers do not keep live Python class instances. Expose an
63+
operation as a value-returning module function instead:
6364

6465
```ts
65-
import { Counter } from './generated/collections.generated.js';
66+
import { median } from './generated/statistics.generated.js';
6667

67-
const counter = await Counter.create([1, 2, 2]);
68-
console.log(await counter.mostCommon(1));
69-
await counter.disposeHandle();
68+
console.log(await median([1, 2, 2])); // 2
7069
```
7170

7271
## More Docs

docs/guide/getting-started.md

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -138,19 +138,13 @@ def calculate_area(width: float, height: float) -> float:
138138
"""Calculate rectangular area."""
139139
return width * height
140140

141-
class Calculator:
142-
"""Simple calculator class."""
141+
def add(a: float, b: float, precision: int = 2) -> float:
142+
"""Add two numbers."""
143+
return round(a + b, precision)
143144

144-
def __init__(self, precision: int = 2):
145-
self.precision = precision
146-
147-
def add(self, a: float, b: float) -> float:
148-
"""Add two numbers."""
149-
return round(a + b, self.precision)
150-
151-
def multiply(self, a: float, b: float) -> float:
152-
"""Multiply two numbers."""
153-
return round(a * b, self.precision)
145+
def multiply(a: float, b: float, precision: int = 2) -> float:
146+
"""Multiply two numbers."""
147+
return round(a * b, precision)
154148
```
155149

156150
### Configuration
@@ -199,7 +193,8 @@ import { setRuntimeBridge } from 'tywrap/runtime';
199193
import {
200194
greet,
201195
calculate_area,
202-
Calculator,
196+
add,
197+
multiply,
203198
} from './generated/my_utils.generated.js';
204199

205200
const bridge = new NodeBridge({ pythonPath: 'python3' });
@@ -213,14 +208,12 @@ async function demo() {
213208
const area = await calculate_area(10.5, 8.2);
214209
console.log(`Area: ${area}`); // Area: 86.1
215210

216-
// Class instantiation and methods
217-
const calc = await Calculator.create(3);
218-
const sum = await calc.add(3.14159, 2.71828);
219-
const product = await calc.multiply(sum, 2);
220-
await calc.disposeHandle();
211+
// Use value-returning module functions instead of live class handles.
212+
const sum = await add(3.14159, 2.71828, 3);
213+
const product = await multiply(sum, 2, 3);
221214

222-
console.log(`Sum: ${sum}`); // Sum: 5.860
223-
console.log(`Product: ${product}`); // Product: 11.720
215+
console.log(`Sum: ${sum}`); // Sum: 5.86
216+
console.log(`Product: ${product}`); // Product: 11.72
224217
}
225218
```
226219

docs/public/llms-full.txt

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -197,19 +197,13 @@ def calculate_area(width: float, height: float) -> float:
197197
"""Calculate rectangular area."""
198198
return width * height
199199

200-
class Calculator:
201-
"""Simple calculator class."""
200+
def add(a: float, b: float, precision: int = 2) -> float:
201+
"""Add two numbers."""
202+
return round(a + b, precision)
202203

203-
def __init__(self, precision: int = 2):
204-
self.precision = precision
205-
206-
def add(self, a: float, b: float) -> float:
207-
"""Add two numbers."""
208-
return round(a + b, self.precision)
209-
210-
def multiply(self, a: float, b: float) -> float:
211-
"""Multiply two numbers."""
212-
return round(a * b, self.precision)
204+
def multiply(a: float, b: float, precision: int = 2) -> float:
205+
"""Multiply two numbers."""
206+
return round(a * b, precision)
213207
```
214208

215209
### Configuration
@@ -258,7 +252,8 @@ import { setRuntimeBridge } from 'tywrap/runtime';
258252
import {
259253
greet,
260254
calculate_area,
261-
Calculator,
255+
add,
256+
multiply,
262257
} from './generated/my_utils.generated.js';
263258

264259
const bridge = new NodeBridge({ pythonPath: 'python3' });
@@ -272,14 +267,12 @@ async function demo() {
272267
const area = await calculate_area(10.5, 8.2);
273268
console.log(`Area: ${area}`); // Area: 86.1
274269

275-
// Class instantiation and methods
276-
const calc = await Calculator.create(3);
277-
const sum = await calc.add(3.14159, 2.71828);
278-
const product = await calc.multiply(sum, 2);
279-
await calc.disposeHandle();
270+
// Use value-returning module functions instead of live class handles.
271+
const sum = await add(3.14159, 2.71828, 3);
272+
const product = await multiply(sum, 2, 3);
280273

281-
console.log(`Sum: ${sum}`); // Sum: 5.860
282-
console.log(`Product: ${product}`); // Product: 11.720
274+
console.log(`Sum: ${sum}`); // Sum: 5.86
275+
console.log(`Product: ${product}`); // Product: 11.72
283276
}
284277
```
285278

@@ -2671,7 +2664,7 @@ Generates TypeScript shaped like:
26712664
export type Pair<T> = [T, T];
26722665
export type Transform<P extends unknown[], T> = (...args: P) => T;
26732666
export class Container<T> {
2674-
get(): Promise<T>;
2667+
// NOTE: Instance members are not generated in v0.9; migrate this API to value-returning module functions.
26752668
}
26762669
```
26772670

@@ -3464,16 +3457,15 @@ import { sin, pi } from './generated/math.generated.js';
34643457
console.log(await sin(pi / 4));
34653458
```
34663459

3467-
## Generated Classes
3460+
## Value-returning APIs
34683461

3469-
Generated classes have an async `create(...)` constructor and an explicit `disposeHandle()`:
3462+
v0.9 generated wrappers do not keep live Python class instances. Expose an operation as a
3463+
value-returning module function instead:
34703464

34713465
```ts
3472-
import { Counter } from './generated/collections.generated.js';
3466+
import { median } from './generated/statistics.generated.js';
34733467

3474-
const counter = await Counter.create([1, 2, 2]);
3475-
console.log(await counter.mostCommon(1));
3476-
await counter.disposeHandle();
3468+
console.log(await median([1, 2, 2])); // 2
34773469
```
34783470

34793471
## More Docs

docs/reference/type-mapping.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ Generates TypeScript shaped like:
183183
export type Pair<T> = [T, T];
184184
export type Transform<P extends unknown[], T> = (...args: P) => T;
185185
export class Container<T> {
186-
get(): Promise<T>;
186+
// NOTE: Instance members are not generated in v0.9; migrate this API to value-returning module functions.
187187
}
188188
```
189189

runtime/python_bridge.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
the explicit byte-size limit error message)
2020
2121
SECURITY / TRUST MODEL: the bridge imports the requested module and getattrs the
22-
requested function/class/method, so call/instantiate/call_method are an arbitrary
22+
requested function/class method, so call is an arbitrary
2323
import+getattr+call surface. Two guards (implemented in tywrap_bridge_core) bound
2424
that surface:
2525
* TYWRAP_ALLOWED_MODULES (comma/space separated): when set, only those modules
@@ -54,7 +54,6 @@
5454
PROTOCOL_VERSION,
5555
CODEC_VERSION,
5656
ProtocolError,
57-
InstanceHandleError,
5857
ImportNotAllowedError,
5958
AttributeNotAllowedError,
6059
import_allowed_module,
@@ -95,8 +94,6 @@
9594
except (OSError, ValueError):
9695
pass
9796

98-
instances = {}
99-
10097
FALLBACK_JSON = os.environ.get('TYWRAP_CODEC_FALLBACK', '').lower() == 'json'
10198
TORCH_ALLOW_COPY = os.environ.get('TYWRAP_TORCH_ALLOW_COPY', '').lower() in ('1', 'true', 'yes')
10299
BRIDGE_NAME = 'python-subprocess'
@@ -334,7 +331,6 @@ def handle_meta():
334331
real pid and bridge='python-subprocess'.
335332
"""
336333
return core.build_meta(
337-
instances,
338334
bridge=BRIDGE_NAME,
339335
pid=os.getpid(),
340336
python_version=sys.version.split()[0],
@@ -353,7 +349,6 @@ def dispatch_request(msg, *, has_envelope_markers=True):
353349
"""
354350
out = core.dispatch_request(
355351
msg,
356-
instances,
357352
bridge=BRIDGE_NAME,
358353
pid=os.getpid(),
359354
force_json_markers=FALLBACK_JSON,

runtime/tywrap_bridge_core.py

Lines changed: 4 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,8 @@
4343
import base64
4444
import datetime as dt
4545
import decimal
46-
import functools
4746
import importlib
4847
import importlib.util
49-
import inspect
5048
import json
5149
import math
5250
import sys
@@ -65,10 +63,6 @@ class ProtocolError(Exception):
6563
"""Raised for malformed requests (bad protocol/id/method/params)."""
6664

6765

68-
class InstanceHandleError(ValueError):
69-
"""Raised when an instance handle is unknown or no longer valid."""
70-
71-
7266
class ImportNotAllowedError(PermissionError):
7367
"""Raised when a requested module import is not on the active allowlist."""
7468

@@ -97,8 +91,8 @@ def __init__(self, attr_name):
9791
# IMPORT / ATTRIBUTE ALLOWLIST (trust boundary enforcement)
9892
# =============================================================================
9993
#
100-
# The bridge dispatches call/instantiate/call_method by importing the requested
101-
# module and getattr-ing the requested function/class/method. That is an
94+
# The bridge dispatches call requests by importing the requested module and
95+
# getattr-ing the requested function/class method. That is an
10296
# arbitrary import+getattr+call surface, so two complementary guards live here.
10397
# Both are PURE (no env reads) so the rules behave identically under the
10498
# subprocess server and the in-WASM Pyodide server; the subprocess server derives
@@ -205,22 +199,6 @@ def resolve_allowed_attr_path(root, dotted_name, *, allow_private_attrs):
205199
return obj
206200

207201

208-
def is_accessor_attr(obj, attr_name):
209-
"""
210-
True when attr_name resolves to a @property or functools.cached_property on
211-
obj's type — i.e. it is read by attribute access, not called.
212-
213-
Inspects type(obj)'s MRO via getattr_static (which never triggers the
214-
descriptor protocol), NOT the instance dict. That matters for
215-
cached_property: after the first read it stores its value in the instance
216-
__dict__, so an instance-level static lookup would return the cached value
217-
rather than the descriptor and misclassify it as a method on the next read.
218-
Reading from the type keeps the classification stable across repeated reads.
219-
"""
220-
descriptor = inspect.getattr_static(type(obj), attr_name, None)
221-
return isinstance(descriptor, (property, functools.cached_property))
222-
223-
224202
class CodecError(Exception):
225203
"""Raised when value encoding fails (e.g. NaN/Infinity not allowed)."""
226204

@@ -990,64 +968,7 @@ def handle_call(
990968
return serialize(res, force_json_markers=force_json_markers, torch_allow_copy=torch_allow_copy)
991969

992970

993-
def handle_instantiate(
994-
params, instances, *, allowed_modules, allow_private_attrs, has_envelope_markers
995-
):
996-
module_name = require_str(params, 'module')
997-
class_name = require_str(params, 'className')
998-
args = deserialize(coerce_list(params.get('args'), 'args'), has_envelope_markers=has_envelope_markers)
999-
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'), has_envelope_markers=has_envelope_markers)
1000-
mod = import_allowed_module(module_name, allowed_modules)
1001-
cls = get_allowed_attr(mod, class_name, allow_private_attrs=allow_private_attrs)
1002-
obj = cls(*args, **kwargs)
1003-
handle_id = str(id(obj))
1004-
instances[handle_id] = obj
1005-
return handle_id
1006-
1007-
1008-
def handle_call_method(
1009-
params,
1010-
instances,
1011-
*,
1012-
force_json_markers,
1013-
torch_allow_copy,
1014-
allow_private_attrs,
1015-
has_envelope_markers,
1016-
):
1017-
handle_id = require_str(params, 'handle')
1018-
method_name = require_str(params, 'methodName')
1019-
args = deserialize(coerce_list(params.get('args'), 'args'), has_envelope_markers=has_envelope_markers)
1020-
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'), has_envelope_markers=has_envelope_markers)
1021-
if handle_id not in instances:
1022-
raise InstanceHandleError(f'Unknown instance handle: {handle_id}')
1023-
obj = instances[handle_id]
1024-
# A @property / functools.cached_property is read, not called: the generated
1025-
# `get prop()` accessor emits callMethod(handle, name, []). Classify before
1026-
# touching the value (so cached_property is detected on its first read) and
1027-
# return the attribute directly; everything else is a bound method to call.
1028-
if is_accessor_attr(obj, method_name):
1029-
# An accessor is read, never called: a generated `get prop()` always
1030-
# sends empty args. Reject a malformed request that supplies any so it
1031-
# fails loudly instead of silently dropping the arguments.
1032-
if args or kwargs:
1033-
raise ProtocolError(f'Accessor {method_name!r} does not accept arguments')
1034-
res = get_allowed_attr(obj, method_name, allow_private_attrs=allow_private_attrs)
1035-
else:
1036-
func = get_allowed_attr(obj, method_name, allow_private_attrs=allow_private_attrs)
1037-
res = func(*args, **kwargs)
1038-
return serialize(res, force_json_markers=force_json_markers, torch_allow_copy=torch_allow_copy)
1039-
1040-
1041-
def handle_dispose_instance(params, instances):
1042-
handle_id = require_str(params, 'handle')
1043-
if handle_id not in instances:
1044-
return False
1045-
del instances[handle_id]
1046-
return True
1047-
1048-
1049971
def build_meta(
1050-
instances,
1051972
*,
1052973
bridge,
1053974
pid,
@@ -1088,7 +1009,7 @@ def build_meta(
10881009
'scipyAvailable': module_available('scipy'),
10891010
'torchAvailable': module_available('torch'),
10901011
'sklearnAvailable': module_available('sklearn'),
1091-
'instances': len(instances),
1012+
'instances': 0,
10921013
}
10931014
if transport_info is not None:
10941015
meta['transport'] = transport_info
@@ -1097,7 +1018,6 @@ def build_meta(
10971018

10981019
def dispatch_request(
10991020
msg,
1100-
instances,
11011021
*,
11021022
bridge,
11031023
pid,
@@ -1121,7 +1041,7 @@ def dispatch_request(
11211041
the final encode_value() call, which the caller performs.
11221042
11231043
allowed_modules: None (default) disables the import allowlist so existing
1124-
behavior is preserved. Supplying a set restricts call/instantiate imports to
1044+
behavior is preserved. Supplying a set restricts call imports to
11251045
those modules (plus the stdlib the bridge itself needs) and raises
11261046
ImportNotAllowedError otherwise. allow_private_attrs=False (default) blocks
11271047
getattr of underscore-prefixed names; True restores unrestricted access. See
@@ -1141,32 +1061,12 @@ def dispatch_request(
11411061
allow_private_attrs=allow_private_attrs,
11421062
has_envelope_markers=has_envelope_markers,
11431063
)
1144-
elif method == 'instantiate':
1145-
result = handle_instantiate(
1146-
params,
1147-
instances,
1148-
allowed_modules=allowed_modules,
1149-
allow_private_attrs=allow_private_attrs,
1150-
has_envelope_markers=has_envelope_markers,
1151-
)
1152-
elif method == 'call_method':
1153-
result = handle_call_method(
1154-
params,
1155-
instances,
1156-
force_json_markers=force_json_markers,
1157-
torch_allow_copy=torch_allow_copy,
1158-
allow_private_attrs=allow_private_attrs,
1159-
has_envelope_markers=has_envelope_markers,
1160-
)
1161-
elif method == 'dispose_instance':
1162-
result = handle_dispose_instance(params, instances)
11631064
elif method == 'meta':
11641065
if python_version is None:
11651066
import sys
11661067
python_version = sys.version.split()[0]
11671068
codec_fallback = 'json' if force_json_markers else 'none'
11681069
result = build_meta(
1169-
instances,
11701070
bridge=bridge,
11711071
pid=pid,
11721072
python_version=python_version,

0 commit comments

Comments
 (0)