cattrs icon indicating copy to clipboard operation
cattrs copied to clipboard

Automatic disambiguation fails for Union[BaseClass, SubClass].

Open kevin-d-omara opened this issue 4 years ago • 2 comments

  • cattrs version: 0.9.0
  • Python version: 3.6.9
  • Operating System: Red Hat Enterprise Linux Server release 5.3 (Tikanga)

Description

I tried to structure data for a SubClass into a typing.Union[BaseClass, SubClass], but got a TypeError because it tried to instantiate a BaseClass when it should have created a SubClass.

The root cause is that when I called structure(data, typing.Union[BaseClass, SubClass]), it was actually calling structure(data, BaseClass), because typing.Union ignores the subclass -- "When a class and its subclass are present, the latter is skipped". (docs)

The cattr docs advertise it can structure typing.Union's of supported attrs classes, given that all of the classes have a unique field., but when dealing with unions of subclasses it doesn't work because a baseclass and subclass can't be in the same typing.Union.

Do you have any suggestions on how to get the automatic disambiguator to work with a union of a subclass and baseclass? One idea is for structure() to accept a frozenset of types as an alternative to a typing.Union.

What I Did

Here's the minimum code necessary to reproduce the problem:

from typing import Union

import attr
from cattr import structure, unstructure


@attr.s()
class BaseClass:
    base_field: int = attr.ib()


@attr.s()
class SubClass(BaseClass):
    sub_field: str = attr.ib()


BaseOrSub = Union[BaseClass, SubClass]
print(BaseOrSub)  # <class '__main__.BaseClass'>


my_base = BaseClass(base_field=7)
print(structure(unstructure(my_base), BaseOrSub))  # BaseClass(foo=7)

my_sub = SubClass(base_field=8, sub_field="bar")
print(structure(unstructure(my_sub), BaseOrSub))  # TypeError: __init__() got an unexpected keyword argument 'bar'

Which gives this stack trace:

Traceback (most recent call last):
BaseClass(base_field=7)
  File "/workplace/kevomara/Blackhawk/src/BlackhawkLambda/src/common/dict_mapping/cattr_union_issue.py", line 25, in <module>
    print(structure(unstructure(my_sub), BaseOrSub))  # TypeError: __init__() got an unexpected keyword argument 'bar'
  File "/workplace/kevomara/Blackhawk/env/BlackhawkLambda-1.0/test-runtime/lib/python3.6/site-packages/cattr/converters.py", line 178, in structure
    return self._structure_func.dispatch(cl)(obj, cl)
  File "/workplace/kevomara/Blackhawk/env/BlackhawkLambda-1.0/test-runtime/lib/python3.6/site-packages/cattr/converters.py", line 298, in structure_attrs_fromdict
    return cl(**conv_obj)
TypeError: __init__() got an unexpected keyword argument 'sub_field'

I also tried both of these:

print(structure(unstructure(my_sub), [BaseClass, SubClass]))  #  TypeError: unhashable type: 'list'
print(structure(unstructure(my_sub), frozenset([BaseClass, SubClass])))  # ValueError: Unsupported type: frozenset({<class '__main__.SubClass'>, <class '__main__.BaseClass'>}). Register a structure hook for it.

kevin-d-omara avatar Dec 11 '19 23:12 kevin-d-omara

Same issue. Can we please get help? Thanks!

vladivanov20 avatar May 26 '22 13:05 vladivanov20

Hello, I am facing the same issue, is there a possible workaround?

matmel avatar Aug 03 '22 19:08 matmel

Since Python 3.7, explicit subclasses aren't removed from unions any longer. The code snippet from the OP works now:

❯ poetry run python a10.py
typing.Union[__main__.BaseClass, __main__.SubClass]
BaseClass(base_field=7)
SubClass(base_field=8, sub_field='bar')

So I'm closing this particular issue.

Tinche avatar Sep 29 '22 21:09 Tinche