aws / jsii

jsii allows code in any language to naturally interact with JavaScript classes. It is the technology that enables the AWS Cloud Development Kit to deliver polyglot libraries from a single codebase!
https://aws.github.io/jsii
Apache License 2.0
2.59k stars 242 forks source link

Python class (Interface) is generated out of order #4426

Open automartin5000 opened 4 months ago

automartin5000 commented 4 months ago

Describe the bug

I have a CDK construct library which has been working fine. But suddenly, I think possibly with the addition of named exports, I'm getting an error when attempting to import a construct when it attempts to reference one of the interfaces in a Python CDK app. Looking at the generated Python code, I noticed that the reference occurs before the class is defined.

Here's an example of how the interfaces are defined. Note: These are not really empty.

import { StackProps } from "aws-cdk-lib";

export interface BaseProps {}

export interface SomeAdditionalProps extends SomeProps {} //This may not be relevant

export interface AllTheProps extends StackProps, BaseProps {}

And here's the generated code:

@jsii.data_type(
    jsii_type="@scope/package/moduleName.SomeAdditionalProps",
    jsii_struct_bases=[BaseProps],
    name_mapping={}
)

class SomeAdditionalProps(BaseProps):
    def __init__()....

## A couple hundred lines later
class BaseProps:

Stack trace:

line 133, in <module>
    jsii_struct_bases=[BaseProps],
                       ^^^^^^^^^^^^^^^
NameError: name 'BaseProps' is not defined. Did you mean: ....

Expected Behavior

CDK Synth correctly

Current Behavior

Synth error (See above)

Reproduction Steps

See above code

Possible Solution

No response

Additional Information/Context

No response

SDK version used

5.3.12

Environment details (OS name and version, etc.)

Mac OS Sonoma

automartin5000 commented 4 months ago

So based on my testing, what seems to be happening is JSII is generating classes in alphabetical order (is that correct?). But if Class A depends on Class B, then Class B is not yet defined and it throws an error. So I was able to work around this problem by renaming Class B to something like Class AppB so it went before the original Class A.

Does this seem like expected behavior? If so, I would expect that some dependency resolution is needed.

automartin5000 commented 4 months ago

I just ran into this again today. I can't possibly be the only person experiencing this?

otaviomacedo commented 2 months ago

Hi, @automartin5000. I can't reproduce the bug based on the information you provided. Is there a complete example you can share?

JSII is generating classes in alphabetical order

jsii generates classes in topological order, where class A is a predecessor of class B if A depends on B in some way (being a subclass is one example). This is done precisely to avoid this issue. The root cause must be something else.

github-actions[bot] commented 1 month ago

This issue has not received a response in a while. If you want to keep this issue open, please leave a comment below and auto-close will be canceled.

automartin5000 commented 1 month ago

Thanks for the response @otaviomacedo. I'll try to recreate it this week or next

automartin5000 commented 1 month ago

Sorry for the delay, here's a clean example:

constructs.ts

import { Construct } from "constructs";
import { AProps } from "./interfaces";

export class TestConstruct extends Construct {
  constructor(scope: Construct, id: string, props: AProps) {
    console.log(`Initialized prop: ${props.testProp}`);
    super(scope, id);
  }
}

interfaces.ts

export interface AProps extends BProps {}

export interface BProps {
  readonly testProp: string;
}

Python app code:

from <construct_library>.test_construct import AProps
from aws_cdk import App

class TestConstruct:
    def __init__(self, scope, id, props: AProps):
        super().__init__(scope, id, props)

test = TestConstruct(App(), 'TestConstruct', AProps(test_prop='test'))

Error:

_init__.py", line 20, in <module>
    jsii_struct_bases=[BProps],
                       ^^^^^^
NameError: name 'BProps' is not defined

Full JSII generated code (anonymized):

import abc
import builtins
import datetime
import enum
import typing

import jsii
import publication
import typing_extensions

from typeguard import check_type

from .._jsii import *

import constructs as _constructs_77d1e7e8

