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

Added support for ForwardRef types #15

Merged
merged 5 commits into from
Oct 19, 2021
Merged

Conversation

mlamby
Copy link
Contributor

@mlamby mlamby commented Oct 10, 2021

This pull request is to address #14.

Added code to evaluate typing.ForwardRef types. These are generally used to
reference types that haven't been defined yet. Unfortunately to evaluate
these types you need to pass a dictionary containing all the globals to all
the validate functions so that the type can be evaluated. This has meant
that many lines had to updated just to add the additional argument.

Also had to add special handling of the Optional type to enable the
tests to pass on Python 3.9.

Added code to evaluate typing.ForwardRef types. These are generally used to
reference types that haven't been defined yet. Unfortunately to evaluate
these types you need to pass a dictionary containing all the globals to all
the validate functions so that the type can be evaluated. This has meant
that many lines had to updated just to add the additional argument.

Also had to add special handling of the Optional type to enable the
tests to pass on Python 3.9.
dataclass_type_validator/__init__.py Outdated Show resolved Hide resolved
dataclass_type_validator/__init__.py Outdated Show resolved Hide resolved
dataclass_type_validator/__init__.py Outdated Show resolved Hide resolved
dataclass_type_validator/__init__.py Outdated Show resolved Hide resolved
dataclass_type_validator/__init__.py Outdated Show resolved Hide resolved
dataclass_type_validator/__init__.py Outdated Show resolved Hide resolved
@akiray03
Copy link
Member

Thank you for your contribution.
Sorry for my late reply. Please wait a little more.

@akiray03
Copy link
Member

With your support, it now works with Python 3.9 as well. Thanks!
But, this library supports both Python 3.8 and 3.9.
Currently it does not work with Python 3.8.
The test using Python 3.8 must also pass to merge this pull request.

pytest result using Python 3.8

============================= test session starts ==============================
platform darwin -- Python 3.8.12, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: /Users/akira-yumiyama/PycharmProjects/dataclass-type-validator
collected 26 items

tests/test_validator.py .......F.F......FF........                       [100%]

=================================== FAILURES ===================================
_____ TestTypeValidationList.test_build_failure_on_array_optional_strings ______

self = <tests.test_validator.TestTypeValidationList object at 0x10577bdc0>

    def test_build_failure_on_array_optional_strings(self):
        with pytest.raises(TypeValidationError,
                           match="must be an instance of typing.List\\[typing.Optional\\[(str)\\]\\]"):
>           assert isinstance(DataclassTestList(
                array_of_numbers=[1, 2],
                array_of_strings=['abc'],
                array_of_optional_strings=[123, None]
            ), DataclassTestList)

tests/test_validator.py:107: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = DataclassTestList(array_of_numbers=[1, 2], array_of_strings=['abc'], array_of_optional_strings=[123, None])
array_of_numbers = [1, 2], array_of_strings = ['abc']
array_of_optional_strings = [123, None]

>   ???

<string>:6: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = DataclassTestList(array_of_numbers=[1, 2], array_of_strings=['abc'], array_of_optional_strings=[123, None])

    def __post_init__(self):
>       dataclass_type_validator(self)

tests/test_validator.py:72: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

target = DataclassTestList(array_of_numbers=[1, 2], array_of_strings=['abc'], array_of_optional_strings=[123, None])
strict = False

    def dataclass_type_validator(target, strict: bool = False):
        fields = dataclasses.fields(target)
        globalns = sys.modules[target.__module__].__dict__.copy()
    
        errors = {}
        for field in fields:
            field_name = field.name
            expected_type = field.type
            value = getattr(target, field_name)
    
            err = _validate_types(expected_type=expected_type, value=value, strict=strict, globalns=globalns)
            if err is not None:
                errors[field_name] = err
    
        if len(errors) > 0:
>           raise TypeValidationError(
                "Dataclass Type Validation Error", target=target, errors=errors
            )
E           dataclass_type_validator.TypeValidationError: tests.test_validator.DataclassTestList (errors = {'array_of_optional_strings': "must be an instance of typing.List[typing.Union[NoneType, str]], but there are some errors: ['must be an instance of typing.Union[NoneType, str], but received 123']"})

dataclass_type_validator/__init__.py:166: TypeValidationError

During handling of the above exception, another exception occurred:

self = <tests.test_validator.TestTypeValidationList object at 0x10577bdc0>

    def test_build_failure_on_array_optional_strings(self):
        with pytest.raises(TypeValidationError,
                           match="must be an instance of typing.List\\[typing.Optional\\[(str)\\]\\]"):
