From 568e85e67ae8177afc85efdb0084302e749cc156 Mon Sep 17 00:00:00 2001 From: Endika Iglesias Date: Mon, 3 Aug 2026 14:47:35 +0200 Subject: [PATCH] Report access to abstract static and class methods on the class --- mypy/checkmember.py | 31 +++++++++++++++ mypy/messages.py | 9 +++++ test-data/unit/check-abstract.test | 60 ++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 3ba99d8e8c6b2..8f10ff70787e8 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -423,6 +423,7 @@ def analyze_type_callable_member_access(name: str, typ: FunctionLike, mx: Member # the corresponding method in the current instance to avoid this edge case. # See https://github.com/python/mypy/pull/1787 for more info. # TODO: do not rely on same type variables being present in all constructor overloads. + check_abstract_class_attribute_access(name, instance_type, mx) result = analyze_class_attribute_access( instance_type, name, @@ -438,6 +439,36 @@ def analyze_type_callable_member_access(name: str, typ: FunctionLike, mx: Member assert False, f"Unexpected type {instance_type!r}" +def check_abstract_class_attribute_access( + name: str, instance_type: Instance, mx: MemberContext +) -> None: + """Report accessing an abstract static or class method through its own class. + + Only reached for a direct reference to the class. Through `type[T]`, or on an + instance, the runtime object may be a subclass which implements the method, + which is also why instance methods are excluded here. + """ + if mx.suppress_errors or not instance_type.type.is_abstract: + return + if all(attr != name for attr, _ in instance_type.type.abstract_attributes): + return + + node = instance_type.type.get(name) + if node is None: + return + func = node.node.func if isinstance(node.node, Decorator) else node.node + if not isinstance(func, SYMBOL_FUNCBASE_TYPES): + return + + if func.is_static: + kind = "static method" + elif func.is_class: + kind = "class method" + else: + return + mx.msg.cannot_access_abstract_class_attribute(instance_type.type.name, name, kind, mx.context) + + def analyze_type_type_member_access( name: str, typ: TypeType, mx: MemberContext, override_info: TypeInfo | None ) -> Type: diff --git a/mypy/messages.py b/mypy/messages.py index ffac78201a6cd..849b40c6c4944 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1580,6 +1580,15 @@ def incompatible_conditional_function_def( self.note("Redefinition:", defn) self.pretty_callable_or_overload(new_type, defn, offset=4, parent_error=error) + def cannot_access_abstract_class_attribute( + self, class_name: str, attr_name: str, kind: str, context: Context + ) -> None: + self.fail( + f'Cannot access abstract {kind} "{attr_name}" of abstract class "{class_name}"', + context, + code=codes.ABSTRACT, + ) + def cannot_instantiate_abstract_class( self, class_name: str, abstract_attributes: dict[str, bool], context: Context ) -> None: diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test index 7507a31d115a9..c9ba1cd8ef1cd 100644 --- a/test-data/unit/check-abstract.test +++ b/test-data/unit/check-abstract.test @@ -1688,3 +1688,63 @@ from typing import TYPE_CHECKING class C: if TYPE_CHECKING: def dynamic(self) -> int: ... # OK + +[case testAccessAbstractStaticAndClassMethodOnClass] +from abc import abstractmethod, ABCMeta +from typing import Type + +class A(metaclass=ABCMeta): + @staticmethod + @abstractmethod + def s() -> int: pass + @classmethod + @abstractmethod + def c(cls) -> int: pass + @abstractmethod + def m(self) -> int: pass + +A.s() # E: Cannot access abstract static method "s" of abstract class "A" +A.c() # E: Cannot access abstract class method "c" of abstract class "A" +f = A.s # E: Cannot access abstract static method "s" of abstract class "A" + +# Unbound: whatever is passed as self may implement the method. +g = A.m + +# An instance may be of a subclass which implements the method. +def via_instance(a: A) -> None: + a.m() + a.s() + a.c() + +# So may the class object behind type[A]. +def via_type(t: Type[A]) -> None: + t.s() + t.c() +[builtins fixtures/classmethod.pyi] + +[case testAccessAbstractStaticAndClassMethodOnSubclass] +from abc import abstractmethod, ABCMeta + +class A(metaclass=ABCMeta): + @staticmethod + @abstractmethod + def s() -> int: pass + @classmethod + @abstractmethod + def c(cls) -> int: pass + +class Implemented(A): + @staticmethod + def s() -> int: return 0 + @classmethod + def c(cls) -> int: return 0 + +class StillAbstract(A): + @staticmethod + def s() -> int: return 0 + +Implemented.s() +Implemented.c() +StillAbstract.s() +StillAbstract.c() # E: Cannot access abstract class method "c" of abstract class "StillAbstract" +[builtins fixtures/classmethod.pyi]