diff --git a/packages/jsii-pacmak/lib/targets/python.ts b/packages/jsii-pacmak/lib/targets/python.ts index 1aba2ca687..ac9023a0ad 100644 --- a/packages/jsii-pacmak/lib/targets/python.ts +++ b/packages/jsii-pacmak/lib/targets/python.ts @@ -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 @@ -655,6 +656,7 @@ 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); @@ -662,7 +664,7 @@ class TypedDict extends BasePythonClassType { 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))) { @@ -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 diff --git a/packages/jsii-pacmak/test/expected.jsii-calc-base/python/src/scope/jsii_calc_base/__init__.py b/packages/jsii-pacmak/test/expected.jsii-calc-base/python/src/scope/jsii_calc_base/__init__.py index 745af9bc02..9c9344ba49 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc-base/python/src/scope/jsii_calc_base/__init__.py +++ b/packages/jsii-pacmak/test/expected.jsii-calc-base/python/src/scope/jsii_calc_base/__init__.py @@ -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 diff --git a/packages/jsii-pacmak/test/expected.jsii-calc-lib/python/src/scope/jsii_calc_lib/__init__.py b/packages/jsii-pacmak/test/expected.jsii-calc-lib/python/src/scope/jsii_calc_lib/__init__.py index e96c965b3e..f28e6ca8c7 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc-lib/python/src/scope/jsii_calc_lib/__init__.py +++ b/packages/jsii-pacmak/test/expected.jsii-calc-lib/python/src/scope/jsii_calc_lib/__init__.py @@ -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 @@ -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 diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/python/src/jsii_calc/__init__.py b/packages/jsii-pacmak/test/expected.jsii-calc/python/src/jsii_calc/__init__.py index 8605d25ac0..d8a33d6d1a 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/python/src/jsii_calc/__init__.py +++ b/packages/jsii-pacmak/test/expected.jsii-calc/python/src/jsii_calc/__init__.py @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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] @@ -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 @@ -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"] diff --git a/packages/jsii-python-runtime/src/jsii/__init__.py b/packages/jsii-python-runtime/src/jsii/__init__.py index 108813ac3d..5e6c190ed4 100644 --- a/packages/jsii-python-runtime/src/jsii/__init__.py +++ b/packages/jsii-python-runtime/src/jsii/__init__.py @@ -7,6 +7,7 @@ JSIIAbstractClass, enum, data_type, + data_type_optionals, implements, interface, member, @@ -44,6 +45,7 @@ "Number", "enum", "data_type", + "data_type_optionals", "implements", "interface", "member", diff --git a/packages/jsii-python-runtime/src/jsii/_runtime.py b/packages/jsii-python-runtime/src/jsii/_runtime.py index d8cc273951..bc2568eb80 100644 --- a/packages/jsii-python-runtime/src/jsii/_runtime.py +++ b/packages/jsii-python-runtime/src/jsii/_runtime.py @@ -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 diff --git a/packages/jsii-python-runtime/tests/test_python.py b/packages/jsii-python-runtime/tests/test_python.py index 1be5b4c191..f14f2912bc 100644 --- a/packages/jsii-python-runtime/tests/test_python.py +++ b/packages/jsii-python-runtime/tests/test_python.py @@ -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 \ No newline at end of file