Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(python): correctly handle interface declarations on returned objects #980

Merged
merged 4 commits into from
Nov 13, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions packages/jsii-calc/lib/compliance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2208,3 +2208,31 @@ export class RootStructValidator {

private constructor() { }
}

/**
* Returns a subclass of a known class which implements an interface.
*/
export interface IReturnJsii976 {
readonly foo: number;
}

export class BaseJsii976 { }

export class SomeTypeJsii976 {

static returnReturn(): IReturnJsii976 {
class Derived extends BaseJsii976 implements IReturnJsii976 {
public readonly foo = 333
}

return new Derived();
}

static returnAnonymous(): any {
class Derived implements IReturnJsii976 {
public readonly foo = 1337;
}

return new Derived();
}
}
95 changes: 94 additions & 1 deletion packages/jsii-calc/test/assembly.jsii
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,20 @@
],
"name": "AugmentableClass"
},
"jsii-calc.BaseJsii976": {
"assembly": "jsii-calc",
"docs": {
"stability": "experimental"
},
"fqn": "jsii-calc.BaseJsii976",
"initializer": {},
"kind": "class",
"locationInModule": {
"filename": "lib/compliance.ts",
"line": 2219
},
"name": "BaseJsii976"
},
"jsii-calc.Bell": {
"assembly": "jsii-calc",
"docs": {
Expand Down Expand Up @@ -5140,6 +5154,37 @@
],
"name": "IRandomNumberGenerator"
},
"jsii-calc.IReturnJsii976": {
"assembly": "jsii-calc",
"docs": {
"stability": "experimental",
"summary": "Returns a subclass of a known class which implements an interface."
},
"fqn": "jsii-calc.IReturnJsii976",
"kind": "interface",
"locationInModule": {
"filename": "lib/compliance.ts",
"line": 2215
},
"name": "IReturnJsii976",
"properties": [
{
"abstract": true,
"docs": {
"stability": "experimental"
},
"immutable": true,
"locationInModule": {
"filename": "lib/compliance.ts",
"line": 2216
},
"name": "foo",
"type": {
"primitive": "number"
}
}
]
},
"jsii-calc.IReturnsNumber": {
"assembly": "jsii-calc",
"docs": {
Expand Down Expand Up @@ -8654,6 +8699,54 @@
],
"name": "SingletonStringEnum"
},
"jsii-calc.SomeTypeJsii976": {
"assembly": "jsii-calc",
"docs": {
"stability": "experimental"
},
"fqn": "jsii-calc.SomeTypeJsii976",
"initializer": {},
"kind": "class",
"locationInModule": {
"filename": "lib/compliance.ts",
"line": 2221
},
"methods": [
{
"docs": {
"stability": "experimental"
},
"locationInModule": {
"filename": "lib/compliance.ts",
"line": 2231
},
"name": "returnAnonymous",
"returns": {
"type": {
"primitive": "any"
}
},
"static": true
},
{
"docs": {
"stability": "experimental"
},
"locationInModule": {
"filename": "lib/compliance.ts",
"line": 2223
},
"name": "returnReturn",
"returns": {
"type": {
"fqn": "jsii-calc.IReturnJsii976"
}
},
"static": true
}
],
"name": "SomeTypeJsii976"
},
"jsii-calc.StableClass": {
"assembly": "jsii-calc",
"docs": {
Expand Down Expand Up @@ -10913,5 +11006,5 @@
}
},
"version": "0.20.3",
"fingerprint": "1F+uskR3++T5mjRcWge9oG3H/jJvXm1C3IhR1AwsBTE="
"fingerprint": "umMeNAH41pX11GUAjHkw6RTyAwGCeTqRNJ2vPv5aeM0="
}
44 changes: 27 additions & 17 deletions packages/jsii-python-runtime/src/jsii/_reference_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def resolve(self, kernel, ref):
# First we need to check our reference map to see if we have any instance that
# already matches this reference.
try:
# TODO: Handle discovery of possible new interfaces on the ObjRef
return self._refs[ref.ref]
except KeyError:
pass
Expand All @@ -69,6 +70,13 @@ def resolve(self, kernel, ref):
# then assign our reference to __jsii_ref__
inst = klass.__new__(klass)
inst.__jsii_ref__ = ref

if ref.interfaces is not None:
return InterfaceDynamicProxy([inst] + self.build_interface_proxies_for_ref(ref))
else:
return inst

