Skip to content

Commit 10242eb

Browse files
authored
fix: Proxy interface literals in the generated Python code
1 parent 6f1c9c0 commit 10242eb

File tree

5 files changed

+79
-22
lines changed

5 files changed

+79
-22
lines changed

packages/jsii-pacmak/lib/targets/python.ts

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ interface BaseMethodOpts {
277277

278278
interface BaseMethodEmitOpts {
279279
renderAbstract?: boolean;
280+
forceEmitBody?: boolean;
280281
}
281282

282283
abstract class BaseMethod implements PythonBase {
@@ -285,10 +286,11 @@ abstract class BaseMethod implements PythonBase {
285286
public readonly abstract: boolean;
286287

287288
protected readonly abstract implicitParameter: string;
288-
protected readonly jsiiMethod?: string;
289+
protected readonly jsiiMethod: string;
289290
protected readonly decorator?: string;
290291
protected readonly classAsFirstParameter: boolean = false;
291292
protected readonly returnFromJSIIMethod: boolean = true;
293+
protected readonly shouldEmitBody: boolean = true;
292294

293295
private readonly jsName?: string;
294296
private readonly parameters: spec.Parameter[];
@@ -313,7 +315,7 @@ abstract class BaseMethod implements PythonBase {
313315
}
314316

315317
public emit(code: CodeMaker, resolver: TypeResolver, opts?: BaseMethodEmitOpts) {
316-
const { renderAbstract = true } = opts || {};
318+
const { renderAbstract = true, forceEmitBody = false } = opts || {};
317319

318320
let returnType: string;
319321
if (this.returns !== undefined) {
@@ -420,12 +422,12 @@ abstract class BaseMethod implements PythonBase {
420422
}
421423

422424
code.openBlock(`def ${this.name}(${pythonParams.join(", ")}) -> ${returnType}`);
423-
this.emitBody(code, resolver, renderAbstract);
425+
this.emitBody(code, resolver, renderAbstract, forceEmitBody);
424426
code.closeBlock();
425427
}
426428

427-
private emitBody(code: CodeMaker, resolver: TypeResolver, renderAbstract: boolean) {
428-
if (this.jsiiMethod === undefined || (renderAbstract && this.abstract)) {
429+
private emitBody(code: CodeMaker, resolver: TypeResolver, renderAbstract: boolean, forceEmitBody: boolean) {
430+
if ((!this.shouldEmitBody && !forceEmitBody) || (renderAbstract && this.abstract)) {
429431
code.line("...");
430432
} else {
431433
if (this.liftedProp !== undefined) {
@@ -498,6 +500,7 @@ interface BasePropertyOpts {
498500

499501
interface BasePropertyEmitOpts {
500502
renderAbstract?: boolean;
503+
forceEmitBody?: boolean;
501504
}
502505

503506
abstract class BaseProperty implements PythonBase {
@@ -507,8 +510,9 @@ abstract class BaseProperty implements PythonBase {
507510

508511
protected readonly abstract decorator: string;
509512
protected readonly abstract implicitParameter: string;
510-
protected readonly jsiiGetMethod?: string;
511-
protected readonly jsiiSetMethod?: string;
513+
protected readonly jsiiGetMethod: string;
514+
protected readonly jsiiSetMethod: string;
515+
protected readonly shouldEmitBody: boolean = true;
512516

513517
private readonly jsName: string;
514518
private readonly type: spec.TypeReference;
@@ -528,7 +532,7 @@ abstract class BaseProperty implements PythonBase {
528532
}
529533

530534
public emit(code: CodeMaker, resolver: TypeResolver, opts?: BasePropertyEmitOpts) {
531-
const { renderAbstract = true } = opts || {};
535+
const { renderAbstract = true, forceEmitBody = false } = opts || {};
532536
const pythonType = resolver.resolve(this.type, { forwardReferences: false });
533537

534538
code.line(`@${this.decorator}`);
@@ -537,7 +541,7 @@ abstract class BaseProperty implements PythonBase {
537541
code.line("@abc.abstractmethod");
538542
}
539543
code.openBlock(`def ${this.name}(${this.implicitParameter}) -> ${pythonType}`);
540-
if (this.jsiiGetMethod !== undefined && (!renderAbstract || !this.abstract)) {
544+
if ((this.shouldEmitBody || forceEmitBody) && (!renderAbstract || !this.abstract)) {
541545
code.line(`return jsii.${this.jsiiGetMethod}(${this.implicitParameter}, "${this.jsName}")`);
542546
} else {
543547
code.line("...");
@@ -550,7 +554,7 @@ abstract class BaseProperty implements PythonBase {
550554
code.line("@abc.abstractmethod");
551555
}
552556
code.openBlock(`def ${this.name}(${this.implicitParameter}, value: ${pythonType})`);
553-
if (this.jsiiSetMethod !== undefined && (!renderAbstract || !this.abstract)) {
557+
if ((this.shouldEmitBody || forceEmitBody) && (!renderAbstract || !this.abstract)) {
554558
code.line(`return jsii.${this.jsiiSetMethod}(${this.implicitParameter}, "${this.jsName}", value)`);
555559
} else {
556560
code.line("...");
@@ -562,6 +566,29 @@ abstract class BaseProperty implements PythonBase {
562566

563567
class Interface extends BasePythonClassType {
564568

569+
public emit(code: CodeMaker, resolver: TypeResolver) {
570+
code.line(`@jsii.interface(jsii_type="${this.fqn}")`);
571+
572+
// First we do our normal class logic for emitting our members.
573+
super.emit(code, resolver);
574+
575+
// Then, we have to emit a Proxy class which implements our proxy interface.
576+
resolver = this.fqn ? resolver.bind(this.fqn) : resolver;
577+
const proxyBases: string[] = this.bases.map(b => `jsii.proxy_for(${resolver.resolve(b)})`);
578+
code.openBlock(`class ${this.getProxyClassName()}(${proxyBases.join(", ")})`);
579+
code.line(`__jsii_type__ = "${this.fqn}"`);
580+
581+
if (this.members.length > 0) {
582+
for (const member of this.members) {
583+
member.emit(code, resolver, { forceEmitBody: true });
584+
}
585+
} else {
586+
code.line("pass");
587+
}
588+
589+
code.closeBlock();
590+
}
591+
565592
protected getClassParams(resolver: TypeResolver): string[] {
566593
const params: string[] = this.bases.map(b => resolver.resolve(b));
567594

@@ -570,15 +597,31 @@ class Interface extends BasePythonClassType {
570597
return params;
571598
}
572599

600+
protected emitPreamble(code: CodeMaker, _resolver: TypeResolver) {
601+
code.line("@staticmethod");
602+
code.openBlock("def __jsii_proxy_class__()");
603+
code.line(`return ${this.getProxyClassName()}`);
604+
code.closeBlock();
605+
}
606+
607+
private getProxyClassName(): string {
608+
return `_${this.name}Proxy`;
609+
}
610+
573611
}
574612

575613
class InterfaceMethod extends BaseMethod {
576614
protected readonly implicitParameter: string = "self";
615+
protected readonly jsiiMethod: string = "invoke";
616+
protected readonly shouldEmitBody: boolean = false;
577617
}
578618

579619
class InterfaceProperty extends BaseProperty {
580620
protected readonly decorator: string = "property";
581621
protected readonly implicitParameter: string = "self";
622+
protected readonly jsiiGetMethod: string = "get";
623+
protected readonly jsiiSetMethod: string = "set";
624+
protected readonly shouldEmitBody: boolean = false;
582625
}
583626

584627
class TypedDict extends BasePythonClassType {

packages/jsii-python-runtime/src/jsii/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
enum,
99
data_type,
1010
implements,
11+
interface,
1112
member,
1213
kernel,
1314
proxy_for,
@@ -43,6 +44,7 @@
4344
"enum",
4445
"data_type",
4546
"implements",
47+
"interface",
4648
"member",
4749
"kernel",
4850
"proxy_for",

packages/jsii-python-runtime/src/jsii/_reference_map.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
_types = {}
1010
_data_types: MutableMapping[str, Any] = {}
1111
_enums: MutableMapping[str, Any] = {}
12+
_interfaces: MutableMapping[str, Any] = {}
1213

1314

1415
def register_type(klass: JSClass):
@@ -23,6 +24,10 @@ def register_enum(enum_type: Any):
2324
_enums[enum_type.__jsii_type__] = enum_type
2425

2526

27+
def register_interface(iface: Any):
28+
_interfaces[iface.__jsii_type__] = iface
29+
30+
2631
class _FakeReference:
2732
def __init__(self, ref: str) -> None:
2833
self.__jsii_ref__ = ref
@@ -78,6 +83,16 @@ def resolve(self, kernel, ref):
7883
inst[name] = kernel.get(_FakeReference(ref), name)
7984
elif class_fqn in _enums:
8085
inst = _enums[class_fqn]
86+
elif class_fqn in _interfaces:
87+
# Get our proxy class by finding our interface, then asking it to give us
88+
# the proxy class.
89+
iface = _interfaces[class_fqn]
90+
klass = iface.__jsii_proxy_class__()
91+
92+
# Create our instance, bypassing __init__ by directly calling __new__, and
93+
# then assign our reference to __jsii_ref__
94+
inst = klass.__new__(klass)
95+
inst.__jsii_ref__ = ref
8196
else:
8297
raise ValueError(f"Unknown type: {class_fqn}")
8398

packages/jsii-python-runtime/src/jsii/_runtime.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ def deco(cls):
109109
return deco
110110

111111

112+
def interface(*, jsii_type):
113+
def deco(iface):
114+
iface.__jsii_type__ = jsii_type
115+
_reference_map.register_interface(iface)
116+
return iface
117+
118+
return deco
119+
120+
112121
def proxy_for(abstract_class):
113122
if not hasattr(abstract_class, "__jsii_proxy_class__"):
114123
raise TypeError(f"{abstract_class} is not a JSII Abstract class.")

packages/jsii-python-runtime/tests/test_compliance.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,6 @@
5050

5151
# These map distinct reasons for failures, so we an easily find them.
5252
xfail_async = pytest.mark.xfail(reason="Implement async methods", strict=True)
53-
xfail_literal_interface = pytest.mark.xfail(
54-
reason="Implement someone returning a literal interface", strict=True
55-
)
56-
xfail_abstract_class = pytest.mark.xfail(
57-
reason="Implement (or fix?) abstract class property", strict=True
58-
)
59-
xfail_private_class = pytest.mark.xfail(
60-
reason="Implement receiving a private class", strict=True
61-
)
6253
xfail_callbacks = pytest.mark.xfail(reason="Implement callback support", strict=True)
6354

6455

@@ -742,7 +733,6 @@ def test_testNativeObjectsWithInterfaces():
742733
assert generator_bound_to_pure_native.next_times100() == 200_000
743734

744735

745-
@xfail_literal_interface
746736
def test_testLiteralInterface():
747737
obj = JSObjectLiteralForInterface()
748738
friendly = obj.give_me_friendly()
@@ -753,7 +743,6 @@ def test_testLiteralInterface():
753743
assert gen.next() == 42
754744

755745

756-
@xfail_literal_interface
757746
def test_testInterfaceParameter():
758747
obj = JSObjectLiteralForInterface()
759748
friendly = obj.give_me_friendly()
@@ -887,6 +876,5 @@ def test_testJsiiAgent():
887876
assert JsiiAgent.jsii_agent == f"Python/{platform.python_version()}"
888877

889878

890-
@xfail_private_class
891879
def test_receiveInstanceOfPrivateClass():
892880
assert ReturnsPrivateImplementationOfInterface().private_implementation.success

0 commit comments

Comments
 (0)