From 10242eb585eae723f9b7dc795f8fb96334c5f914 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Wed, 13 Mar 2019 09:04:22 -0400 Subject: [PATCH] fix: Proxy interface literals in the generated Python code --- packages/jsii-pacmak/lib/targets/python.ts | 63 ++++++++++++++++--- .../jsii-python-runtime/src/jsii/__init__.py | 2 + .../src/jsii/_reference_map.py | 15 +++++ .../jsii-python-runtime/src/jsii/_runtime.py | 9 +++ .../tests/test_compliance.py | 12 ---- 5 files changed, 79 insertions(+), 22 deletions(-) diff --git a/packages/jsii-pacmak/lib/targets/python.ts b/packages/jsii-pacmak/lib/targets/python.ts index 74ecf418a1..99794c4b6d 100644 --- a/packages/jsii-pacmak/lib/targets/python.ts +++ b/packages/jsii-pacmak/lib/targets/python.ts @@ -277,6 +277,7 @@ interface BaseMethodOpts { interface BaseMethodEmitOpts { renderAbstract?: boolean; + forceEmitBody?: boolean; } abstract class BaseMethod implements PythonBase { @@ -285,10 +286,11 @@ abstract class BaseMethod implements PythonBase { public readonly abstract: boolean; protected readonly abstract implicitParameter: string; - protected readonly jsiiMethod?: string; + protected readonly jsiiMethod: string; protected readonly decorator?: string; protected readonly classAsFirstParameter: boolean = false; protected readonly returnFromJSIIMethod: boolean = true; + protected readonly shouldEmitBody: boolean = true; private readonly jsName?: string; private readonly parameters: spec.Parameter[]; @@ -313,7 +315,7 @@ abstract class BaseMethod implements PythonBase { } public emit(code: CodeMaker, resolver: TypeResolver, opts?: BaseMethodEmitOpts) { - const { renderAbstract = true } = opts || {}; + const { renderAbstract = true, forceEmitBody = false } = opts || {}; let returnType: string; if (this.returns !== undefined) { @@ -420,12 +422,12 @@ abstract class BaseMethod implements PythonBase { } code.openBlock(`def ${this.name}(${pythonParams.join(", ")}) -> ${returnType}`); - this.emitBody(code, resolver, renderAbstract); + this.emitBody(code, resolver, renderAbstract, forceEmitBody); code.closeBlock(); } - private emitBody(code: CodeMaker, resolver: TypeResolver, renderAbstract: boolean) { - if (this.jsiiMethod === undefined || (renderAbstract && this.abstract)) { + private emitBody(code: CodeMaker, resolver: TypeResolver, renderAbstract: boolean, forceEmitBody: boolean) { + if ((!this.shouldEmitBody && !forceEmitBody) || (renderAbstract && this.abstract)) { code.line("..."); } else { if (this.liftedProp !== undefined) { @@ -498,6 +500,7 @@ interface BasePropertyOpts { interface BasePropertyEmitOpts { renderAbstract?: boolean; + forceEmitBody?: boolean; } abstract class BaseProperty implements PythonBase { @@ -507,8 +510,9 @@ abstract class BaseProperty implements PythonBase { protected readonly abstract decorator: string; protected readonly abstract implicitParameter: string; - protected readonly jsiiGetMethod?: string; - protected readonly jsiiSetMethod?: string; + protected readonly jsiiGetMethod: string; + protected readonly jsiiSetMethod: string; + protected readonly shouldEmitBody: boolean = true; private readonly jsName: string; private readonly type: spec.TypeReference; @@ -528,7 +532,7 @@ abstract class BaseProperty implements PythonBase { } public emit(code: CodeMaker, resolver: TypeResolver, opts?: BasePropertyEmitOpts) { - const { renderAbstract = true } = opts || {}; + const { renderAbstract = true, forceEmitBody = false } = opts || {}; const pythonType = resolver.resolve(this.type, { forwardReferences: false }); code.line(`@${this.decorator}`); @@ -537,7 +541,7 @@ abstract class BaseProperty implements PythonBase { code.line("@abc.abstractmethod"); } code.openBlock(`def ${this.name}(${this.implicitParameter}) -> ${pythonType}`); - if (this.jsiiGetMethod !== undefined && (!renderAbstract || !this.abstract)) { + if ((this.shouldEmitBody || forceEmitBody) && (!renderAbstract || !this.abstract)) { code.line(`return jsii.${this.jsiiGetMethod}(${this.implicitParameter}, "${this.jsName}")`); } else { code.line("..."); @@ -550,7 +554,7 @@ abstract class BaseProperty implements PythonBase { code.line("@abc.abstractmethod"); } code.openBlock(`def ${this.name}(${this.implicitParameter}, value: ${pythonType})`); - if (this.jsiiSetMethod !== undefined && (!renderAbstract || !this.abstract)) { + if ((this.shouldEmitBody || forceEmitBody) && (!renderAbstract || !this.abstract)) { code.line(`return jsii.${this.jsiiSetMethod}(${this.implicitParameter}, "${this.jsName}", value)`); } else { code.line("..."); @@ -562,6 +566,29 @@ abstract class BaseProperty implements PythonBase { class Interface extends BasePythonClassType { + public emit(code: CodeMaker, resolver: TypeResolver) { + code.line(`@jsii.interface(jsii_type="${this.fqn}")`); + + // First we do our normal class logic for emitting our members. + super.emit(code, resolver); + + // Then, we have to emit a Proxy class which implements our proxy interface. + resolver = this.fqn ? resolver.bind(this.fqn) : resolver; + const proxyBases: string[] = this.bases.map(b => `jsii.proxy_for(${resolver.resolve(b)})`); + code.openBlock(`class ${this.getProxyClassName()}(${proxyBases.join(", ")})`); + code.line(`__jsii_type__ = "${this.fqn}"`); + + if (this.members.length > 0) { + for (const member of this.members) { + member.emit(code, resolver, { forceEmitBody: true }); + } + } else { + code.line("pass"); + } + + code.closeBlock(); + } + protected getClassParams(resolver: TypeResolver): string[] { const params: string[] = this.bases.map(b => resolver.resolve(b)); @@ -570,15 +597,31 @@ class Interface extends BasePythonClassType { return params; } + protected emitPreamble(code: CodeMaker, _resolver: TypeResolver) { + code.line("@staticmethod"); + code.openBlock("def __jsii_proxy_class__()"); + code.line(`return ${this.getProxyClassName()}`); + code.closeBlock(); + } + + private getProxyClassName(): string { + return `_${this.name}Proxy`; + } + } class InterfaceMethod extends BaseMethod { protected readonly implicitParameter: string = "self"; + protected readonly jsiiMethod: string = "invoke"; + protected readonly shouldEmitBody: boolean = false; } class InterfaceProperty extends BaseProperty { protected readonly decorator: string = "property"; protected readonly implicitParameter: string = "self"; + protected readonly jsiiGetMethod: string = "get"; + protected readonly jsiiSetMethod: string = "set"; + protected readonly shouldEmitBody: boolean = false; } class TypedDict extends BasePythonClassType { diff --git a/packages/jsii-python-runtime/src/jsii/__init__.py b/packages/jsii-python-runtime/src/jsii/__init__.py index 7262e1d77b..c784ed8cc4 100644 --- a/packages/jsii-python-runtime/src/jsii/__init__.py +++ b/packages/jsii-python-runtime/src/jsii/__init__.py @@ -8,6 +8,7 @@ enum, data_type, implements, + interface, member, kernel, proxy_for, @@ -43,6 +44,7 @@ "enum", "data_type", "implements", + "interface", "member", "kernel", "proxy_for", diff --git a/packages/jsii-python-runtime/src/jsii/_reference_map.py b/packages/jsii-python-runtime/src/jsii/_reference_map.py index 9c4c4cd258..ea9172e724 100644 --- a/packages/jsii-python-runtime/src/jsii/_reference_map.py +++ b/packages/jsii-python-runtime/src/jsii/_reference_map.py @@ -9,6 +9,7 @@ _types = {} _data_types: MutableMapping[str, Any] = {} _enums: MutableMapping[str, Any] = {} +_interfaces: MutableMapping[str, Any] = {} def register_type(klass: JSClass): @@ -23,6 +24,10 @@ def register_enum(enum_type: Any): _enums[enum_type.__jsii_type__] = enum_type +def register_interface(iface: Any): + _interfaces[iface.__jsii_type__] = iface + + class _FakeReference: def __init__(self, ref: str) -> None: self.__jsii_ref__ = ref @@ -78,6 +83,16 @@ def resolve(self, kernel, ref): inst[name] = kernel.get(_FakeReference(ref), name) elif class_fqn in _enums: inst = _enums[class_fqn] + elif class_fqn in _interfaces: + # Get our proxy class by finding our interface, then asking it to give us + # the proxy class. + iface = _interfaces[class_fqn] + klass = iface.__jsii_proxy_class__() + + # Create our instance, bypassing __init__ by directly calling __new__, and + # then assign our reference to __jsii_ref__ + inst = klass.__new__(klass) + inst.__jsii_ref__ = ref else: raise ValueError(f"Unknown type: {class_fqn}") diff --git a/packages/jsii-python-runtime/src/jsii/_runtime.py b/packages/jsii-python-runtime/src/jsii/_runtime.py index 435cd4507a..d8cc273951 100644 --- a/packages/jsii-python-runtime/src/jsii/_runtime.py +++ b/packages/jsii-python-runtime/src/jsii/_runtime.py @@ -109,6 +109,15 @@ def deco(cls): return deco +def interface(*, jsii_type): + def deco(iface): + iface.__jsii_type__ = jsii_type + _reference_map.register_interface(iface) + return iface + + return deco + + def proxy_for(abstract_class): if not hasattr(abstract_class, "__jsii_proxy_class__"): raise TypeError(f"{abstract_class} is not a JSII Abstract class.") diff --git a/packages/jsii-python-runtime/tests/test_compliance.py b/packages/jsii-python-runtime/tests/test_compliance.py index 2321900611..431d7928ed 100644 --- a/packages/jsii-python-runtime/tests/test_compliance.py +++ b/packages/jsii-python-runtime/tests/test_compliance.py @@ -50,15 +50,6 @@ # These map distinct reasons for failures, so we an easily find them. xfail_async = pytest.mark.xfail(reason="Implement async methods", strict=True) -xfail_literal_interface = pytest.mark.xfail( - reason="Implement someone returning a literal interface", strict=True -) -xfail_abstract_class = pytest.mark.xfail( - reason="Implement (or fix?) abstract class property", strict=True -) -xfail_private_class = pytest.mark.xfail( - reason="Implement receiving a private class", strict=True -) xfail_callbacks = pytest.mark.xfail(reason="Implement callback support", strict=True) @@ -742,7 +733,6 @@ def test_testNativeObjectsWithInterfaces(): assert generator_bound_to_pure_native.next_times100() == 200_000 -@xfail_literal_interface def test_testLiteralInterface(): obj = JSObjectLiteralForInterface() friendly = obj.give_me_friendly() @@ -753,7 +743,6 @@ def test_testLiteralInterface(): assert gen.next() == 42 -@xfail_literal_interface def test_testInterfaceParameter(): obj = JSObjectLiteralForInterface() friendly = obj.give_me_friendly() @@ -887,6 +876,5 @@ def test_testJsiiAgent(): assert JsiiAgent.jsii_agent == f"Python/{platform.python_version()}" -@xfail_private_class def test_receiveInstanceOfPrivateClass(): assert ReturnsPrivateImplementationOfInterface().private_implementation.success