Skip to content

Commit

Permalink
fix: Proxy interface literals in the generated Python code
Browse files Browse the repository at this point in the history
  • Loading branch information
dstufft committed Mar 13, 2019
1 parent 6f1c9c0 commit 10242eb
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 22 deletions.
63 changes: 53 additions & 10 deletions packages/jsii-pacmak/lib/targets/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ interface BaseMethodOpts {

interface BaseMethodEmitOpts {
renderAbstract?: boolean;
forceEmitBody?: boolean;
}

abstract class BaseMethod implements PythonBase {
Expand All @@ -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[];
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -498,6 +500,7 @@ interface BasePropertyOpts {

interface BasePropertyEmitOpts {
renderAbstract?: boolean;
forceEmitBody?: boolean;
}

abstract class BaseProperty implements PythonBase {
Expand All @@ -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;
Expand All @@ -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}`);
Expand All @@ -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("...");
Expand All @@ -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("...");
Expand All @@ -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));

Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions packages/jsii-python-runtime/src/jsii/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
enum,
data_type,
implements,
interface,
member,
kernel,
proxy_for,
Expand Down Expand Up @@ -43,6 +44,7 @@
"enum",
"data_type",
"implements",
"interface",
"member",
"kernel",
"proxy_for",
Expand Down
15 changes: 15 additions & 0 deletions packages/jsii-python-runtime/src/jsii/_reference_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
_types = {}
_data_types: MutableMapping[str, Any] = {}
_enums: MutableMapping[str, Any] = {}
_interfaces: MutableMapping[str, Any] = {}


def register_type(klass: JSClass):
Expand All @@ -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
Expand Down Expand Up @@ -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}")

Expand Down
9 changes: 9 additions & 0 deletions packages/jsii-python-runtime/src/jsii/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
12 changes: 0 additions & 12 deletions packages/jsii-python-runtime/tests/test_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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()
Expand All @@ -753,7 +743,6 @@ def test_testLiteralInterface():
assert gen.next() == 42


@xfail_literal_interface
def test_testInterfaceParameter():
obj = JSObjectLiteralForInterface()
friendly = obj.give_me_friendly()
Expand Down Expand Up @@ -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

0 comments on commit 10242eb

Please sign in to comment.