python-attrs / cattrs

Composable custom class converters for attrs, dataclasses and friends.
https://catt.rs
MIT License
806 stars 111 forks source link

Weird output while structuring unions with custom disambiguation function #267

Open pritsh opened 2 years ago

pritsh commented 2 years ago

Description

Thanks for this amazing library !!

I am trying to write disambiguation function to handle union type based on the default value of child class but got into weird error, also want to understand the correct way to do it with cattrs.


@define(kw_only=True)
class GenType:
    pass

@define(kw_only=True)
class TypeOne(GenType):
    id:Literal["A"] = "A"

@define(kw_only=True)
class TypeTwo(GenType):
   id:Literal["B"] = "B"

UniType = Union[TypeOne, TypeTwo]

@define(kw_only=True)
class Container:
    types: List[UniType]

def discriminate_model_types(value: typing.Any, _klass: typing.Type) -> UniType:
    # Write some logic to find the exact type !!, I was wondering if there is correct pythonic version to do this with cattrs.
    for child_cls in GenType.__subclasses__():
        if value["id"] == child_cls().id:
            return cattr.structure(value, child_cls)

def run_union_logic():
    json = {"types":[{"id":"A"}]}
    cattr.register_structure_hook(UniType, discriminate_model_types)
    obj =cattr.structure(json, Container)
    print(obj)

    json = {"id": "A"}
    obj = cattr.structure(json, TypeOne)
    print(obj)

#Prints following:
#Container(types=[TypeOne()]) --> I dont know why property 'id' is not populated here. 
#TypeOne(id='A') 
AdrianSosic commented 1 year ago

Hi, I just stumbled across this issue because I noticed the same problem. While I don't know what exactly causes the problem, I know at least why you get this result. The reason is because the original classes are still contained in the list returned by GenType.__subclasses__(). (Weirdly enough, the problem with your code example occurs for me only for class TypeTwo, but I experienced similar behavior in my own code).

What I get as an output is [__main__.TypeOne, __main__.TypeTwo, __main__.TypeTwo], where you can see that the class appears twice (in your case, you'll probably also see the same for TypeOne, causing your error). If you select from the list the second version of the class, your example will probably work.

However, I don't know the root cause for the problem, but it might have something to do with garbage collection!? On the attrs page, there is a hint related to slotted classes:

image

which points to this issue: https://github.com/python-attrs/attrs/issues/407

Also, it seems somehow related to the Union call because in my cases the duplicates only appeared after the call. Any idea here?

Anyway, the clean way to do it is described here: #140 Hope this helps 👍