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): maintain inheritance chain for structs #482

Merged
merged 1 commit into from
Apr 23, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
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}])`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the _ for here?

Copy link
Contributor Author

@rix0rrr rix0rrr Apr 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every struct gets compiled into 2 classes:

class _MyStruct(...):
  ...

class MyStruct(_MyStruct):
  ...

One holds the required fields, the other one the optional fields.

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