-
Notifications
You must be signed in to change notification settings - Fork 246
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(pacmak): allow typeguard==3.x
and typeguard==4.x
#4611
Conversation
typeguard==3.x
and typeguard==4.x
@@ -168,6 +168,10 @@ def implements(*interfaces: Type[Any]) -> Callable[[T], T]: | |||
def deco(cls): | |||
cls.__jsii_type__ = getattr(cls, "__jsii_type__", None) | |||
cls.__jsii_ifaces__ = getattr(cls, "__jsii_ifaces__", []) + list(interfaces) | |||
cls.__jsii_proxy_class__ = lambda: getattr(cls, "__jsii_proxy_class__", None) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is required because __jsii_proxy_class__
is added to protocols, and therefore should be implemented in classes.
jsii/packages/jsii-pacmak/lib/targets/python.ts
Lines 1068 to 1070 in 9ecea56
code.line( | |
'# Adding a "__jsii_proxy_class__(): typing.Type" function to the interface', | |
); |
For pacmak generated classes, __jsii_proxy_class__
is autoamtically added:
jsii/packages/jsii-pacmak/lib/targets/python.ts
Lines 1496 to 1498 in 9ecea56
code.line( | |
'# Adding a "__jsii_proxy_class__(): typing.Type" function to the abstract class', | |
); |
But in order to support user defined protocol implementations, this needs to be added via the @jsii.implements
decorator as well.
Note that prior to 4.x, typeguard
didn't actually type check our protocols so this wasn't needed.
@@ -27,17 +27,6 @@ def test_inheritance_maintained(self): | |||
|
|||
assert base_names == ["DerivedStruct", "MyFirstStruct"] | |||
|
|||
def test_descriptive_error_when_passing_function(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to test_runtime_checking.py
, where all other type checking is asserted on.
code.closeBlock(); | ||
code.openBlock('else'); | ||
code.line( | ||
'if isinstance(value, jsii._reference_map.InterfaceDynamicProxy): # pyright: ignore [reportAttributeAccessIssue]', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typeguard doesn't support type checking against dynamic proxies because it only compares against type declaration:
def check_protocol(
value: Any,
origin_type: Any,
args: tuple[Any, ...],
memo: TypeCheckMemo,
) -> None:
subject: type[Any] = value if isclass(value) else type(value)
Prior to version 4.x, this wasn't needed because typeguard
did not even attempt it. Its protocol checking logic was completely different:
def check_protocol(argname: str, value, expected_type):
# TODO: implement proper compatibility checking and support non-runtime protocols
if getattr(expected_type, '_is_runtime_protocol', False):
if not isinstance(value, expected_type):
raise TypeError('type of {} ({}) is not compatible with the {} protocol'.
format(argname, type(value).__qualname__, expected_type.__qualname__))
This code never got executed on our protocols because _is_runtime_protocol
defaults to False
, and is only set to True
if the protocol is decorated with @typing.runtime_checkable
. Since our protocols aren't decorated with it, the protocol type checking never took place.
I considered adding the
@typing.runtime_checkable
decorator to gain protocol checking with versions 2.x and 3.x, but decided against it since i'm not sure what the implications would be and it might break existing projects.
So with support for 4.x we are now adding protocol checking, but still avoiding runtime proxies because they are not handled properly by typeguard
.
@@ -28,9 +28,11 @@ | |||
"build": "cp ../../../README.md . && rm -f jsii-*.whl && npm run generate && npm run deps && npm run lint", | |||
"lint": "ts-node build-tools/venv.ts black .", | |||
"package": "package-python && package-private", | |||
"test": "npm run test:gen && npm run test:run && npm run test:types", | |||
"test": "npm run test:gen && npm run test:run:typeguard-2 && npm run test:run:typeguard-3 && npm run test:run:typeguard-4 && npm run test:types", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will now run three test suites, for each typeguard major version we support. These tests are pretty quick so compared to the current latency of PR validation, they don't add much.
code.line('from typeguard import check_type'); | ||
|
||
code.line('import typeguard'); | ||
code.line('from importlib.metadata import version as _metadata_package_version'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was added in Python 3.8, which is the lowest version of python we support - so its safe to use. Aliasing to avoid possible conflicts with other generated code.
|
||
|
||
@jsii.implements(jsii_calc.IBellRinger) | ||
class PythonInvalidBellRinger: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for 4.x we can now statically typecheck protocols so added a test for it.
|
||
|
||
@pytest.mark.skipif(TYPEGUARD_MAJOR_VERSION != 3, reason="requires typeguard 3.x") | ||
class TestRuntimeTypeCheckingTypeGuardV3: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is the primary maintenance burden we introduce with supporting multiple major versions. We have 3 classes, each testing a different major version, but containing 99% equal test cases. Note however that assertions are different because typeguard
changed both error messages and exception types.
Given that typeguard
only actively maintains a single version line (current 4.x) I think our most likely changes to this file are additional test cases that are only supported in 4.x, and therefore do not need backporting to the older test suites. If however we identify a coverage gap that applies to all major versions, we will have to add the test 3 times. I feel this possible overhead doesn't merit a new major version of pacmak, but interested in more thoughts here.
There are probably also ways to increase code sharing between the different 3 test classes that I didn't dive deeper into, but we could also investigate this route if you think its worth while.
code.openBlock('else'); | ||
code.openBlock('if TYPEGUARD_MAJOR_VERSION == 3'); | ||
code.line( | ||
'typeguard.config.collection_check_strategy = typeguard.CollectionCheckStrategy.ALL_ITEMS', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In version 3.x collection_check_strategy
can only be configured globally, while in 4.x it can be specified as an argument to check_type
.
Thank you for contributing! ❤️ I will now look into making sure the PR is up-to-date, then proceed to try and merge it! |
Merging (with squash)... |
…served (#4625) In #4611, we added the `_jsii_proxy_class__` attributes to the `@jsii.interface` implementations. This was required in order to comply with `typeguard` protocol checking. We didn't implement it correctly, accidentally overriding user defined proxy classes. ## Note I have been wrecking my brain trying to understand if this bug has any runtime implications, and I couldn't find any. #### How so? At runtime, from what I could gather, the `__jsii_proxy_class__` attribute is only used when we try to instantiate a subclass of an abstract class: https://github.com/aws/jsii/blob/dc77d6c7016bcb7531f6e374243410f969ea1fbf/packages/%40jsii/python-runtime/src/jsii/_reference_map.py#L65-L70 However, for abstract classes, we assign an explicit value to `__jsii_proxy_class__`: https://github.com/aws/jsii/blob/dc77d6c7016bcb7531f6e374243410f969ea1fbf/packages/jsii-pacmak/lib/targets/python.ts#L1496-L1501 Luckily, this happens **AFTER** the `@jsii.implements` decorator has finished, thus overriding the mistake in the decorator. Presumably, this would still be a problem for user defined abstract classes (since they don't have this assignment). However, reference resolving for user defined classes is done via native reference lookup: https://github.com/aws/jsii/blob/dc77d6c7016bcb7531f6e374243410f969ea1fbf/packages/%40jsii/python-runtime/src/jsii/_reference_map.py#L48-L54 This is also why I couldn't come up with a real life test case, and had to resort to an artificial one. --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
…served (#4625) In #4611, we added the `_jsii_proxy_class__` attributes to the `@jsii.interface` implementations. This was required in order to comply with `typeguard` protocol checking. We didn't implement it correctly, accidentally overriding user defined proxy classes. ## Note I have been wrecking my brain trying to understand if this bug has any runtime implications, and I couldn't find any. #### How so? At runtime, from what I could gather, the `__jsii_proxy_class__` attribute is only used when we try to instantiate a subclass of an abstract class: https://github.com/aws/jsii/blob/dc77d6c7016bcb7531f6e374243410f969ea1fbf/packages/%40jsii/python-runtime/src/jsii/_reference_map.py#L65-L70 However, for abstract classes, we assign an explicit value to `__jsii_proxy_class__`: https://github.com/aws/jsii/blob/dc77d6c7016bcb7531f6e374243410f969ea1fbf/packages/jsii-pacmak/lib/targets/python.ts#L1496-L1501 Luckily, this happens **AFTER** the `@jsii.implements` decorator has finished, thus overriding the mistake in the decorator. Presumably, this would still be a problem for user defined abstract classes (since they don't have this assignment). However, reference resolving for user defined classes is done via native reference lookup: https://github.com/aws/jsii/blob/dc77d6c7016bcb7531f6e374243410f969ea1fbf/packages/%40jsii/python-runtime/src/jsii/_reference_map.py#L48-L54 This is also why I couldn't come up with a real life test case, and had to resort to an artificial one. --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
Currently python projects generated by pacmak define a
typeguard
dependency range oftypeguard~=2.13.3
, which prevents users from brining newer major version oftypeguard
into their projects.This PR adds support for
typeguard==3.x
andtypguard==4.x
, which are the latest versions currently available. We intentionally do not allow an open range because every major version brings breaking changes with it that might need to be addressed.Notes
typeguard==2.x
because that would be a breaking change.Fixes #4469
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.