# Legacy code path - Kernel invariant ought to guarantee that class_fqn can't be Struct (they're interfaces)
elif class_fqn in _data_types:
# Data types have been serialized by-reference (see aws/jsii#400).
# We retrieve all of its properties right now and then construct a value
Expand All @@ -86,8 +94,9 @@ def resolve(self, kernel, ref):

return data_type(**python_props)
elif class_fqn in _enums:
inst = _enums[class_fqn]
return _enums[class_fqn]
elif class_fqn == "Object" and ref.interfaces is not None:
# If any one interface is a struct, all of them are guaranteed to be (Kernel invariant)
if any(fqn in _data_types for fqn in ref.interfaces):
# Ugly delayed import here because I can't solve the cyclic
# package dependency right now :(.
Expand All @@ -100,32 +109,34 @@ def resolve(self, kernel, ref):
}) for struct in structs]
return StructDynamicProxy(insts)
else:
ifaces = [_interfaces[fqn] for fqn in ref.interfaces]
classes = [iface.__jsii_proxy_class__() for iface in ifaces]
insts = [klass.__new__(klass) for klass in classes]
for inst in insts:
inst.__jsii_ref__ = ref
return InterfaceDynamicProxy(insts)
return InterfaceDynamicProxy(self.build_interface_proxies_for_ref(ref))
else:
raise ValueError(f"Unknown type: {class_fqn}")

return inst

def resolve_id(self, id):
return self._refs[id]

def build_interface_proxies_for_ref(self, ref):
if ref.interfaces is None:
raise AssertionError("Attempted to create interface proxies for ObjectRef without interfaces!")
ifaces = [_interfaces[fqn] for fqn in ref.interfaces]
classes = [iface.__jsii_proxy_class__() for iface in ifaces]
insts = [klass.__new__(klass) for klass in classes]
for inst in insts:
inst.__jsii_ref__ = ref
return insts


class InterfaceDynamicProxy(object):
def __init__(self, delegates):
self._delegates = delegates

def __getattr__(self, name):
for delegate in self._delegates:
try:
if hasattr(delegate, name):
return getattr(delegate, name)
except NameError:
pass
return None
type_info = "+".join([str(delegate.__class__) for delegate in self._delegates])
raise AttributeError(f"'%s' object has no attribute '%s'" % (type_info, name))


class StructDynamicProxy(object):
Expand All @@ -134,11 +145,10 @@ def __init__(self, delegates):

def __getattr__(self, name):
for delegate in self._delegates:
try:
if hasattr(delegate, name):
return getattr(delegate, name)
except NameError:
pass
return None
type_info = "+".join([str(delegate.__class__) for delegate in self._delegates])
raise AttributeError("'%s' object has no attribute '%s'" % (type_info, name))

def __eq__(self, rhs) -> bool:
if len(self._delegates) == 1:
Expand Down
22 changes: 20 additions & 2 deletions packages/jsii-python-runtime/tests/test_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@
EraseUndefinedHashValues,
EraseUndefinedHashValuesOptions,
VariadicMethod,
RootStruct,
RootStructValidator,
StructPassing,
TopLevelStruct,
SecondLevelStruct,
StructA,
StructB,
StructUnionConsumer
StructUnionConsumer,
SomeTypeJsii976
)
from scope.jsii_calc_lib import IFriendly, EnumFromScopedModule, Number

Expand Down Expand Up @@ -1013,6 +1013,24 @@ def test_can_pass_nested_struct_as_dict():
}
)

# https://github.com/aws/jsii/issues/976
def test_return_subclass_that_implements_interface_976():
obj = SomeTypeJsii976.return_return()
assert obj.foo == 333

def test_return_subclass_that_implements_interface_976_raises_attributeerror_when_using_non_existent_method():
obj = SomeTypeJsii976.return_return()
try:
print(obj.not_a_real_method_I_swear)
failed = False
except AttributeError as err:
failed = True
assert err.args[0] == "'<class 'jsii_calc.BaseJsii976'>+<class 'jsii_calc._IReturnJsii976Proxy'>' object has no attribute 'not_a_real_method_I_swear'"
assert failed

def test_return_anonymous_implementation_of_interface():
assert SomeTypeJsii976.return_anonymous() is not None

@jsii.implements(IBellRinger)
class PythonBellRinger:
def your_turn(self, bell):
Expand Down