Skip to content

Commit 566ba2d

Browse files
author
brett-bonner_infodesk
committed
feat(generator)!: emit call-only class wrappers — no instance handles
1 parent 9c6937d commit 566ba2d

10 files changed

Lines changed: 109 additions & 429 deletions

File tree

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 operation as a
63+
value-returning module function instead:
6364

6465
```ts
65-
import { Counter } from './generated/collections.generated.js';
66+
import { most_common } from './generated/collections.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 most_common([1, 2, 2], 1));
7069
```
7170

7271
## More Docs

docs/guide/getting-started.md

Lines changed: 11 additions & 18 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,11 +208,9 @@ 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

222215
console.log(`Sum: ${sum}`); // Sum: 5.860
223216
console.log(`Product: ${product}`); // Product: 11.720

docs/public/llms-full.txt

Lines changed: 17 additions & 25 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,11 +267,9 @@ 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

281274
console.log(`Sum: ${sum}`); // Sum: 5.860
282275
console.log(`Product: ${product}`); // Product: 11.720
@@ -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 { most_common } from './generated/collections.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 most_common([1, 2, 2], 1));
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

src/core/generator.ts

Lines changed: 23 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -570,10 +570,6 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
570570
);
571571
const classTypeParamDecl = classGenericContext.declaration;
572572
const cname = this.escapeIdentifier(cls.name);
573-
const classSelfType = `${cname}${classGenericContext.typeArguments}`;
574-
const tsValueType = (p: (typeof cls.methods)[number]['parameters'][number]): string =>
575-
this.typeToTsFromPython(p.type, classGenericContext, 'value');
576-
577573
const wrapAlias = (body: string): GeneratedCode => {
578574
const ts = `${jsdoc}export type ${cname}${classTypeParamDecl} = ${body}\n`;
579575
return this.wrap(ts, ts, [cls.name]);
@@ -659,14 +655,15 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
659655
const methodDeclarations: string[] = [];
660656

661657
sortedMethods
662-
.filter(method => method.name !== '__init__')
658+
.filter(
659+
method =>
660+
method.name !== '__init__' &&
661+
(method.methodKind === 'class' || method.methodKind === 'static')
662+
)
663663
.forEach(method => {
664-
// @classmethod and @staticmethod become `static` members invoked through
665-
// the class (`Class.method`), not the instance handle. Instance methods
666-
// (the default, or an absent methodKind) keep the existing emission so
667-
// their output stays byte-identical.
668-
const isStatic = method.methodKind === 'class' || method.methodKind === 'static';
669-
const staticPrefix = isStatic ? 'static ' : '';
664+
// v0.9 wrappers expose only class/static methods. They route through
665+
// the ordinary module call path and never retain process-local state.
666+
const staticPrefix = 'static ';
670667
const fparams = method.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
671668
const methodOwnGenericContext = this.buildGenericRenderContext(
672669
this.getTypeParameters(method.typeParameters),
@@ -784,13 +781,9 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
784781
const guardLines = emitArgGuards(callDescriptor);
785782
const guards = guardLines.length > 0 ? `${guardLines.join('\n')}\n` : '';
786783

787-
const callExpr = isStatic
788-
? `getRuntimeBridge().call('${moduleId}', '${cls.name}.${method.name}', __args${
789-
needsKwargsParam ? ', __kwargs' : ''
790-
})`
791-
: `getRuntimeBridge().callMethod(this.__handle, '${method.name}', __args${
792-
needsKwargsParam ? ', __kwargs' : ''
793-
})`;
784+
const callExpr = `getRuntimeBridge().call('${moduleId}', '${cls.name}.${method.name}', __args${
785+
needsKwargsParam ? ', __kwargs' : ''
786+
})`;
794787
methodBodies.push(`${overloadDecl} ${staticPrefix}async ${mname}${methodTypeParamDecl}(${paramsDecl}): Promise<${returnType}> {
795788
${callPrelude}${guards} return ${callExpr};
796789
}`);
@@ -799,167 +792,26 @@ ${callPrelude}${guards} return ${callExpr};
799792
);
800793
});
801794

802-
const init = cls.methods.find(m => m.name === '__init__');
803-
const ctorSpec = (() => {
804-
if (!init) {
805-
return {
806-
overloadDecl: '',
807-
declaration: ` static create${classTypeParamDecl}(...args: unknown[]): Promise<${classSelfType}>;\n`,
808-
paramsDecl: `...args: unknown[]`,
809-
callPrelude: ` const __args: unknown[] = [...args];\n`,
810-
hasKwargs: false,
811-
guardLines: [] as string[],
812-
};
813-
}
814-
815-
const fparams = init.parameters.filter(p => p.name !== 'self' && p.name !== 'cls');
816-
const keywordOnlyParams = fparams.filter(p => p.keywordOnly);
817-
const positionalOnlyNames = fparams.filter(p => p.positionalOnly).map(p => p.name);
818-
const hasVarKwArgs = fparams.some(p => p.kwArgs);
819-
const needsKwargsParam = keywordOnlyParams.length > 0 || hasVarKwArgs;
820-
const varArgsParam = fparams.find(p => p.varArgs);
821-
const needsVarArgsArray = Boolean(varArgsParam) && needsKwargsParam;
822-
const positionalParams = fparams.filter(p => !p.keywordOnly && !p.varArgs && !p.kwArgs);
823-
const firstOptionalPosIndex = positionalParams.findIndex(p => p.optional);
824-
const requiredPosCount =
825-
firstOptionalPosIndex >= 0 ? firstOptionalPosIndex : positionalParams.length;
826-
const keywordOnlyNames = keywordOnlyParams.map(p => p.name);
827-
828-
const renderPositionalParam = (
829-
p: (typeof positionalParams)[number],
830-
forceRequired = false
831-
): string => {
832-
const pname = this.escapeIdentifier(p.name);
833-
const opt = !forceRequired && p.optional ? '?' : '';
834-
return `${pname}${opt}: ${tsValueType(p)}`;
835-
};
836-
837-
const kwargsType = (() => {
838-
if (!needsKwargsParam) {
839-
return '';
840-
}
841-
if (keywordOnlyParams.length === 0 && hasVarKwArgs) {
842-
return 'Record<string, unknown>';
843-
}
844-
const props = keywordOnlyParams
845-
.map(p => `${JSON.stringify(p.name)}${p.optional ? '?' : ''}: ${tsValueType(p)};`)
846-
.join(' ');
847-
const obj = `{ ${props} }`;
848-
return hasVarKwArgs ? `(${obj} & Record<string, unknown>)` : obj;
849-
})();
850-
851-
const paramsDeclParts: string[] = [];
852-
positionalParams.forEach(p => {
853-
paramsDeclParts.push(renderPositionalParam(p));
854-
});
855-
if (varArgsParam) {
856-
const vname = this.escapeIdentifier(varArgsParam.name);
857-
paramsDeclParts.push(needsVarArgsArray ? `${vname}?: unknown[]` : `...${vname}: unknown[]`);
858-
}
859-
if (needsKwargsParam) {
860-
paramsDeclParts.push(`kwargs?: ${kwargsType}`);
861-
}
862-
const paramsDecl = paramsDeclParts.join(', ');
863-
864-
const requiredKwOnlyNames = keywordOnlyParams.filter(p => !p.optional).map(p => p.name);
865-
const overloads: string[] = [];
866-
if (needsKwargsParam && requiredKwOnlyNames.length > 0) {
867-
const firstOptionalIndex = positionalParams.findIndex(p => p.optional);
868-
const requiredPosCount =
869-
firstOptionalIndex >= 0 ? firstOptionalIndex : positionalParams.length;
870-
for (let i = requiredPosCount; i <= positionalParams.length; i++) {
871-
const head = positionalParams.slice(0, i).map(p => renderPositionalParam(p, true));
872-
const rest: string[] = [];
873-
if (varArgsParam) {
874-
const vname = this.escapeIdentifier(varArgsParam.name);
875-
rest.push(
876-
needsVarArgsArray ? `${vname}: unknown[] | undefined` : `...${vname}: unknown[]`
877-
);
878-
}
879-
rest.push(`kwargs: ${kwargsType}`);
880-
overloads.push(
881-
` static create${classTypeParamDecl}(${[...head, ...rest].join(', ')}): Promise<${classSelfType}>;`
882-
);
883-
if (varArgsParam && needsVarArgsArray) {
884-
overloads.push(
885-
` static create${classTypeParamDecl}(${[...head, `kwargs: ${kwargsType}`].join(', ')}): Promise<${classSelfType}>;`
886-
);
887-
}
888-
}
889-
}
890-
const overloadDecl = overloads.length > 0 ? `${overloads.join('\n')}\n` : '';
891-
const declaration =
892-
overloads.length > 0
893-
? overloadDecl
894-
: ` static create${classTypeParamDecl}(${paramsDecl}): Promise<${classSelfType}>;\n`;
895-
896-
const callDescriptor: CallDescriptor = {
897-
positionalParams,
898-
varArgsParam,
899-
needsVarArgsArray,
900-
hasKwArgs: needsKwargsParam,
901-
hasVarKwArgs,
902-
keywordOnlyNames,
903-
requiredKwOnlyNames,
904-
positionalOnlyNames,
905-
requiredPosCount,
906-
indent: ' ',
907-
errorLabel: '__init__',
908-
};
909-
const callPreludeLines = emitCallPrelude(callDescriptor, this.callEmitHelpers());
910-
const callPrelude = callPreludeLines.length > 0 ? `${callPreludeLines.join('\n')}\n` : '';
911-
const guardLines = emitArgGuards(callDescriptor);
912-
913-
return {
914-
overloadDecl,
915-
declaration,
916-
paramsDecl,
917-
callPrelude,
918-
hasKwargs: needsKwargsParam,
919-
guardLines,
920-
};
921-
})();
922-
923-
// @property / functools.cached_property → TS getters returning Promise<T>.
924-
// Reading the attribute fires the getter on the Python side; the bridge
925-
// resolves it via callMethod with no args. Sorted for stable output.
926-
const accessorBodies: string[] = [];
927-
const accessorDeclarations: string[] = [];
928-
const sortedAccessors = [...(cls.accessors ?? [])].sort((a, b) => a.name.localeCompare(b.name));
929-
sortedAccessors.forEach(accessor => {
930-
const aname = this.escapeIdentifier(accessor.name);
931-
const accessorType = this.typeToTsFromPython(accessor.type, classGenericContext, 'return');
932-
const jsdocAcc = this.generateJsDoc(accessor.docstring);
933-
accessorBodies.push(
934-
`${jsdocAcc} get ${aname}(): Promise<${accessorType}> { return getRuntimeBridge().callMethod(this.__handle, '${accessor.name}', []); }`
935-
);
936-
accessorDeclarations.push(`${jsdocAcc} get ${aname}(): Promise<${accessorType}>;\n`);
937-
});
938-
const accessorsImpl = accessorBodies.length > 0 ? `${accessorBodies.join('\n')}\n` : '';
939-
const accessorsDecl = accessorDeclarations.length > 0 ? `${accessorDeclarations.join('')}` : '';
940-
941795
const methodsSection = methodBodies.length > 0 ? `\n${methodBodies.join('\n')}\n` : '\n';
942796
const declarationMethodsSection =
943797
methodDeclarations.length > 0 ? `\n${methodDeclarations.join('')}\n` : '\n';
944-
const ctorGuards = ctorSpec.guardLines.length > 0 ? `${ctorSpec.guardLines.join('\n')}\n` : '';
945-
const newClassExpr = `new ${cname}${classGenericContext.typeArguments}(handle)`;
798+
// The constructor counts: a class whose only member was __init__ loses
799+
// create(), so it needs the migration note as much as one with methods.
800+
const omittedInstanceMembers =
801+
cls.methods.some(
802+
method => method.methodKind !== 'class' && method.methodKind !== 'static'
803+
) || (cls.accessors?.length ?? 0) > 0;
804+
const migrationNote =
805+
methodBodies.length === 0 && omittedInstanceMembers
806+
? ' // NOTE: Instance members are not generated in v0.9; migrate this API to value-returning module functions.\n'
807+
: '';
946808
const ts = `${jsdoc}export class ${cname}${classTypeParamDecl} {
947-
private readonly __handle: string;
948-
private constructor(handle: string) { this.__handle = handle; }
949-
${ctorSpec.overloadDecl} static async create${classTypeParamDecl}(${ctorSpec.paramsDecl}): Promise<${classSelfType}> {
950-
${ctorSpec.callPrelude}${ctorGuards} const handle = await getRuntimeBridge().instantiate<string>('${moduleId}', '${cls.name}', __args${
951-
ctorSpec.hasKwargs ? ', __kwargs' : ''
952-
});
953-
return ${newClassExpr};
954-
}
955-
static fromHandle${classTypeParamDecl}(handle: string): ${classSelfType} { return ${newClassExpr}; }${methodsSection}${accessorsImpl} async disposeHandle(): Promise<void> { await getRuntimeBridge().disposeInstance(this.__handle); }
809+
${migrationNote}${methodsSection}
956810
}
957811
`;
958812

959813
const declaration = `${jsdoc}export class ${cname}${classTypeParamDecl} {
960-
private readonly __handle: string;
961-
private constructor(handle: string);
962-
${ctorSpec.declaration} static fromHandle${classTypeParamDecl}(handle: string): ${classSelfType};${declarationMethodsSection}${accessorsDecl} disposeHandle(): Promise<void>;
814+
${migrationNote}${declarationMethodsSection}
963815
}
964816
`;
965817

0 commit comments

Comments
 (0)