python-attrs / cattrs

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

`AttributeError` during fetching `__orig_bases__` on non-generic protocol implementation. #374

Closed nekitdev closed 10 months ago

nekitdev commented 1 year ago

Description

I wanted to register an unstructure hook for an Entity type derived from the non-generic Binary protocol.

What I Did

CONVERTER.register_unstructure_hook(
    Entity, make_dict_unstructure_fn(Entity, CONVERTER, client_unchecked=override(omit=True))
)

Which caused an error:

  File ".../cattrs/gen/__init__.py", line 82, in make_dict_unstructure_fn
    mapping = generate_mapping(cl, mapping)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../cattrs/gen/_generics.py", line 32, in generate_mapping
    orig_bases = cl.__orig_bases__
                 ^^^^^^^^^^^^^^^^^
AttributeError: type object 'Entity' has no attribute '__orig_bases__'
Tinche commented 1 year ago

Can you show me the Entity class? Or a minimal example of it that reproduces the issue.

nekitdev commented 1 year ago

Will do so a bit later, thanks for responding!

nekitdev commented 1 year ago

Here is an example I came up with; importing Protocol from typing does not change anything. If the Protocol was generic, everything would work just fine.

from attrs import define
from cattrs import Converter
from cattrs.gen import make_dict_unstructure_fn
from typing_extensions import Protocol

CONVERTER = Converter()

class SomeProtocol(Protocol):
    ...

@define()
class Entity(SomeProtocol):
    ...

CONVERTER.register_unstructure_hook(Entity, make_dict_unstructure_fn(Entity, CONVERTER))
shadchin commented 1 year ago

I have the same error :-(

waweber commented 1 year ago

Also seeing this error.

There's a couple workarounds I tried that seem to work:

Adding a no-op protocol as a base:

_T = TypeVar("_T", covariant=True)

class _Protocol(Protocol[_T]):
    ...

class SomeProtocol(_Protocol[Any], Protocol):
    ...

@define()
class Entity(SomeProtocol):
    ...

Adding a fake __orig_bases__ attribute (this one might have unknown consequences):


class SomeProtocol(Protocol):

    __orig_bases__: ClassVar[tuple] = ()

    ...

@define()
class Entity(SomeProtocol):
    ...
Tinche commented 10 months ago

Alright, starting on this before wrapping up the next release.

I think the problem is Protocol is a subclass of Generic, even though it's not necessarily generic. Should be an easy fix.