>           assert isinstance(DataclassTestList(
                array_of_numbers=[1, 2],
                array_of_strings=['abc'],
                array_of_optional_strings=[123, None]
            ), DataclassTestList)
E           AssertionError: Regex pattern 'must be an instance of typing.List\\[typing.Optional\\[(str)\\]\\]' does not match 'tests.test_validator.DataclassTestList (errors = {\'array_of_optional_strings\': "must be an instance of typing.List[typing.Union[NoneType, str]], but there are some errors: [\'must be an instance of typing.Union[NoneType, str], but received 123\']"})'.

tests/test_validator.py:107: AssertionError
__________________ TestTypeValidationUnion.test_build_failure __________________

self = <tests.test_validator.TestTypeValidationUnion object at 0x1057f7490>

    def test_build_failure(self):
        with pytest.raises(TypeValidationError, match='must be an instance of typing.Union\\[str, int\\]'):
            assert isinstance(DataclassTestUnion(
                string_or_number=None,
                optional_string=None
            ), DataclassTestUnion)
    
        with pytest.raises(TypeValidationError, match='must be an instance of typing.Optional\\[str\\]'):
>           assert isinstance(DataclassTestUnion(
                string_or_number=123,
                optional_string=123
            ), DataclassTestUnion)

tests/test_validator.py:142: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = DataclassTestUnion(string_or_number=123, optional_string=123)
string_or_number = 123, optional_string = 123

>   ???

<string>:5: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = DataclassTestUnion(string_or_number=123, optional_string=123)

    def __post_init__(self):
>       dataclass_type_validator(self)

tests/test_validator.py:120: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

target = DataclassTestUnion(string_or_number=123, optional_string=123)
strict = False

    def dataclass_type_validator(target, strict: bool = False):
        fields = dataclasses.fields(target)
        globalns = sys.modules[target.__module__].__dict__.copy()
    
        errors = {}
        for field in fields:
            field_name = field.name
            expected_type = field.type
            value = getattr(target, field_name)
    
            err = _validate_types(expected_type=expected_type, value=value, strict=strict, globalns=globalns)
            if err is not None:
                errors[field_name] = err
    
        if len(errors) > 0:
>           raise TypeValidationError(
                "Dataclass Type Validation Error", target=target, errors=errors
            )
E           dataclass_type_validator.TypeValidationError: tests.test_validator.DataclassTestUnion (errors = {'optional_string': 'must be an instance of typing.Union[str, NoneType], but received 123'})

dataclass_type_validator/__init__.py:166: TypeValidationError

During handling of the above exception, another exception occurred:

self = <tests.test_validator.TestTypeValidationUnion object at 0x1057f7490>

    def test_build_failure(self):
        with pytest.raises(TypeValidationError, match='must be an instance of typing.Union\\[str, int\\]'):
            assert isinstance(DataclassTestUnion(
                string_or_number=None,
                optional_string=None
            ), DataclassTestUnion)
    
        with pytest.raises(TypeValidationError, match='must be an instance of typing.Optional\\[str\\]'):
>           assert isinstance(DataclassTestUnion(
                string_or_number=123,
                optional_string=123
            ), DataclassTestUnion)
E           AssertionError: Regex pattern 'must be an instance of typing.Optional\\[str\\]' does not match "tests.test_validator.DataclassTestUnion (errors = {'optional_string': 'must be an instance of typing.Union[str, NoneType], but received 123'})".

tests/test_validator.py:142: AssertionError
_______________ TestTypeValidationForwardRef.test_build_success ________________

self = <tests.test_validator.TestTypeValidationForwardRef object at 0x105883f40>

    def test_build_success(self):
>       assert isinstance(DataclassTestForwardRef(
            number=1,
            ref=None,
        ), DataclassTestForwardRef)

tests/test_validator.py:233: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
<string>:5: in __init__
    ???
tests/test_validator.py:228: in __post_init__
    dataclass_type_validator(self)
dataclass_type_validator/__init__.py:161: in dataclass_type_validator
    err = _validate_types(expected_type=expected_type, value=value, strict=strict, globalns=globalns)
