feat(pacmak): allow typeguard==3.x and typeguard==4.x #4611
feat(pacmak): allow typeguard==3.x and typeguard==4.x #4611mergify[bot] merged 20 commits intomainfrom
typeguard==3.x and typeguard==4.x #4611Conversation
typeguard==3.x and typeguard==4.x
| 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.
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
For pacmak generated classes, __jsii_proxy_class__ is autoamtically added:
jsii/packages/jsii-pacmak/lib/targets/python.ts
Lines 1496 to 1498 in 9ecea56
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.
|
|
||
| assert base_names == ["DerivedStruct", "MyFirstStruct"] | ||
|
|
||
| def test_descriptive_error_when_passing_function(self): |
There was a problem hiding this comment.
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.
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_checkabledecorator 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.
| "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.
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.
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.
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.
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.
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
This reverts commits 937e204 and cba7046. After investigation, the user was correct to question these changes. PR aws#4986 (commit e9b53ec) pinned typeguard to 2.13.3 specifically because "Versions 3.0 and higher of typeguard can't check the jsii protocol interfaces and produce a noisy warning" (aws/constructs#2825). The root cause: jsii generates Python Protocol interfaces (IRole, IVpc, etc.) that are NOT marked with @runtime_checkable. Typeguard 3.x/4.x strictly enforces this, producing warnings like "Typeguard cannot check the IRole protocol because it is a non-runtime protocol." In typeguard 4.3.0+, some cases produce errors that break deployments (aws#4669). While PR aws#4611 (August 2024) added runtime version detection and the __protocol_attrs__ workaround, this does NOT eliminate the warnings. Simply reverting PR aws#4986 without addressing the underlying Protocol checking issue was incorrect. To properly support modern typeguard, we would need to either: 1. Mark all generated Protocol interfaces as @runtime_checkable, or 2. Skip type checking for Protocol interfaces entirely, or 3. Wait for a typeguard upstream fix Until one of these approaches is implemented, the pin to typeguard==2.13.3 should remain. Co-authored-by: qodfathr <16322190+qodfathr@users.noreply.github.com>
Currently python projects generated by pacmak define a
typeguarddependency range oftypeguard~=2.13.3, which prevents users from brining newer major version oftypeguardinto their projects.This PR adds support for
typeguard==3.xandtypguard==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.xbecause 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.