pythonabc

`NotImplementedError` gets triggered unexpectedly when tring to define an "abstract class property"


This question is related to a question about the implementation of class property in Python which I have asked yesterday. I received a working solution and wrote my classProp and ClassPropMeta as the solution suggests. However, when I want to use class property together with @abstractmethod, problem occurs.

Below is a minimum example of the problem I have encountered. I defined my ClassPropMeta to extend ABCMeta because I want to avoid metaclass conflict and use both the features of abstract method decorator and class property to define an abstract class property, which means that all concrete subclasses of the class with abstract class property should have the property defined.

In the example, A, B are abstract classes that should have a class property called X, while C is the concrete class where the value of X is defined.

When running this code, I found that the NotImplementedError in the abstract method gets triggered unexpectedly. Further tracing the source by printing the cls value before the error is raised, I found that the value of cls points to class B. Then I set a breakpoint in the method and executed the command w in Pdb. The call stack output is attached after the code.

From my analysis, it seems like that B.X has been called somewhere in the program. So can it be explained where does the B.X access take place?

From further investigation the stack trace points to line 107 of the abc module, which executed the _abc_init() function from the C implementation of _abc module. However till this point, due to my limited knowledge of CPython's internals, I cannot locate where the access takes place.

from abc import ABCMeta, abstractmethod
from typing import Any


class classProp[T, P](property):
    def __get__(self, instance: T, owner: type[T] = None) -> P:
        return self.fget(owner)


class ClassPropMeta(ABCMeta):
    def __setattr__(cls, name: str, value: Any):
        if isinstance(desc := vars(cls).get(name), classProp) and not callable(
            desc.fset
        ):
            raise AttributeError("can't set attribute")
        return super().__setattr__(name, value)


class A(metaclass=ClassPropMeta):
    __slots__ = ()

    @classProp
    @abstractmethod
    def X(cls):
        # breakpoint()
        raise NotImplementedError


class B(A):
    ...
    # @classProp
    # def X(cls):
    #     return 1


class C(B):
    @classProp
    def X(cls):
        return 1


print(C.X)
# result: NotImplementedError gets triggered
> d:\path\to\my\example.py(31)X()
-> breakpoint()
(Pdb) w
  d:\path\to\my\example.py(35)<module>()
-> class B(A):
  <frozen abc>(107)__new__()
  d:\path\to\my\example.py(13)__get__()
-> return self.fget(owner)
> d:\path\to\my\example.py(31)X()
-> breakpoint()

Solution

  • This is slihghtly tricky at first glance. The problem occurs during class creation, when Python runs thru initialization of your B class. At this point, the __new__() method of ABCMeta class needs to figure out which methods are abstract and it does so by actually accessing the X descriptor on B. That in turn triggers classProp.__get__(), which in turn tries to call the implementation. But as you have no implementation there the NotImplementedError is raised because of that.

    To work this around you would need to make classProp smarter and aware of abstract methods so in your __get__() implementation you check if the class you're accessing implemented the abstract method or not and if not, return the abstract method instead of calling it:

    class classProp[T, P](property):
        def __get__(self, instance: T, owner: type[T] = None) -> P:
           if getattr(self.fget, "__isabstractmethod__", False) and owner is not None:
               if not hasattr(owner, "__abstractmethods__"):
                   return self.fget
               if self.fget.__name__ in getattr(owner, "__abstractmethods__", set()):
                   return self.fget
           return self.fget(owner)