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

feat(python): support inheritance-based interface implementation #3350

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from

Conversation

RomainMuller
Copy link
Contributor

@RomainMuller RomainMuller commented Jan 24, 2022

The code generator used to represent jsii interfaces as python
Protocols, however this mechanism is normally intended to allow
structural typing in python. Python Protocol types are declared using
a custom metaclass, which makes them tricky to inherit from without
running into metaclass conflicts (especially in cases where multiple
inheritance involving a Protocol and a non-Protocol base class).

The jsii runtime does not perform structural typing, and hence
interfaces are better modelled as abstract base classes that only
declare abstract members.

This, together with a minor modification to the @jsii.interface
decorator, allows interfaces to be "implemented" by simply adding them
to the implementor's inheritance chain, as is "natural" to do in Python
in such cases.

The "legacy" @jsii.implements approach to implementing interfaces is
no longer recommended, however it is still supported as it is necessary
when dealing with libraries generated by older versions of
jsii-pacmak. A Python warning will be emitted when the decorator is
used with interfaces that should instead be inherited.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

The code generator used to represent jsii interfaces as python
`Protocols`, however this mechanism is normally intended to allow
structural typing in python. Python `Protocol` types are declared using
a custom metaclass, which makes them tricky to inherit from without
running into metaclass conflicts (especially in cases where multiple
inheritance involving a `Protocol` and a non-`Protocol` base class).

The jsii runtime does not perform structural typing, and hence
interfaces are better modelled as abstract base classes that only
declare abstract members.

This, together with a minor modification to the `@jsii.interface`
decorator, allows interfaces to be "implemented" by simply adding them
to the implementor's inheritance chain, as is "natural" to do in Python
in such cases.

The "legacy" `@jsii.implements` approach to implementing interfaces is
no longer recommended, however it is still supported as it is necessary
when dealing with libraries generated by older versions of
`jsii-pacmak`.
@RomainMuller RomainMuller requested a review from a team January 24, 2022 15:00
@RomainMuller RomainMuller self-assigned this Jan 24, 2022
@mergify mergify bot added the contribution/core This is a PR that came from AWS. label Jan 24, 2022
@mergify
Copy link
Contributor

mergify bot commented Jan 24, 2022

The title of this Pull Request does not conform with [Conventional Commits] guidelines. It will need to be adjusted before the PR can be merged.
[Conventional Commits]: https://www.conventionalcommits.org

@RomainMuller
Copy link
Contributor Author

I am leaving this as a draft until I was able to validate that this does not have surprising consequences on a mixed codebase (i.e: aws-cdk built with the updated jsii-pacmak, depending on constructs built with the older jsii-pacmak).

Copy link
Contributor

@eladb eladb left a comment

Choose a reason for hiding this comment

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

So much nicer!

Comment on lines 1413 to 1415
...this.interfaces.map((i) =>
toTypeName(i).pythonType({ ...context, typeAnnotation: false }),
),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I realized this particular part of the change would break if the implemented interface is from a library that was generated with the typing_extensions.Protocol metaclass.

Not putting this change in means the type checkers will not accept instances of classes where implementations of interfaces are expected (because typing_extensions.Protocol is what causes structural typing to apply).

I need to find a strategy to be backwards-compatible here, either by conditionally extending interfaces IIF they are not typing_extensions.Protocol types, or by making type-checkers understand the interface is implemented implicitly in all cases.

@mergify
Copy link
Contributor

mergify bot commented Jan 26, 2022

The title of this Pull Request does not conform with [Conventional Commits] guidelines. It will need to be adjusted before the PR can be merged.
[Conventional Commits]: https://www.conventionalcommits.org

@mergify
Copy link
Contributor

mergify bot commented Jan 26, 2022

The title of this Pull Request does not conform with [Conventional Commits] guidelines. It will need to be adjusted before the PR can be merged.
[Conventional Commits]: https://www.conventionalcommits.org

@RomainMuller
Copy link
Contributor Author

Okay looks like this is working at this stage. Mypy is not reporting any type checking errors anymore at this stage, and a cursory test appears to work (still got to make something a little more meaningful).

@mergify
Copy link
Contributor

mergify bot commented Jan 28, 2022

The title of this Pull Request does not conform with [Conventional Commits] guidelines. It will need to be adjusted before the PR can be merged.
[Conventional Commits]: https://www.conventionalcommits.org

@RomainMuller RomainMuller marked this pull request as ready for review January 28, 2022 15:38
@RomainMuller RomainMuller requested a review from a team January 31, 2022 16:03
@MrArnoldPalmer MrArnoldPalmer added the pr/do-not-merge This PR should not be merged at this time. label Jan 31, 2022
@MrArnoldPalmer
Copy link
Contributor

added pr/do-not-merge after discussing possible breakage when an existing construct library depends on a package that then will be generated with the new python code gen. We only explicitly built for the reverse case when a newly generated library depends on one generated with the old style inheritance.

@RomainMuller RomainMuller force-pushed the main branch 2 times, most recently from 7c847b6 to 27c610d Compare May 20, 2022 11:50
Copy link
Contributor

@comcalvi comcalvi left a comment

Choose a reason for hiding this comment

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

I'm not sure why this hasn't been reviewed in a while, it looks really good. I have only two questions

Comment on lines +151 to +152
# Un-set __jsii_class__ as this is an interface, and not a class.
iface.__jsii_class__ = None
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way to not set this at all?

*
* @returns the de-duplicated list of interface types.
*/
private deduplicatedInterfaces(
Copy link
Contributor

Choose a reason for hiding this comment

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

This is really nice, good catch!

@@ -636,10 +638,7 @@ abstract class BaseMethod implements PythonBase {
}

if (decorators.length > 0) {
// "# type: ignore[misc]" needed because mypy does not know how to check decorated declarations
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this removed because of the PR that introduces pyright?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It appears mypy is now able to check most of these.

@polothy
Copy link
Contributor

polothy commented May 15, 2024

Is it possible to pick this up again? The IDE warnings/errors of types not matching in Python is not delightful 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contribution/core This is a PR that came from AWS. pr/do-not-merge This PR should not be merged at this time.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants