Skip to content

Commit

Permalink
fix(python): maintain inheritance chain for structs (#482)
Browse files Browse the repository at this point in the history
Because structs all inherit from TypedDict, and TypedDict erases the
inheritance chain of structs, we have to maintain a copy of the
inheritance hierarchy on the class objects, for later use during
doc generation.

This addresses (part of) #473.
  • Loading branch information
rix0rrr committed Apr 23, 2019
1 parent fa4d000 commit 607f151
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 20 deletions.
6 changes: 4 additions & 2 deletions packages/jsii-pacmak/lib/targets/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ class TypedDict extends BasePythonClassType {
// and implement this "split" class logic.

const classParams = this.getClassParams(resolver);
const baseInterfaces = classParams.slice(0, classParams.length - 1);

const mandatoryMembers = this.members.filter(
item => item instanceof TypedDictProperty ? !item.optional : true
Expand All @@ -655,14 +656,15 @@ class TypedDict extends BasePythonClassType {

// We'll emit the optional members first, just because it's a little nicer
// for the final class in the chain to have the mandatory members.
code.line(`@jsii.data_type_optionals(jsii_struct_bases=[${baseInterfaces.join(', ')}])`);
code.openBlock(`class _${this.name}(${classParams.concat(["total=False"]).join(", ")})`);
for (const member of optionalMembers) {
member.emit(code, resolver);
}
code.closeBlock();

// Now we'll emit the mandatory members.
code.line(`@jsii.data_type(jsii_type="${this.fqn}")`);
code.line(`@jsii.data_type(jsii_type="${this.fqn}", jsii_struct_bases=[_${this.name}])`);
code.openBlock(`class ${this.name}(_${this.name})`);
emitDocString(code, this.docs);
for (const [member, sep] of separate(sortMembers(mandatoryMembers, resolver))) {
Expand All @@ -671,7 +673,7 @@ class TypedDict extends BasePythonClassType {
}
code.closeBlock();
} else {
code.line(`@jsii.data_type(jsii_type="${this.fqn}")`);
code.line(`@jsii.data_type(jsii_type="${this.fqn}", jsii_struct_bases=[${baseInterfaces.join(', ')}])`);

// In this case we either have no members, or we have all of one type, so
// we'll see if we have any optional members, if we don't then we'll use
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def type_name(self) -> typing.Any:
class _BaseProxy(Base):
pass

@jsii.data_type(jsii_type="@scope/jsii-calc-base.BaseProps")
@jsii.data_type(jsii_type="@scope/jsii-calc-base.BaseProps", jsii_struct_bases=[scope.jsii_calc_base_of_base.VeryBaseProps])
class BaseProps(scope.jsii_calc_base_of_base.VeryBaseProps, jsii.compat.TypedDict):
bar: str

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,11 @@ def baz(self) -> None:
return jsii.invoke(self, "baz", [])


@jsii.data_type_optionals(jsii_struct_bases=[])
class _MyFirstStruct(jsii.compat.TypedDict, total=False):
firstOptional: typing.List[str]

@jsii.data_type(jsii_type="@scope/jsii-calc-lib.MyFirstStruct")
@jsii.data_type(jsii_type="@scope/jsii-calc-lib.MyFirstStruct", jsii_struct_bases=[_MyFirstStruct])
class MyFirstStruct(_MyFirstStruct):
"""This is the first struct we have created in jsii."""
anumber: jsii.Number
Expand All @@ -109,7 +110,7 @@ class MyFirstStruct(_MyFirstStruct):
astring: str
"""A string value."""

@jsii.data_type(jsii_type="@scope/jsii-calc-lib.StructWithOnlyOptionals")
@jsii.data_type(jsii_type="@scope/jsii-calc-lib.StructWithOnlyOptionals", jsii_struct_bases=[])
class StructWithOnlyOptionals(jsii.compat.TypedDict, total=False):
"""This is a struct with only optional properties."""
optional1: str
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ def value(self) -> jsii.Number:
return jsii.get(self, "value")


@jsii.data_type(jsii_type="jsii-calc.CalculatorProps")
@jsii.data_type(jsii_type="jsii-calc.CalculatorProps", jsii_struct_bases=[])
class CalculatorProps(jsii.compat.TypedDict, total=False):
"""Properties for Calculator."""
initialValue: jsii.Number
Expand Down Expand Up @@ -571,13 +571,14 @@ def __init__(self) -> None:



@jsii.data_type_optionals(jsii_struct_bases=[scope.jsii_calc_lib.MyFirstStruct])
class _DerivedStruct(scope.jsii_calc_lib.MyFirstStruct, jsii.compat.TypedDict, total=False):
anotherOptional: typing.Mapping[str,scope.jsii_calc_lib.Value]
"""This is optional."""
optionalAny: typing.Any
optionalArray: typing.List[str]

@jsii.data_type(jsii_type="jsii-calc.DerivedStruct")
@jsii.data_type(jsii_type="jsii-calc.DerivedStruct", jsii_struct_bases=[_DerivedStruct])
class DerivedStruct(_DerivedStruct):
"""A struct which derives from another struct."""
anotherRequired: datetime.datetime
Expand Down Expand Up @@ -711,7 +712,7 @@ def prop2_is_undefined(cls) -> typing.Any:
return jsii.sinvoke(cls, "prop2IsUndefined", [])


@jsii.data_type(jsii_type="jsii-calc.EraseUndefinedHashValuesOptions")
@jsii.data_type(jsii_type="jsii-calc.EraseUndefinedHashValuesOptions", jsii_struct_bases=[])
class EraseUndefinedHashValuesOptions(jsii.compat.TypedDict, total=False):
option1: str

Expand All @@ -731,7 +732,7 @@ def success(self) -> bool:
return jsii.get(self, "success")


@jsii.data_type(jsii_type="jsii-calc.ExtendsInternalInterface")
@jsii.data_type(jsii_type="jsii-calc.ExtendsInternalInterface", jsii_struct_bases=[])
class ExtendsInternalInterface(jsii.compat.TypedDict):
boom: bool

Expand Down Expand Up @@ -828,7 +829,7 @@ def struct_literal(self) -> scope.jsii_calc_lib.StructWithOnlyOptionals:
return jsii.get(self, "structLiteral")


@jsii.data_type(jsii_type="jsii-calc.Greetee")
@jsii.data_type(jsii_type="jsii-calc.Greetee", jsii_struct_bases=[])
class Greetee(jsii.compat.TypedDict, total=False):
"""These are some arguments you can pass to a method."""
name: str
Expand Down Expand Up @@ -1616,7 +1617,7 @@ def private(self, value: str):
return jsii.set(self, "private", value)


@jsii.data_type(jsii_type="jsii-calc.ImplictBaseOfBase")
@jsii.data_type(jsii_type="jsii-calc.ImplictBaseOfBase", jsii_struct_bases=[scope.jsii_calc_base.BaseProps])
class ImplictBaseOfBase(scope.jsii_calc_base.BaseProps, jsii.compat.TypedDict):
goo: datetime.datetime

Expand All @@ -1635,13 +1636,13 @@ def bar(self, value: typing.Optional[str]):
return jsii.set(self, "bar", value)


@jsii.data_type(jsii_type="jsii-calc.InterfaceInNamespaceIncludesClasses.Hello")
@jsii.data_type(jsii_type="jsii-calc.InterfaceInNamespaceIncludesClasses.Hello", jsii_struct_bases=[])
class Hello(jsii.compat.TypedDict):
foo: jsii.Number


class InterfaceInNamespaceOnlyInterface:
@jsii.data_type(jsii_type="jsii-calc.InterfaceInNamespaceOnlyInterface.Hello")
@jsii.data_type(jsii_type="jsii-calc.InterfaceInNamespaceOnlyInterface.Hello", jsii_struct_bases=[])
class Hello(jsii.compat.TypedDict):
foo: jsii.Number

Expand Down Expand Up @@ -1966,7 +1967,7 @@ def jsii_agent(cls) -> typing.Optional[str]:
return jsii.sget(cls, "jsiiAgent")


@jsii.data_type(jsii_type="jsii-calc.LoadBalancedFargateServiceProps")
@jsii.data_type(jsii_type="jsii-calc.LoadBalancedFargateServiceProps", jsii_struct_bases=[])
class LoadBalancedFargateServiceProps(jsii.compat.TypedDict, total=False):
"""jsii#298: show default values in sphinx documentation, and respect newlines."""
containerPort: jsii.Number
Expand Down Expand Up @@ -2148,10 +2149,11 @@ def change_me_to_undefined(self, value: typing.Optional[str]):
return jsii.set(self, "changeMeToUndefined", value)


@jsii.data_type_optionals(jsii_struct_bases=[])
class _NullShouldBeTreatedAsUndefinedData(jsii.compat.TypedDict, total=False):
thisShouldBeUndefined: typing.Any

@jsii.data_type(jsii_type="jsii-calc.NullShouldBeTreatedAsUndefinedData")
@jsii.data_type(jsii_type="jsii-calc.NullShouldBeTreatedAsUndefinedData", jsii_struct_bases=[_NullShouldBeTreatedAsUndefinedData])
class NullShouldBeTreatedAsUndefinedData(_NullShouldBeTreatedAsUndefinedData):
arrayWithThreeElementsAndUndefinedAsSecondArgument: typing.List[typing.Any]

Expand Down Expand Up @@ -2251,7 +2253,7 @@ def arg3(self) -> typing.Optional[datetime.datetime]:
return jsii.get(self, "arg3")


@jsii.data_type(jsii_type="jsii-calc.OptionalStruct")
@jsii.data_type(jsii_type="jsii-calc.OptionalStruct", jsii_struct_bases=[])
class OptionalStruct(jsii.compat.TypedDict, total=False):
field: str

Expand Down Expand Up @@ -2877,10 +2879,11 @@ def value(self) -> jsii.Number:
return jsii.get(self, "value")


@jsii.data_type_optionals(jsii_struct_bases=[])
class _UnionProperties(jsii.compat.TypedDict, total=False):
foo: typing.Union[str, jsii.Number]

@jsii.data_type(jsii_type="jsii-calc.UnionProperties")
@jsii.data_type(jsii_type="jsii-calc.UnionProperties", jsii_struct_bases=[_UnionProperties])
class UnionProperties(_UnionProperties):
bar: typing.Union[str, jsii.Number, "AllTypes"]

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 @@ -7,6 +7,7 @@
JSIIAbstractClass,
enum,
data_type,
data_type_optionals,
implements,
interface,
member,
Expand Down Expand Up @@ -44,6 +45,7 @@
"Number",
"enum",
"data_type",
"data_type_optionals",
"implements",
"interface",
"member",
Expand Down
11 changes: 10 additions & 1 deletion packages/jsii-python-runtime/src/jsii/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,24 @@ def deco(cls):
return deco


def data_type(*, jsii_type):
def data_type(*, jsii_type, jsii_struct_bases):
def deco(cls):
cls.__jsii_type__ = jsii_type
cls.__jsii_struct_bases__ = jsii_struct_bases
_reference_map.register_data_type(cls)
return cls

return deco


def data_type_optionals(*, jsii_struct_bases):
def deco(cls):
cls.__jsii_struct_bases__ = jsii_struct_bases
return cls

return deco


def member(*, jsii_name):
def deco(fn):
fn.__jsii_name__ = jsii_name
Expand Down
30 changes: 28 additions & 2 deletions packages/jsii-python-runtime/tests/test_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,40 @@
import pytest

from jsii.errors import JSIIError
from jsii_calc import Calculator
import jsii_calc


class TestErrorHandling:
def test_jsii_error(self):
obj = Calculator()
obj = jsii_calc.Calculator()

with pytest.raises(
JSIIError, match="Class jsii-calc.Calculator doesn't have a method"
):
jsii.kernel.invoke(obj, "nonexistentMethod")

def test_inheritance_maintained(self):
"""Check that for JSII struct types we can get the inheritance tree in some way."""
# inspect.getmro() won't work because of TypedDict, but we add another annotation
bases = find_struct_bases(jsii_calc.DerivedStruct)

base_names = [b.__name__ for b in bases]

assert base_names == ['DerivedStruct', '_DerivedStruct', 'MyFirstStruct', '_MyFirstStruct']



def find_struct_bases(x):
ret = []
seen = set([])

def recurse(s):
if s not in seen:
ret.append(s)
seen.add(s)
bases = getattr(s, '__jsii_struct_bases__', [])
for base in bases:
recurse(base)

recurse(x)
return ret

0 comments on commit 607f151

Please sign in to comment.