Skip to content

Commit 568e85e

Browse files
committed
Report access to abstract static and class methods on the class
1 parent f709fc1 commit 568e85e

3 files changed

Lines changed: 100 additions & 0 deletions

File tree

mypy/checkmember.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ def analyze_type_callable_member_access(name: str, typ: FunctionLike, mx: Member
423423
# the corresponding method in the current instance to avoid this edge case.
424424
# See https://github.com/python/mypy/pull/1787 for more info.
425425
# TODO: do not rely on same type variables being present in all constructor overloads.
426+
check_abstract_class_attribute_access(name, instance_type, mx)
426427
result = analyze_class_attribute_access(
427428
instance_type,
428429
name,
@@ -438,6 +439,36 @@ def analyze_type_callable_member_access(name: str, typ: FunctionLike, mx: Member
438439
assert False, f"Unexpected type {instance_type!r}"
439440

440441

442+
def check_abstract_class_attribute_access(
443+
name: str, instance_type: Instance, mx: MemberContext
444+
) -> None:
445+
"""Report accessing an abstract static or class method through its own class.
446+
447+
Only reached for a direct reference to the class. Through `type[T]`, or on an
448+
instance, the runtime object may be a subclass which implements the method,
449+
which is also why instance methods are excluded here.
450+
"""
451+
if mx.suppress_errors or not instance_type.type.is_abstract:
452+
return
453+
if all(attr != name for attr, _ in instance_type.type.abstract_attributes):
454+
return
455+
456+
node = instance_type.type.get(name)
457+
if node is None:
458+
return
459+
func = node.node.func if isinstance(node.node, Decorator) else node.node
460+
if not isinstance(func, SYMBOL_FUNCBASE_TYPES):
461+
return
462+
463+
if func.is_static:
464+
kind = "static method"
465+
elif func.is_class:
466+
kind = "class method"
467+
else:
468+
return
469+
mx.msg.cannot_access_abstract_class_attribute(instance_type.type.name, name, kind, mx.context)
470+
471+
441472
def analyze_type_type_member_access(
442473
name: str, typ: TypeType, mx: MemberContext, override_info: TypeInfo | None
443474
) -> Type:

mypy/messages.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,15 @@ def incompatible_conditional_function_def(
15801580
self.note("Redefinition:", defn)
15811581
self.pretty_callable_or_overload(new_type, defn, offset=4, parent_error=error)
15821582

1583+
def cannot_access_abstract_class_attribute(
1584+
self, class_name: str, attr_name: str, kind: str, context: Context
1585+
) -> None:
1586+
self.fail(
1587+
f'Cannot access abstract {kind} "{attr_name}" of abstract class "{class_name}"',
1588+
context,
1589+
code=codes.ABSTRACT,
1590+
)
1591+
15831592
def cannot_instantiate_abstract_class(
15841593
self, class_name: str, abstract_attributes: dict[str, bool], context: Context
15851594
) -> None:

test-data/unit/check-abstract.test

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,3 +1688,63 @@ from typing import TYPE_CHECKING
16881688
class C:
16891689
if TYPE_CHECKING:
16901690
def dynamic(self) -> int: ... # OK
1691+
1692+
[case testAccessAbstractStaticAndClassMethodOnClass]
1693+
from abc import abstractmethod, ABCMeta
1694+
from typing import Type
1695+
1696+
class A(metaclass=ABCMeta):
1697+
@staticmethod
1698+
@abstractmethod
1699+
def s() -> int: pass
1700+
@classmethod
1701+
@abstractmethod
1702+
def c(cls) -> int: pass
1703+
@abstractmethod
1704+
def m(self) -> int: pass
1705+
1706+
A.s() # E: Cannot access abstract static method "s" of abstract class "A"
1707+
A.c() # E: Cannot access abstract class method "c" of abstract class "A"
1708+
f = A.s # E: Cannot access abstract static method "s" of abstract class "A"
1709+
1710+
# Unbound: whatever is passed as self may implement the method.
1711+
g = A.m
1712+
1713+
# An instance may be of a subclass which implements the method.
1714+
def via_instance(a: A) -> None:
1715+
a.m()
1716+
a.s()
1717+
a.c()
1718+
1719+
# So may the class object behind type[A].
1720+
def via_type(t: Type[A]) -> None:
1721+
t.s()
1722+
t.c()
1723+
[builtins fixtures/classmethod.pyi]
1724+
1725+
[case testAccessAbstractStaticAndClassMethodOnSubclass]
1726+
from abc import abstractmethod, ABCMeta
1727+
1728+
class A(metaclass=ABCMeta):
1729+
@staticmethod
1730+
@abstractmethod
1731+
def s() -> int: pass
1732+
@classmethod
1733+
@abstractmethod
1734+
def c(cls) -> int: pass
1735+
1736+
class Implemented(A):
1737+
@staticmethod
1738+
def s() -> int: return 0
1739+
@classmethod
1740+
def c(cls) -> int: return 0
1741+
1742+
class StillAbstract(A):
1743+
@staticmethod
1744+
def s() -> int: return 0
1745+
1746+
Implemented.s()
1747+
Implemented.c()
1748+
StillAbstract.s()
1749+
StillAbstract.c() # E: Cannot access abstract class method "c" of abstract class "StillAbstract"
1750+
[builtins fixtures/classmethod.pyi]

0 commit comments

Comments
 (0)