Skip to content

Commit 6250063

Browse files
authored
Fix interaction between ClassVar in protocols and class objects (#13545)
Fixes #13537 I also discovered another similar false negative, when a property in protocol was allowed to be implemented by an instance variable in class object. This is also unsafe because instance variable may not be present on class object at all. Only class variables are allowed (similar to mutable attributes in protocols).
1 parent 4cfacc6 commit 6250063

File tree

3 files changed

+56
-10
lines changed

3 files changed

+56
-10
lines changed

mypy/messages.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
IS_CLASS_OR_STATIC,
5555
IS_CLASSVAR,
5656
IS_SETTABLE,
57+
IS_VAR,
5758
find_member,
5859
get_member_flags,
5960
is_same_type,
@@ -1959,13 +1960,25 @@ def report_protocol_problems(
19591960
context,
19601961
code=code,
19611962
)
1962-
if class_obj and IS_SETTABLE in superflags and IS_CLASSVAR not in subflags:
1963+
if (
1964+
class_obj
1965+
and IS_VAR in superflags
1966+
and (IS_VAR in subflags and IS_CLASSVAR not in subflags)
1967+
):
19631968
self.note(
19641969
"Only class variables allowed for class object access on protocols,"
19651970
' {} is an instance variable of "{}"'.format(name, subtype.type.name),
19661971
context,
19671972
code=code,
19681973
)
1974+
if class_obj and IS_CLASSVAR in superflags:
1975+
self.note(
1976+
"ClassVar protocol member {}.{} can never be matched by a class object".format(
1977+
supertype.type.name, name
1978+
),
1979+
context,
1980+
code=code,
1981+
)
19691982
self.print_more(conflict_flags, context, OFFSET, MAX_ITEMS, code=code)
19701983

19711984
def pretty_overload(
@@ -2600,8 +2613,10 @@ def get_bad_protocol_flags(
26002613
or IS_CLASS_OR_STATIC in superflags
26012614
and IS_CLASS_OR_STATIC not in subflags
26022615
or class_obj
2603-
and IS_SETTABLE in superflags
2616+
and IS_VAR in superflags
26042617
and IS_CLASSVAR not in subflags
2618+
or class_obj
2619+
and IS_CLASSVAR in superflags
26052620
):
26062621
bad_flags.append((name, subflags, superflags))
26072622
return bad_flags

mypy/subtypes.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
IS_SETTABLE: Final = 1
6969
IS_CLASSVAR: Final = 2
7070
IS_CLASS_OR_STATIC: Final = 3
71+
IS_VAR: Final = 4
7172

7273
TypeParameterChecker: _TypeAlias = Callable[[Type, Type, int, bool, "SubtypeContext"], bool]
7374

@@ -1020,9 +1021,12 @@ def named_type(fullname: str) -> Instance:
10201021
if (IS_CLASSVAR in subflags) != (IS_CLASSVAR in superflags):
10211022
return False
10221023
else:
1023-
if IS_SETTABLE in superflags and IS_CLASSVAR not in subflags:
1024+
if IS_VAR in superflags and IS_CLASSVAR not in subflags:
10241025
# Only class variables are allowed for class object access.
10251026
return False
1027+
if IS_CLASSVAR in superflags:
1028+
# This can be never matched by a class object.
1029+
return False
10261030
if IS_SETTABLE in superflags and IS_SETTABLE not in subflags:
10271031
return False
10281032
# This rule is copied from nominal check in checker.py
@@ -1118,13 +1122,17 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set
11181122
if isinstance(method, Decorator):
11191123
if method.var.is_staticmethod or method.var.is_classmethod:
11201124
return {IS_CLASS_OR_STATIC}
1125+
elif method.var.is_property:
1126+
return {IS_VAR}
11211127
elif method.is_property: # this could be settable property
11221128
assert isinstance(method, OverloadedFuncDef)
11231129
dec = method.items[0]
11241130
assert isinstance(dec, Decorator)
11251131
if dec.var.is_settable_property or setattr_meth:
1126-
return {IS_SETTABLE}
1127-
return set()
1132+
return {IS_VAR, IS_SETTABLE}
1133+
else:
1134+
return {IS_VAR}
1135+
return set() # Just a regular method
11281136
node = info.get(name)
11291137
if not node:
11301138
if setattr_meth:
@@ -1137,8 +1145,10 @@ def get_member_flags(name: str, itype: Instance, class_obj: bool = False) -> set
11371145
return set()
11381146
v = node.node
11391147
# just a variable
1140-
if isinstance(v, Var) and not v.is_property:
1141-
flags = set()
1148+
if isinstance(v, Var):
1149+
if v.is_property:
1150+
return {IS_VAR}
1151+
flags = {IS_VAR}
11421152
if not v.is_final:
11431153
flags.add(IS_SETTABLE)
11441154
if v.is_classvar:

test-data/unit/check-protocols.test

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3182,8 +3182,21 @@ test(C) # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P"
31823182
test(D) # E: Argument 1 to "test" has incompatible type "Type[D]"; expected "P" \
31833183
# N: Only class variables allowed for class object access on protocols, foo is an instance variable of "D"
31843184

3185+
[case testProtocolClassObjectClassVarRejected]
3186+
from typing import ClassVar, Protocol
3187+
3188+
class P(Protocol):
3189+
foo: ClassVar[int]
3190+
3191+
class B:
3192+
foo: ClassVar[int]
3193+
3194+
def test(arg: P) -> None: ...
3195+
test(B) # E: Argument 1 to "test" has incompatible type "Type[B]"; expected "P" \
3196+
# N: ClassVar protocol member P.foo can never be matched by a class object
3197+
31853198
[case testProtocolClassObjectPropertyRejected]
3186-
from typing import Protocol
3199+
from typing import ClassVar, Protocol
31873200

31883201
class P(Protocol):
31893202
@property
@@ -3192,12 +3205,20 @@ class P(Protocol):
31923205
class B:
31933206
@property
31943207
def foo(self) -> int: ...
3208+
class C:
3209+
foo: int
3210+
class D:
3211+
foo: ClassVar[int]
31953212

31963213
def test(arg: P) -> None: ...
3197-
# TODO: give better diagnostics in this case.
3214+
# TODO: skip type mismatch diagnostics in this case.
31983215
test(B) # E: Argument 1 to "test" has incompatible type "Type[B]"; expected "P" \
31993216
# N: Following member(s) of "B" have conflicts: \
3200-
# N: foo: expected "int", got "Callable[[B], int]"
3217+
# N: foo: expected "int", got "Callable[[B], int]" \
3218+
# N: Only class variables allowed for class object access on protocols, foo is an instance variable of "B"
3219+
test(C) # E: Argument 1 to "test" has incompatible type "Type[C]"; expected "P" \
3220+
# N: Only class variables allowed for class object access on protocols, foo is an instance variable of "C"
3221+
test(D) # OK
32013222
[builtins fixtures/property.pyi]
32023223

32033224
[case testProtocolClassObjectInstanceMethod]

0 commit comments

Comments
 (0)