seandstewart / typical

Typical: Fast, simple, & correct data-validation using Python 3 typing.
https://python-typical.org
MIT License
183 stars 9 forks source link

float should not been converted to int with Union[float, int] #167

Closed xbanke closed 3 years ago

xbanke commented 3 years ago

Description

import typic, typing

@typic.klass
class Foo:
    a: typing.Union[float, int] = 5

Foo(0.3)  # Foo(a=0)
seandstewart commented 3 years ago

Hey @xbanke -

This isn't the behavior I'm seeing:

>>> import typic, typing
... 
... 
... @typic.klass
... class Foo:
...     a: typing.Union[float, int] = 5
... 
... 
>>> Foo(0.3)
Foo(a=0.3)

JFYI - this will result in every value becoming a float, since float() will never raise an error.

You can see what the deserializer for a given Union will look like:

>>> import typic, typing
>>> proto = typic.protocol(typing.Union[float, int])
>>> print(proto.transmute.__raw__)

def deserializer__113127302304386461(val):
    _, val = __eval(val) if isinstance(val, (str, bytes)) else (False, val)
    vtype = val.__class__
    try:
        return float_des(val)
    except (TypeError, ValueError, KeyError):
        pass
    try:
        return int_des(val)
    except (TypeError, ValueError, KeyError):
        pass
    raise ValueError(f"Value could not be deserialized into one of {names}: {val!r}")

If you're seeing all floats converted to ints, this is likely because you have a Union[int, float] notated somewhere. As stated in the documentation, Union[int, float] and Union[float, int] have the same hash value, so will cause a hash collision.

xbanke commented 3 years ago

Yes, @seandstewart , there really existes Union[int, float]. By the __raw__ function's defination, it tries to convert the given value sequentially, even if the value is just an instance of one of the union types but not the first one. I personally think this is not suitable. At least it should try to check the type firstly, and then try to convert one by one. The following code shows the weird behavior.

import typic, typing

@typic.klass
class Foo:
    a: typing.Union[int, str] = 5

Foo('3.2')  # Foo(a=3)
xbanke commented 3 years ago

In order to support subclasses, it's better to use issubclass.

def deserializer_5165268981554100199(val):
    _, val = __eval(val) if isinstance(val, (str, bytes)) else (False, val)
    vtype = val.__class__
    if issubclass(vtype, types):
        return val
    try:
        return int_des(val)
    except (TypeError, ValueError, KeyError):
        pass
    try:
        return float_des(val)
    except (TypeError, ValueError, KeyError):
        pass
    raise ValueError(f"Value could not be deserialized into one of {names}: {val!r}")