konradhalas / dacite

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

Support for class objects? #112

Closed electrocnic closed 3 years ago

electrocnic commented 4 years ago

Hi, nice tool!

I ran into a few problems where I am not sure whether I am using it wrong or whether dacite does not support it:

I want to define in my configuration jsons which concrete implementation classes my program should use upon loading:

{
  "impl_class": "config.TestImpl"
}

Therefore I was trying to have my dataclass expecting a type instead of a string or something:

class TestImpl:
    def funcc(self) -> int:
        return 3

@dataclass
class Configuration:
    impl_class: typing.Type[TestImpl]

converters = {
    typing.Type[TestImpl]: lambda classname: locate(classname)
}

Then I load the config file like this:

def _load_cfg_with_path(path: str, config_file: str) -> Configuration:
    with open(path + config_file) as json_file:
        as_dict = json.load(json_file)
        return from_dict(data_class=Configuration, data=as_dict, config=dacite.Config(type_hooks=converters))

however I get this error: dacite.exceptions.WrongTypeError: wrong value type for field "impl_class" - should be "typing.Type[config.TestImpl]" instead of value "<class 'config.TestImpl'>" of type "type"

My current (inconvenient) workaround is to directly instantiate "empty" classes by dacite like this:

converters = {
    # see the extra () at the end which will create an instance
    # also does not make use of typing.Type anymore because we expect the instance, not the class object.
    TestImpl: lambda classname: locate(classname)() 
}

... but I do have to provide default "None" arguments for the init() function of my TestImpl class and would seperately make a new instance by calling config.impl_class.__class__(arg1='not_none', arg2='also_not_none')

So much for this, tried to keep it as short as possible. Maybe you could tell me whether you see a "better" approach than mine? Thanks!

PS: Two more problems which might belong to their own thread, but short:

  1. My actual goal is to check for a super-type. E.g. I have an interface MyInterface and want to inject MyImplA(MyInterface) through the config. Then I would like to check the type for MyInterface and even if the concrete class is MyImplA it should work.
  2. I often run into circular dependency problems for the type checking with dacite: Not even this approach did help: https://www.stefaanlippens.net/circular-imports-type-hints-python.html The thing is, I really would love to keep the config classes and the implementation in seperated files, but this seems impossible as soon as you have custom classes in the type check or in those converter-dicts.
konradhalas commented 3 years ago

Hi @electrocnic - thank you for reporting this issue. I didn't support Type fields at all. I pushed simple fix to master so I hope it will help in your case. 🎉

electrocnic commented 3 years ago

Hi @electrocnic - thank you for reporting this issue. I didn't support Type fields at all. I pushed simple fix to master so I hope it will help in your case. 🎉

Wohoo nice! Thanks a lot. I will probably use it sometime in the next year!

Are Forward References also supported now together with the typing library? Like impl_class: typing.Type['TestImpl'] instead of impl_class: typing.Type[TestImpl] ? That could resolve the circular dependency issue... (Didn't try it, just asking)

konradhalas commented 3 years ago

I think so :)

You have to pass forward_references argument to Config class - sth like this:

@dataclass
class X:
    t: Type["Y"]

class Y:
    pass

result = from_dict(X, {"t": Y}, config=Config(forward_references={"Y": Y}))

assert result == X(t=Y)
electrocnic commented 3 years ago

perfect :)