FelixTheC / strongtyping

Decorator which checks whether the function is called with the correct type of parameters.
https://pypi.org/project/strongtyping/
107 stars 3 forks source link

Cannot validate typing of nested TypedDict having a NotRequired clause in Python 3.10 #108

Closed frallain closed 1 year ago

frallain commented 1 year ago

I want to validate a dictionary with a key whose value is another dictionary. Therefore, I have a parent TypedDict with a key targeting another TypedDict.

The thing is, I am using the NotRequired clause, which is available in Python 3.10 through the typing_extensions package (4.4.0 being the latest version for now). As seen at https://peps.python.org/pep-0655/#usage-in-python-3-11 , in Python 3.10, when using the NotRequired clause, it is recommended to use the TypedDict class from typing_extensions

from typing_extensions import TypedDict
# instead of
from typing import TypedDict

But trying to validate this dict with this configuration gives me the following error:

root@340bf78f3c82:/$ python script.py 
Traceback (most recent call last):
  File "/f/.local/lib/python3.10/site-packages/strongtyping/strong_typing_utils.py", line 489, in check_type
    is_instance = isinstance(argument, type_of) or argument == type_of
  File "/f/.local/lib/python3.10/site-packages/typing_extensions.py", line 671, in _check_fails
    raise TypeError('TypedDict does not support instance and class checks')
TypeError: TypedDict does not support instance and class checks

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/f/src/script.py", line 17, in <module>
    ParentType(parent)
  File "/f/.local/lib/python3.10/site-packages/strongtyping/strong_typing.py", line 182, in __call__
    if not checking_typing_typedict_values(arguments, self.__annotations__, self.__total__):
  File "/f/.local/lib/python3.10/site-packages/strongtyping/strong_typing_utils.py", line 268, in checking_typing_typedict_values
    return all(check_type(args.get(key), val) for key, val in required_types.items())
  File "/f/.local/lib/python3.10/site-packages/strongtyping/strong_typing_utils.py", line 268, in <genexpr>
    return all(check_type(args.get(key), val) for key, val in required_types.items())
  File "/f/.local/lib/python3.10/site-packages/strongtyping/strong_typing_utils.py", line 491, in check_type
    return isinstance(argument, type_of._subs_tree()[1:])
  File "/f/.local/lib/python3.10/site-packages/strongtyping/strong_typing.py", line 136, in __getattr__
    return getattr(self.cls, item)
AttributeError: type object 'ChildType' has no attribute '_subs_tree'

Python Version used Python 3.10.4 (main, May 28 2022, 13:25:38) [GCC 10.2.1 20210110] on linux

Package Version used strongtyping==3.10.3

Addon in use strongtyping_modules [yes/no] : no

To Reproduce Steps to reproduce the behavior:

  1. Run a container of this docker image python:3.10.4-slim-bullseye
    docker run -it --rm python:3.10.4-slim-bullseye bash
  2. In this container, install the following :
    pip install typing-extensions==4.4.0
    pip install strongtyping==3.10.3
  3. In this container, run the following script:
    
    from typing_extensions import NotRequired, TypedDict
    from strongtyping.strong_typing import match_class_typing

@match_class_typing class ChildType(TypedDict): key_a: int key_b: int

@match_class_typing class ParentType(TypedDict): child: ChildType not_required: NotRequired[int]

parent = {"child": {"key_a": 1, "key_b": 2}} ParentType(parent)


**Expected behavior**
Should exit without error.

**Desktop (please complete the following information):**
 - OS: 
 ```bash
$ uname -a
Linux c 5.13.0-25-generic #26~20.04.1-Ubuntu SMP Fri Jan 7 16:27:40 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

Additional context

It is to note that the following works as expected though:

from typing import TypedDict
from typing_extensions import NotRequired
from strongtyping.strong_typing import match_class_typing

@match_class_typing
class ChildType(TypedDict):
    key_a: int
    key_b: int

@match_class_typing
class ParentType(TypedDict):
    child: ChildType
    not_required: NotRequired[int]

parent = {"child": {"key_a": 1, "key_b": 2}}
ParentType(parent)

But then, in this case, the NotRequired clause is not respected, the following will error with a TypeMisMatch error, and it should not:

parent = {"not_required": 3, "child": {"key_a": 1, "key_b": "2"}}
ParentType(parent)
Incorrect parameter: `{'child': {'key_a': 1,
           'key_b': '2'},
 'not_required': 3}`
    required: {'child': <strongtyping.strong_typing.MatchTypedDict object at 0x7f169c29eb90>, 'not_required': typing_extensions.NotRequired[int]}
Traceback (most recent call last):
  File "/f/src/script.py", line 24, in <module>
    ParentType(parent)
  File "/f/.local/lib/python3.10/site-packages/strongtyping/strong_typing.py", line 183, in __call__
    raise self.excep_raise(self.create_error_msg(arguments))
strongtyping.strong_typing_utils.TypeMisMatch

It is an edge case, but I thought it was worth reporting it because NotRequired brings a lot for typing. Thanks for your work!

FelixTheC commented 1 year ago

Thanks for finding this, I also think that NotRequired brings a lot for the typing. I will see that I can fix this as fast as possible. The same for https://github.com/FelixTheC/strongtyping/issues/109

FelixTheC commented 1 year ago

I think this should raise an error, have a look at key_b

parent = {"not_required": 3, "child": {"key_a": 1, "key_b": "2"}}
ParentType(parent)
FelixTheC commented 1 year ago

This new version should fix this issue pip install strongtyping==3.10.4