cattrs icon indicating copy to clipboard operation
cattrs copied to clipboard

Unstructuring subclasses

Open AdrianSosic opened this issue 2 years ago • 2 comments

  • cattrs version: 22.2.0
  • Python version: 3.8
  • Operating System: macOS

Description

Hi @Tinche, I am currently working on several projects where I need to (un)structure objects of subclasses of a common base class. Digging through the documentation and also some issues here on Github, I eventually found your example how this can be done with the current version of cattrs. However, I think I have found two problems (bugs? unsupported features?) with the approach and would love to hear your opinion.

But before I go into the details, a quick question upfront: Is this going to remain the "recommended" approach to unstructure objects from a class hierarchy? I am asking because I also saw #312 which seems about to be merged but I am not entirely sure how/if it's going to replace your described method.

So here are now the two issues I noticed:

  1. With the version described above, your example no longer seems to work. In order to get it running, one now needs to explicitly set slotted=False. I guess this is probably due to due the changes defaults (see also #267)? On the other hand, I wonder why your example then originally worked because it already uses define instead of attr.s?
  2. Still, I struggled a lot to get it running in my own code until I finally found the problem: The approach crashes when using from __future__ import annotations and throws the following exception:
Traceback (most recent call last):
  File "/usr/local/Caskroom/mambaforge/base/envs/baybe/lib/python3.8/site-packages/cattrs/converters.py", line 461, in _structure_attribute
    return self._structure_func.dispatch(type_)(value, type_)
  File "/usr/local/Caskroom/mambaforge/base/envs/baybe/lib/python3.8/site-packages/cattrs/converters.py", line 377, in _structure_error
    raise StructureHandlerNotFoundError(msg, type_=cl)
cattrs.errors.StructureHandlerNotFoundError: Unsupported type: 'int'. Register a structure hook for it.

Is the second issue related to #215 or is this a bug?

AdrianSosic avatar Apr 14 '23 07:04 AdrianSosic

A little difficult to answer these without concrete examples.

I think #312 is a better bet than the old snippets since it's a more sophisticated approach, and it's a better base for experimentation.

Tinche avatar Apr 14 '23 12:04 Tinche

Thanks for your reply! The code example is really just your code + the future import:

from __future__ import annotations

from attr import define

from cattr import GenConverter

c = GenConverter()


@define
class Base:
   a: int


@define
class SubOne(Base):
   b: int


@define
class SubTwo(Base):
   c: int


def unstructure_base(b):
   return {"_type": b.__class__.__name__, **c.unstructure_attrs_asdict(b)}


c.register_unstructure_hook(Base, unstructure_base)


print(c.unstructure(SubOne(1, 2)))


def structure_base(val, _):
   sub_name = val.pop("_type")
   for klass in Base.__subclasses__():
       if klass.__name__ == sub_name:
           return c.structure_attrs_fromdict(val, klass)
   return c.structure_attrs_fromdict(val, Base)


c.register_structure_hook(Base, structure_base)

print(c.structure(c.unstructure(SubOne(1, 2)), Base))
print(c.structure(c.unstructure(SubTwo(1, 2)), Base))
print(c.structure(c.unstructure(Base(1)), Base))

However, I've now also tested with python 3.11 and there everything works as expected! So the issue seems really related to how the future imports are handled in older python versions 🤔 Nonetheless, it might be worth mentioning this somewhere in the docs once one of us figures out the source of the problem...

AdrianSosic avatar Apr 14 '23 13:04 AdrianSosic