argparse_dataclass icon indicating copy to clipboard operation
argparse_dataclass copied to clipboard

ValueError when using `from __future__ import annotations`

Open isbadawi opened this issue 2 years ago • 3 comments
trafficstars

With the below script, I get an exception:

from __future__ import annotations
from argparse_dataclass import dataclass

@dataclass
class MyArgs:
    foo: int = 0
    bar: str = 'hello'
    baz: bool = False

def main():
    print(MyArgs.parse_args())

if __name__ == '__main__':
    main()
$ python3 targ.py
Traceback (most recent call last):
  File "/Users/ibadawi/targ.py", line 4, in <module>
    @dataclass
     ^^^^^^^^^
  File "/Users/ibadawi/Library/Python/3.11/lib/python/site-packages/argparse_dataclass.py", line 474, in dataclass
    return wrap(cls)
           ^^^^^^^^^
  File "/Users/ibadawi/Library/Python/3.11/lib/python/site-packages/argparse_dataclass.py", line 465, in wrap
    cls.parse_args = staticmethod(ArgumentParser(cls).parse_args)
                                  ^^^^^^^^^^^^^^^^^^^
  File "/Users/ibadawi/Library/Python/3.11/lib/python/site-packages/argparse_dataclass.py", line 423, in __init__
    _add_dataclass_options(options_class, self)
  File "/Users/ibadawi/Library/Python/3.11/lib/python/site-packages/argparse_dataclass.py", line 374, in _add_dataclass_options
    parser.add_argument(*args, **kwargs)
  File "/usr/local/Cellar/[email protected]/3.11.2_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/argparse.py", line 1448, in add_argument
    raise ValueError('%r is not callable' % (type_func,))
ValueError: 'int' is not callable

Removing from __future__ import annotations works fine and prints MyArgs(foo=0, bar='hello', baz=False) as expected.

It looks like the types as returned by dataclasses.fields() are just strings in this case. I'm not super familiar with writing code to process annotations but a workaround might be to use typing.get_type_hints() instead of dataclasses.fields() to get at the field types:

typing.get_type_hints(MyArgs):

{'bar': <class 'str'>, 'baz': <class 'bool'>, 'foo': <class 'int'>}

dataclasses.fields(MyArgs):

(Field(name='foo',type='int',default=0,default_factory=<dataclasses._MISSING_TYPE object at 0x10b1d5610>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
 Field(name='bar',type='str',default='hello',default_factory=<dataclasses._MISSING_TYPE object at 0x10b1d5610>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),
 Field(name='baz',type='bool',default=False,default_factory=<dataclasses._MISSING_TYPE object at 0x10b1d5610>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD))

isbadawi avatar May 04 '23 20:05 isbadawi

I encountered the same issue. By patching argparse_dataclass.fields() based on @isbadawi's idea, I could solve the error.

from __future__ import annotations
from dataclasses import fields
import argparse_dataclass
from typing import get_type_hints

def _patch_fields(cls, *args, **kwargs):
    t = get_type_hints(cls)
    def _update(_f):
        _f.type = t[_f.name]
        return _f
    
    return tuple(_update(f) for f in fields(cls, *args, **kwargs))

argparse_dataclass.fields = _patch_fields


@argparse_dataclass.dataclass
class MyArgs:
    foo: int = 0
    bar: str = 'hello'
    baz: bool = False

def main():
    print(MyArgs.parse_args())

if __name__ == '__main__':
    main()
MyArgs(foo=0, bar='hello', baz=False)

ymd-h avatar Jun 06 '23 15:06 ymd-h

See https://github.com/lovasoa/marshmallow_dataclass/pull/186 for a solution to a similar problem.

Personally I never do this import because it has caused problems in the past with other libraries but I would be willing to accept a PR that addresses this.

mivade avatar Jun 06 '23 16:06 mivade

@mivade Thank you for your reply.

I sent PR (#51).

ymd-h avatar Jun 06 '23 22:06 ymd-h