konradhalas / dacite

Simple creation of data classes from dictionaries.
MIT License
1.72k stars 106 forks source link

Empty tuple cannot be used for Sequence type #168

Closed mcgibbon closed 1 year ago

mcgibbon commented 2 years ago

Reproducer:

import dacite
import dataclasses
from typing import Sequence

@dataclasses.dataclass
class MyClass:
    names: Sequence[str]

direct = MyClass(())
from_dict = dacite.from_dict(data_class=MyClass, data={"names": ()})
assert direct == from_dict

I expect this to run without error. Instead I get:

$ python reproducer.py
Traceback (most recent call last):
  File "reproducer.py", line 11, in <module>
    from_dict = dacite.from_dict(data_class=MyClass, data={"names": ()})
  File "/Users/jeremym/opt/anaconda3/envs/fv3net/lib/python3.8/site-packages/dacite/core.py", line 68, in from_dict
    raise WrongTypeError(field_path=field.name, field_type=field.type, value=value)
dacite.exceptions.WrongTypeError: wrong value type for field "names" - should be "typing.Sequence[str]" instead of value "(None,)" of type "tuple

The issue seems to be due to this behavior inside from_dict:

>>> _build_value(type_=Sequence[str], data=(), config=Config())
(None,)

I would expect _build_value to return an empty tuple.

However, I'm also noticing that is_instance((), Sequence[str]) gives False, so maybe there's something I'm missing about empty tuples being able to be typed as Sequences in dacite.

mcgibbon commented 2 years ago

Might be worth noting that if I replace the tuples with empty lists, the code runs without an error.

konradhalas commented 2 years ago

Hi @mcgibbon, thank you for your report!

Short answer is: empty tuple has Tuple[()] type and TBH I don't know if this is a subtype of Sequence[str]

Tuple has a little bit different typing notation:

I don't know how to combine it with non-tuple syntax like Sequence[str]. I will think about it.

mcgibbon commented 2 years ago

I believe Tuples are Sequences, and Tuple[type, ...] is a Sequence[type]. You can't live isinstance when passing the generic type definitions, but:

>>> isinstance((), typing.Sequence)
True

And mypy generally doesn't complain about my code when I pass a Tuple[type, ...] as a Sequence[type]. Here's another test:

$ cat mypy_test.py
from typing import Tuple, Sequence

def foo(a: Sequence[str]):
    pass

a: Tuple[()] = ()
foo(a)

$ mypy mypy_test.py
Success: no issues found in 1 source file