Correct mro order attrs object can't find abstract property

52 views Asked by At

The following runs fine:

from abc import ABC, abstractmethod
import attr

class A(ABC):
    @abstractmethod
    def prop(self):
        pass

    def foo(self):
        print(self.prop())

class B:
    def prop(self):
        return 5

class C(B, A):
    pass

C()

Here, C has a function prop inherited from B which is found first in the MRO order so A wouldn't complain about its existence. I tried this with a dataclass as well:

from abc import ABC, abstractmethod
from dataclasses import dataclass, field

class A(ABC):
    @property
    @abstractmethod
    def prop(self):
        pass

    def foo(self):
        print(self.prop())

@dataclass
class B:
    prop: int = field(default=5)

class C(B, A):
    pass

C()

However, this doesn't work with attr:

from abc import ABC, abstractmethod
import attr

@attr.s
class A(ABC):
    @property
    @abstractmethod
    def prop(self):
        pass

    def foo(self):
        print(self.prop())

@attr.s
class B:
    prop: int = attr.ib(default=5)

@attr.s
class C(B, A):
    pass

C()

Running it:

❯ python test.py                                                                                  
Traceback (most recent call last):
  File "test_mro_abstract_property.py", line 22, in <module>
    C()
TypeError: Can't instantiate abstract class C with abstract methods prop

The codebase I'm working in has standardized on using attr so I would like to continue using attr if possible. What does attrs do differently from dataclass such that it can't discover the prop member? Are there workarounds for this issue?

The attr version I'm using is 22.1.0 (latest release)

1

There are 1 answers

0
hynek On

There's a workaround, but I'm not 100% why it works: make B slotted:

@attr.s(slots=True)
class B:
    prop: int = attr.ib(default=5)

makes it work.

I suspect it's because abc checks __slots__ and it's there.

Dataclasses put some defaults as class variables on the class body, but not all.

For instance this fails:

@dataclass
class B:
    prop: list = field(default_factory=list)

This kind of inconsistencies is why I'm not a fan of treating certain defaults in special ways.


JFTR, in modern attrs (20.1.0 from August 2020) your code would look like this (and work, because slots are on by default):

from abc import ABC, abstractmethod
import attr

@attr.define
class A(ABC):
    @property
    @abstractmethod
    def prop(self):
        pass

    def foo(self):
        print(self.prop())

@attr.define
class B:
    prop: int = attr.field(default=5)

@attr.define
class C(B, A):
    pass

C()

(if you're OK with requiring 21.3, you can even use import attrs)