@jsii.data_type(
    jsii_type="<construct_library>.testConstruct.AProps",
    jsii_struct_bases=[BProps],
    name_mapping={"test_prop": "testProp"},
)
class AProps(BProps):
    def __init__(self, *, test_prop: builtins.str) -> None:
        '''
        :param test_prop: 
        '''
        if __debug__:
            type_hints = typing.get_type_hints(_typecheckingstub__6c50750a3eed61a5e3bf6110860acdf626b4cbe1cbe7b2cee25f235971070407)
            check_type(argname="argument test_prop", value=test_prop, expected_type=type_hints["test_prop"])
        self._values: typing.Dict[builtins.str, typing.Any] = {
            "test_prop": test_prop,
        }

    @builtins.property
    def test_prop(self) -> builtins.str:
        result = self._values.get("test_prop")
        assert result is not None, "Required property 'test_prop' is missing"
        return typing.cast(builtins.str, result)

    def __eq__(self, rhs: typing.Any) -> builtins.bool:
        return isinstance(rhs, self.__class__) and rhs._values == self._values

    def __ne__(self, rhs: typing.Any) -> builtins.bool:
        return not (rhs == self)

    def __repr__(self) -> str:
        return "AProps(%s)" % ", ".join(
            k + "=" + repr(v) for k, v in self._values.items()
        )

@jsii.data_type(
    jsii_type="<construct_library>.testConstruct.BProps",
    jsii_struct_bases=[],
    name_mapping={"test_prop": "testProp"},
)
class BProps:
    def __init__(self, *, test_prop: builtins.str) -> None:
        '''
        :param test_prop: 
        '''
        if __debug__:
            type_hints = typing.get_type_hints(_typecheckingstub__a410f745cf17b952ca7c740989abf5daa91608073357db663df598089ac271c0)
            check_type(argname="argument test_prop", value=test_prop, expected_type=type_hints["test_prop"])
        self._values: typing.Dict[builtins.str, typing.Any] = {
            "test_prop": test_prop,
        }

    @builtins.property
    def test_prop(self) -> builtins.str:
        result = self._values.get("test_prop")
        assert result is not None, "Required property 'test_prop' is missing"
        return typing.cast(builtins.str, result)

    def __eq__(self, rhs: typing.Any) -> builtins.bool:
        return isinstance(rhs, self.__class__) and rhs._values == self._values

    def __ne__(self, rhs: typing.Any) -> builtins.bool:
        return not (rhs == self)

    def __repr__(self) -> str:
        return "BProps(%s)" % ", ".join(
            k + "=" + repr(v) for k, v in self._values.items()
        )

class TestConstruct(
    _constructs_77d1e7e8.Construct,
    metaclass=jsii.JSIIMeta,
    jsii_type="<construct_library>.testConstruct.TestConstruct",
):
    def __init__(
        self,
        scope: _constructs_77d1e7e8.Construct,
        id: builtins.str,
        *,
        test_prop: builtins.str,
    ) -> None:
        '''
        :param scope: -
        :param id: -
        :param test_prop: 
        '''
        if __debug__:
            type_hints = typing.get_type_hints(_typecheckingstub__6b4fdcd0c8c3aad835d0884ffd49ae763492010eba1e1acc2290e07ff8485088)
            check_type(argname="argument scope", value=scope, expected_type=type_hints["scope"])
            check_type(argname="argument id", value=id, expected_type=type_hints["id"])
        props = AProps(test_prop=test_prop)

        jsii.create(self.__class__, self, [scope, id, props])

__all__ = [
    "AProps",
    "BProps",
    "TestConstruct",
]

publication.publish()

def _typecheckingstub__6c50750a3eed61a5e3bf6110860acdf626b4cbe1cbe7b2cee25f235971070407(
    *,
    test_prop: builtins.str,
) -> None:
    """Type checking stubs"""
    pass

def _typecheckingstub__a410f745cf17b952ca7c740989abf5daa91608073357db663df598089ac271c0(
    *,
    test_prop: builtins.str,
) -> None:
    """Type checking stubs"""
    pass

def _typecheckingstub__6b4fdcd0c8c3aad835d0884ffd49ae763492010eba1e1acc2290e07ff8485088(
    scope: _constructs_77d1e7e8.Construct,
    id: builtins.str,
    *,
    test_prop: builtins.str,
) -> None:
    """Type checking stubs"""
    pass