dataclass_type_validator/__init__.py:143: in _validate_types
    return _validate_sequential_types(expected_type=expected_type, value=value,
dataclass_type_validator/__init__.py:128: in _validate_sequential_types
    is_valid = any(_validate_types(expected_type=t, value=value, strict=strict, globalns=globalns) is None
dataclass_type_validator/__init__.py:128: in <genexpr>
    is_valid = any(_validate_types(expected_type=t, value=value, strict=strict, globalns=globalns) is None
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

expected_type = ForwardRef('DataclassTestForwardRef'), value = None
strict = False
globalns = {'@py_builtins': <module 'builtins' (built-in)>, '@pytest_ar': <module '_pytest.assertion.rewrite' from '/Users/akira-...'tests.test_validator.ChildValue'>, 'DataclassTestCallable': <class 'tests.test_validator.DataclassTestCallable'>, ...}

    def _validate_types(expected_type: type, value: Any, strict: bool, globalns: GlobalNS_T) -> Optional[str]:
        if isinstance(expected_type, type):
            return _validate_type(expected_type=expected_type, value=value)
    
        if isinstance(expected_type, typing._GenericAlias):
            return _validate_sequential_types(expected_type=expected_type, value=value,
                                              strict=strict, globalns=globalns)
    
        if isinstance(expected_type, typing.ForwardRef):
>           referenced_type = expected_type._evaluate(globalns, None, set())
E           TypeError: _evaluate() takes 3 positional arguments but 4 were given

dataclass_type_validator/__init__.py:147: TypeError
__________ TestTypeValidationForwardRef.test_build_failure_on_number ___________

self = <tests.test_validator.TestTypeValidationForwardRef object at 0x10584f2b0>

    def test_build_failure_on_number(self):
        with pytest.raises(TypeValidationError):
>           assert isinstance(DataclassTestForwardRef(
                number=1,
                ref='string'
            ), DataclassTestForwardRef)

tests/test_validator.py:244: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
<string>:5: in __init__
    ???
tests/test_validator.py:228: in __post_init__
    dataclass_type_validator(self)
dataclass_type_validator/__init__.py:161: in dataclass_type_validator
    err = _validate_types(expected_type=expected_type, value=value, strict=strict, globalns=globalns)
dataclass_type_validator/__init__.py:143: in _validate_types
    return _validate_sequential_types(expected_type=expected_type, value=value,
dataclass_type_validator/__init__.py:128: in _validate_sequential_types
    is_valid = any(_validate_types(expected_type=t, value=value, strict=strict, globalns=globalns) is None
dataclass_type_validator/__init__.py:128: in <genexpr>
    is_valid = any(_validate_types(expected_type=t, value=value, strict=strict, globalns=globalns) is None
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

expected_type = ForwardRef('DataclassTestForwardRef'), value = 'string'
strict = False
globalns = {'@py_builtins': <module 'builtins' (built-in)>, '@pytest_ar': <module '_pytest.assertion.rewrite' from '/Users/akira-...'tests.test_validator.ChildValue'>, 'DataclassTestCallable': <class 'tests.test_validator.DataclassTestCallable'>, ...}

    def _validate_types(expected_type: type, value: Any, strict: bool, globalns: GlobalNS_T) -> Optional[str]:
        if isinstance(expected_type, type):
            return _validate_type(expected_type=expected_type, value=value)
    
        if isinstance(expected_type, typing._GenericAlias):
            return _validate_sequential_types(expected_type=expected_type, value=value,
                                              strict=strict, globalns=globalns)
    
        if isinstance(expected_type, typing.ForwardRef):
>           referenced_type = expected_type._evaluate(globalns, None, set())
E           TypeError: _evaluate() takes 3 positional arguments but 4 were given

dataclass_type_validator/__init__.py:147: TypeError
=========================== short test summary info ============================
FAILED tests/test_validator.py::TestTypeValidationList::test_build_failure_on_array_optional_strings
FAILED tests/test_validator.py::TestTypeValidationUnion::test_build_failure
FAILED tests/test_validator.py::TestTypeValidationForwardRef::test_build_success
FAILED tests/test_validator.py::TestTypeValidationForwardRef::test_build_failure_on_number
========================= 4 failed, 22 passed in 0.07s =========================

@mlamby
Copy link
Contributor Author

mlamby commented Oct 18, 2021

I see now that I have mixed up two different changes in the one pull request, upgrading to python 3.9 and support ForwardRef types. I will submit a targeted pull request to cover off just the 3.9 upgrade and then will revisit this pull request.

@mlamby mlamby mentioned this pull request Oct 18, 2021
The ForwardRef._evaluate function takes 3 arguments on python 3.9 and
only 2 on 3.8.
@mlamby
Copy link
Contributor Author

mlamby commented Oct 18, 2021

Ok. The branch has been updated to support both 3.8 and 3.9.

@akiray03 akiray03 merged commit 6539183 into levii:master Oct 19, 2021
@akiray03
Copy link
Member

@mlamby v0.1.1 released! Thank you for your contribution!
https://pypi.org/project/dataclass-type-validator/0.1.1/

@mlamby
Copy link
Contributor Author

mlamby commented Oct 20, 2021

Awesome! Thanks for your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants