Skip to content

Commit 87e9425

Browse files
authored
Prohibit access via class for instance-only attributes (python#20855)
Fixes python#240 (yes, you read that right, three digits) I am taking a conservative approach and flag only cases that would definitely fail at runtime. Note we already have a more strict check for this for generics, but it is tricky to avoid double errors, so I think it is fine to have two errors sometimes. I also update an outdated comment that confused me for a while.
1 parent 6d30d75 commit 87e9425

7 files changed

Lines changed: 43 additions & 10 deletions

File tree

mypy/checkmember.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,10 +1191,13 @@ def analyze_class_attribute_access(
11911191
if info.slots and name in info.slots:
11921192
mx.fail(message_registry.CLASS_VAR_CONFLICTS_SLOTS.format(name))
11931193

1194-
# If a final attribute was declared on `self` in `__init__`, then it
1195-
# can't be accessed on the class object.
1196-
if node.implicit and isinstance(node.node, Var) and node.node.is_final:
1197-
mx.fail(message_registry.CANNOT_ACCESS_FINAL_INSTANCE_ATTR.format(node.node.name))
1194+
if node.implicit and isinstance(node.node, Var):
1195+
if node.node.is_final:
1196+
# If a final attribute was declared on `self` in `__init__`, then it
1197+
# can't be accessed on the class object.
1198+
mx.fail(message_registry.CANNOT_ACCESS_FINAL_INSTANCE_ATTR.format(node.node.name))
1199+
elif not mx.is_lvalue and not defined_in_superclass(info, name):
1200+
mx.fail(message_registry.CANNOT_ACCESS_INSTANCE_ONLY_ATTR.format(node.node.name))
11981201

11991202
# An assignment to final attribute on class object is also always an error,
12001203
# independently of types.
@@ -1574,3 +1577,12 @@ def meta_has_operator(item: Type, op_method: str, named_type: Callable[[str], In
15741577
item = instance_fallback(item, named_type)
15751578
meta = item.type.metaclass_type or named_type("builtins.type")
15761579
return meta.type.has_readable_member(op_method)
1580+
1581+
1582+
def defined_in_superclass(info: TypeInfo, name: str) -> bool:
1583+
"""Check if a variable has an explicit value at class level in any of superclasses."""
1584+
for base in info.mro[1:]:
1585+
if (node := base.names.get(name)) is not None:
1586+
if not node.implicit and isinstance(node.node, Var) and node.node.has_explicit_value:
1587+
return True
1588+
return False

mypy/message_registry.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
238238
CANNOT_ACCESS_FINAL_INSTANCE_ATTR: Final = (
239239
'Cannot access final instance attribute "{}" on class object'
240240
)
241+
CANNOT_ACCESS_INSTANCE_ONLY_ATTR: Final = (
242+
'Cannot access instance-only attribute "{}" on class object'
243+
)
241244
CANNOT_MAKE_DELETABLE_FINAL: Final = ErrorMessage("Deletable attribute cannot be final")
242245

243246
# Disjoint bases

mypy/nodes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1312,7 +1312,8 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None:
13121312
self.is_cls = False
13131313
self.is_ready = True # If inferred, is the inferred type available?
13141314
self.is_inferred = self.type is None
1315-
# Is this initialized explicitly to a non-None value in class body?
1315+
# Is this variable declared in class body? The name is confusing, but it
1316+
# is a very old attribute, and changing will break some plugins.
13161317
self.is_initialized_in_class = False
13171318
self.is_staticmethod = False
13181319
self.is_classmethod = False

test-data/unit/check-classes.test

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7180,7 +7180,22 @@ b = B(2) # E: Cannot instantiate abstract class "B" with abstract attribute "__i
71807180
B.c # E: "type[B]" has no attribute "c"
71817181
c = C(3)
71827182
c.c
7183-
C.c
7183+
C.c # E: Cannot access instance-only attribute "c" on class object
7184+
7185+
[case testAccessInstanceVarOnClass]
7186+
class A:
7187+
def __init__(self) -> None:
7188+
self.x = 0
7189+
7190+
A.x # E: Cannot access instance-only attribute "x" on class object
7191+
A.x = 1 # We allow this, since it works at runtime, even though it is weird
7192+
7193+
class B:
7194+
x = 1
7195+
class C(B):
7196+
def __init__(self) -> None:
7197+
self.x: int = 2
7198+
C.x # Again, this works at runtime, so we don't prohibit this
71847199

71857200
[case testDecoratedConstructors]
71867201
from typing import TypeVar, Callable, Any

test-data/unit/check-dataclasses.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,8 @@ class A(Generic[T]):
839839
@classmethod
840840
def foo(cls) -> None:
841841
reveal_type(cls) # N: Revealed type is "type[__main__.A[T`1]]"
842-
cls.x # E: Access to generic instance variables via class is ambiguous
842+
cls.x # E: Cannot access instance-only attribute "x" on class object \
843+
# E: Access to generic instance variables via class is ambiguous
843844

844845
@classmethod
845846
def other(cls, x: T) -> A[T]: ...

test-data/unit/check-generics.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2175,7 +2175,8 @@ class C(Generic[T]):
21752175

21762176
@classmethod
21772177
def meth(cls) -> None:
2178-
cls.x # E: Access to generic instance variables via class is ambiguous
2178+
cls.x # E: Cannot access instance-only attribute "x" on class object \
2179+
# E: Access to generic instance variables via class is ambiguous
21792180
[builtins fixtures/classmethod.pyi]
21802181

21812182
[case testGenericClassMethodUnboundOnClassNonMatchingIdNonGeneric]

test-data/unit/check-incremental.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2938,7 +2938,7 @@ class Namespace:
29382938

29392939
[file main.py]
29402940
import ns
2941-
user = ns.Namespace.user
2941+
user = ns.Namespace().user
29422942

29432943
[out1]
29442944
tmp/main.py:2: error: Expression has type "Any"
@@ -5490,7 +5490,7 @@ class A:
54905490
from a import A
54915491
[file b.py.2]
54925492
from a import A
5493-
reveal_type(A.D.x)
5493+
reveal_type(A.D().x)
54945494
[builtins fixtures/isinstance.pyi]
54955495
[out]
54965496
[out2]

0 commit comments

Comments
 (0)