Skip to content

Commit 607f151

Browse files
authored
fix(python): maintain inheritance chain for structs (#482)
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.
1 parent fa4d000 commit 607f151

File tree

7 files changed

+63
-20
lines changed

7 files changed

+63
-20
lines changed

packages/jsii-pacmak/lib/targets/python.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,7 @@ class TypedDict extends BasePythonClassType {
641641
// and implement this "split" class logic.
642642

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

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

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

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

676678
// In this case we either have no members, or we have all of one type, so
677679
// we'll see if we have any optional members, if we don't then we'll use

packages/jsii-pacmak/test/expected.jsii-calc-base/python/src/scope/jsii_calc_base/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def type_name(self) -> typing.Any:
3232
class _BaseProxy(Base):
3333
pass
3434

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

packages/jsii-pacmak/test/expected.jsii-calc-lib/python/src/scope/jsii_calc_lib/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,11 @@ def baz(self) -> None:
9797
return jsii.invoke(self, "baz", [])
9898

9999

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

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

112-
@jsii.data_type(jsii_type="@scope/jsii-calc-lib.StructWithOnlyOptionals")
113+
@jsii.data_type(jsii_type="@scope/jsii-calc-lib.StructWithOnlyOptionals", jsii_struct_bases=[])
113114
class StructWithOnlyOptionals(jsii.compat.TypedDict, total=False):
114115
"""This is a struct with only optional properties."""
115116
optional1: str

packages/jsii-pacmak/test/expected.jsii-calc/python/src/jsii_calc/__init__.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ def value(self) -> jsii.Number:
412412
return jsii.get(self, "value")
413413

414414

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

572572

573573

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

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

713714

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

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

733734

734-
@jsii.data_type(jsii_type="jsii-calc.ExtendsInternalInterface")
735+
@jsii.data_type(jsii_type="jsii-calc.ExtendsInternalInterface", jsii_struct_bases=[])
735736
class ExtendsInternalInterface(jsii.compat.TypedDict):
736737
boom: bool
737738

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

830831

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

16181619

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

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

16371638

1638-
@jsii.data_type(jsii_type="jsii-calc.InterfaceInNamespaceIncludesClasses.Hello")
1639+
@jsii.data_type(jsii_type="jsii-calc.InterfaceInNamespaceIncludesClasses.Hello", jsii_struct_bases=[])
16391640
class Hello(jsii.compat.TypedDict):
16401641
foo: jsii.Number
16411642

16421643

16431644
class InterfaceInNamespaceOnlyInterface:
1644-
@jsii.data_type(jsii_type="jsii-calc.InterfaceInNamespaceOnlyInterface.Hello")
1645+
@jsii.data_type(jsii_type="jsii-calc.InterfaceInNamespaceOnlyInterface.Hello", jsii_struct_bases=[])
16451646
class Hello(jsii.compat.TypedDict):
16461647
foo: jsii.Number
16471648

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

19681969

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

21502151

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

2154-
@jsii.data_type(jsii_type="jsii-calc.NullShouldBeTreatedAsUndefinedData")
2156+
@jsii.data_type(jsii_type="jsii-calc.NullShouldBeTreatedAsUndefinedData", jsii_struct_bases=[_NullShouldBeTreatedAsUndefinedData])
21552157
class NullShouldBeTreatedAsUndefinedData(_NullShouldBeTreatedAsUndefinedData):
21562158
arrayWithThreeElementsAndUndefinedAsSecondArgument: typing.List[typing.Any]
21572159

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

22532255

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

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

28792881

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

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

packages/jsii-python-runtime/src/jsii/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
JSIIAbstractClass,
88
enum,
99
data_type,
10+
data_type_optionals,
1011
implements,
1112
interface,
1213
member,
@@ -44,6 +45,7 @@
4445
"Number",
4546
"enum",
4647
"data_type",
48+
"data_type_optionals",
4749
"implements",
4850
"interface",
4951
"member",

packages/jsii-python-runtime/src/jsii/_runtime.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,24 @@ def deco(cls):
8484
return deco
8585

8686

87-
def data_type(*, jsii_type):
87+
def data_type(*, jsii_type, jsii_struct_bases):
8888
def deco(cls):
8989
cls.__jsii_type__ = jsii_type
90+
cls.__jsii_struct_bases__ = jsii_struct_bases
9091
_reference_map.register_data_type(cls)
9192
return cls
9293

9394
return deco
9495

9596

97+
def data_type_optionals(*, jsii_struct_bases):
98+
def deco(cls):
99+
cls.__jsii_struct_bases__ = jsii_struct_bases
100+
return cls
101+
102+
return deco
103+
104+
96105
def member(*, jsii_name):
97106
def deco(fn):
98107
fn.__jsii_name__ = jsii_name

packages/jsii-python-runtime/tests/test_python.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,40 @@
22
import pytest
33

44
from jsii.errors import JSIIError
5-
from jsii_calc import Calculator
5+
import jsii_calc
66

77

88
class TestErrorHandling:
99
def test_jsii_error(self):
10-
obj = Calculator()
10+
obj = jsii_calc.Calculator()
1111

1212
with pytest.raises(
1313
JSIIError, match="Class jsii-calc.Calculator doesn't have a method"
1414
):
1515
jsii.kernel.invoke(obj, "nonexistentMethod")
16+
17+
def test_inheritance_maintained(self):
18+
"""Check that for JSII struct types we can get the inheritance tree in some way."""
19+
# inspect.getmro() won't work because of TypedDict, but we add another annotation
20+
bases = find_struct_bases(jsii_calc.DerivedStruct)
21+
22+
base_names = [b.__name__ for b in bases]
23+
24+
assert base_names == ['DerivedStruct', '_DerivedStruct', 'MyFirstStruct', '_MyFirstStruct']
25+
26+
27+
28+
def find_struct_bases(x):
29+
ret = []
30+
seen = set([])
31+
32+
def recurse(s):
33+
if s not in seen:
34+
ret.append(s)
35+
seen.add(s)
36+
bases = getattr(s, '__jsii_struct_bases__', [])
37+
for base in bases:
38+
recurse(base)
39+
40+
recurse(x)
41+
return ret

0 commit comments

Comments
 (0)