diff --git a/koerce/annots.py b/koerce/annots.py index 13ad749..6eddd30 100644 --- a/koerce/annots.py +++ b/koerce/annots.py @@ -688,6 +688,7 @@ def __new__( namespace: dict[str, Any] = {} parameters: dict[str, Parameter] = {} + abstractmethods: set = set() for name, value in dct.items(): if isinstance(value, Parameter): parameters[name] = value @@ -697,9 +698,7 @@ def __new__( slots.append(name) else: if getattr(value, "__isabstractmethod__", False): - abstracts.add(name) - else: - abstracts.discard(name) + abstractmethods.add(name) namespace[name] = value # merge the annotations with the parent annotations @@ -724,7 +723,15 @@ def __new__( __spec__=spec, ) klass = super().__new__(metacls, clsname, bases, namespace, **kwargs) - klass.__abstractmethods__ = frozenset(abstracts) + + # check whether the inherited abstract methods are implemented by + # any of the parent classes, basically recalculating the abstractmethods + for name in abstracts: + value = getattr(klass, name, None) + if getattr(value, "__isabstractmethod__", False): + abstractmethods.add(name) + klass.__abstractmethods__ = frozenset(abstractmethods) + return klass def __call__(cls, *args, **kwargs): diff --git a/koerce/tests/test_annots.py b/koerce/tests/test_annots.py index 6c60ada..b2e735d 100644 --- a/koerce/tests/test_annots.py +++ b/koerce/tests/test_annots.py @@ -1961,6 +1961,29 @@ def bar(self): assert Bar.__abstractmethods__ == frozenset() +def test_annotable_recalculates_inherited_abstractmethods(): + class Abstract(Annotable): + @abstractmethod + def foo(self): ... + + @property + @abstractmethod + def bar(self): ... + + class Mixin: + def foo(self): + return 1 + + def bar(self): + return 2 + + class Foo(Mixin, Abstract): + pass + + assert Abstract.__abstractmethods__ == frozenset({"foo", "bar"}) + assert Foo.__abstractmethods__ == frozenset() + + # TODO(kszucs): test __new__ as well def test_annotable_with_custom_init(): called_with = None