Skip to content

Commit 523374e

Browse files
committed
Support protocol checks for self-types in tuple types
1 parent 55ee0bd commit 523374e

4 files changed

Lines changed: 85 additions & 15 deletions

File tree

mypy/constraints.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]:
797797
if isinstance(actual, Instance):
798798
instance = actual
799799
erased = erase_typevars(template)
800-
assert isinstance(erased, Instance) # type: ignore[misc]
800+
assert isinstance(erased, ProperType) and isinstance(erased, Instance)
801801
# We always try nominal inference if possible,
802802
# it is much faster than the structural one.
803803
if self.direction == SUBTYPE_OF and template.type.has_base(instance.type.fullname):
@@ -996,7 +996,24 @@ def visit_instance(self, template: Instance) -> list[Constraint]:
996996
res.extend(cb)
997997
return res
998998
elif isinstance(actual, TupleType) and self.direction == SUPERTYPE_OF:
999-
return infer_constraints(template, mypy.typeops.tuple_fallback(actual), self.direction)
999+
instance = mypy.typeops.tuple_fallback(actual)
1000+
erased = erase_typevars(template)
1001+
assert isinstance(erased, ProperType) and isinstance(erased, Instance)
1002+
# Special-case protocols before using fallback to get more precise constraints
1003+
# for custom tuple types like NamedTuples.
1004+
if (
1005+
template.type.is_protocol
1006+
and self.direction == SUPERTYPE_OF
1007+
and not any(template == t for t in reversed(template.type.inferring))
1008+
and mypy.subtypes.is_protocol_implementation(instance, erased, skip=["__call__"])
1009+
):
1010+
template.type.inferring.append(template)
1011+
res = self.infer_constraints_from_protocol_members(
1012+
instance, template, original_actual, template
1013+
)
1014+
template.type.inferring.pop()
1015+
return res
1016+
return infer_constraints(template, instance, self.direction)
10001017
elif isinstance(actual, TypeVarType):
10011018
if not actual.values and not actual.id.is_meta_var():
10021019
return infer_constraints(template, actual.upper_bound, self.direction)

mypy/messages.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2220,8 +2220,9 @@ def report_protocol_problems(
22202220
class_obj = False
22212221
is_module = False
22222222
skip = []
2223+
original_subtype = subtype
22232224
if isinstance(subtype, TupleType):
2224-
subtype = subtype.partial_fallback
2225+
subtype = mypy.typeops.tuple_fallback(subtype)
22252226
elif isinstance(subtype, TypedDictType):
22262227
subtype = subtype.fallback
22272228
elif isinstance(subtype, TypeType):
@@ -2233,7 +2234,7 @@ def report_protocol_problems(
22332234
if subtype.is_type_obj():
22342235
ret_type = get_proper_type(subtype.ret_type)
22352236
if isinstance(ret_type, TupleType):
2236-
ret_type = ret_type.partial_fallback
2237+
ret_type = mypy.typeops.tuple_fallback(ret_type)
22372238
if not isinstance(ret_type, Instance):
22382239
return
22392240
class_obj = True
@@ -2243,6 +2244,9 @@ def report_protocol_problems(
22432244
skip = ["__call__"]
22442245
if subtype.extra_attrs and subtype.extra_attrs.mod_name:
22452246
is_module = True
2247+
if not isinstance(original_subtype, TupleType):
2248+
# Only tuples are supported as implementing protocols for now.
2249+
original_subtype = subtype
22462250

22472251
# Report missing members
22482252
missing = get_missing_protocol_members(subtype, supertype, skip=skip)
@@ -2274,7 +2278,7 @@ def report_protocol_problems(
22742278

22752279
# Report member type conflicts
22762280
conflict_types = get_conflict_protocol_types(
2277-
subtype, supertype, class_obj=class_obj, options=self.options
2281+
subtype, original_subtype, supertype, class_obj=class_obj, options=self.options
22782282
)
22792283
if conflict_types and (
22802284
not is_subtype(subtype, erase_type(supertype), options=self.options)
@@ -3191,7 +3195,11 @@ def get_missing_protocol_members(left: Instance, right: Instance, skip: list[str
31913195

31923196

31933197
def get_conflict_protocol_types(
3194-
left: Instance, right: Instance, class_obj: bool = False, options: Options | None = None
3198+
left: Instance,
3199+
original_left: Type,
3200+
right: Instance,
3201+
class_obj: bool = False,
3202+
options: Options | None = None,
31953203
) -> list[tuple[str, Type, Type, bool]]:
31963204
"""Find members that are defined in 'left' but have incompatible types.
31973205
Return them as a list of ('member', 'got', 'expected', 'is_lvalue').
@@ -3203,7 +3211,7 @@ def get_conflict_protocol_types(
32033211
continue
32043212
supertype = find_member(member, right, left)
32053213
assert supertype is not None
3206-
subtype = get_protocol_member(left, member, class_obj)
3214+
subtype = get_protocol_member(left, original_left, member, class_obj)
32073215
if not subtype:
32083216
continue
32093217
is_compat = is_subtype(subtype, supertype, ignore_pos_arg_names=True, options=options)
@@ -3219,7 +3227,9 @@ def get_conflict_protocol_types(
32193227
different_setter = True
32203228
supertype = set_supertype
32213229
if IS_EXPLICIT_SETTER in get_member_flags(member, left):
3222-
set_subtype = get_protocol_member(left, member, class_obj, is_lvalue=True)
3230+
set_subtype = get_protocol_member(
3231+
left, original_left, member, class_obj, is_lvalue=True
3232+
)
32233233
if set_subtype and not is_same_type(set_subtype, subtype):
32243234
different_setter = True
32253235
subtype = set_subtype

mypy/subtypes.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,12 @@ def visit_tuple_type(self, left: TupleType) -> bool:
811811
mypy.typeops.tuple_fallback(left), right
812812
):
813813
return True
814+
elif right.type.is_protocol and is_protocol_implementation(
815+
left, right, proper_subtype=self.proper_subtype
816+
):
817+
# Special-case protocols to get precise binding of self type for
818+
# custom tuple types like NamedTuples.
819+
return True
814820
return False
815821
elif isinstance(right, TupleType):
816822
# If right has a variadic unpack this needs special handling. If there is a TypeVarTuple
@@ -1185,7 +1191,7 @@ def pop_on_exit(stack: list[tuple[T, T]], left: T, right: T) -> Iterator[None]:
11851191

11861192

11871193
def is_protocol_implementation(
1188-
left: Instance,
1194+
left: Instance | TupleType,
11891195
right: Instance,
11901196
proper_subtype: bool = False,
11911197
class_obj: bool = False,
@@ -1212,6 +1218,11 @@ def f(self) -> A: ...
12121218
assert right.type.is_protocol
12131219
if skip is None:
12141220
skip = []
1221+
# Preserve original left type for precise self-type binding. Only tuple types are
1222+
# supported for now.
1223+
original_left = left
1224+
if isinstance(left, TupleType):
1225+
left = mypy.typeops.tuple_fallback(left)
12151226
# We need to record this check to generate protocol fine-grained dependencies.
12161227
type_state.record_protocol_subtype_check(left.type, right.type)
12171228
# nominal subtyping currently ignores '__init__' and '__new__' signatures
@@ -1234,10 +1245,10 @@ def f(self) -> A: ...
12341245
ignore_names = member != "__call__" # __call__ can be passed kwargs
12351246
# The third argument below indicates to what self type is bound.
12361247
# We always bind self to the subtype. (Similarly to nominal types).
1237-
supertype = find_member(member, right, left)
1248+
supertype = find_member(member, right, original_left)
12381249
assert supertype is not None
12391250

1240-
subtype = get_protocol_member(left, member, class_obj)
1251+
subtype = get_protocol_member(left, original_left, member, class_obj)
12411252
# Useful for debugging:
12421253
# print(member, 'of', left, 'has type', subtype)
12431254
# print(member, 'of', right, 'has type', supertype)
@@ -1264,9 +1275,11 @@ def f(self) -> A: ...
12641275
if IS_SETTABLE in superflags:
12651276
# Check opposite direction for settable attributes.
12661277
if IS_EXPLICIT_SETTER in superflags:
1267-
supertype = find_member(member, right, left, is_lvalue=True)
1278+
supertype = find_member(member, right, original_left, is_lvalue=True)
12681279
if IS_EXPLICIT_SETTER in subflags:
1269-
subtype = get_protocol_member(left, member, class_obj, is_lvalue=True)
1280+
subtype = get_protocol_member(
1281+
left, original_left, member, class_obj, is_lvalue=True
1282+
)
12701283
# At this point we know attribute is present on subtype, otherwise we
12711284
# would return False above.
12721285
assert supertype is not None and subtype is not None
@@ -1305,7 +1318,7 @@ def f(self) -> A: ...
13051318

13061319

13071320
def get_protocol_member(
1308-
left: Instance, member: str, class_obj: bool, is_lvalue: bool = False
1321+
left: Instance, original_left: Type, member: str, class_obj: bool, is_lvalue: bool = False
13091322
) -> Type | None:
13101323
if member == "__call__" and class_obj:
13111324
# Special case: class objects always have __call__ that is just the constructor.
@@ -1316,7 +1329,7 @@ def get_protocol_member(
13161329
# if constructor signature didn't match, this can cause many false negatives.
13171330
return None
13181331

1319-
subtype = find_member(member, left, left, class_obj=class_obj, is_lvalue=is_lvalue)
1332+
subtype = find_member(member, left, original_left, class_obj=class_obj, is_lvalue=is_lvalue)
13201333
if isinstance(subtype, PartialType):
13211334
subtype = (
13221335
NoneType()

test-data/unit/check-protocols.test

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4779,3 +4779,33 @@ class A(Protocol):
47794779
pass
47804780

47814781
[builtins fixtures/tuple.pyi]
4782+
4783+
[case testTupleTypeSelfTypeProto]
4784+
from typing import Protocol, TypeVar
4785+
4786+
R = TypeVar("R", covariant=True)
4787+
4788+
class P(Protocol[R]):
4789+
def rep(self) -> R: ...
4790+
4791+
T = TypeVar("T")
4792+
def rep(x: P[T]) -> T: ...
4793+
4794+
class C(tuple[int, str]):
4795+
def rep(self: T) -> T: ...
4796+
4797+
t: C
4798+
reveal_type(t) # N: Revealed type is "tuple[builtins.int, builtins.str, fallback=__main__.C]"
4799+
reveal_type(rep(t)) # N: Revealed type is "tuple[builtins.int, builtins.str, fallback=__main__.C]"
4800+
4801+
def ok_rep(x: P[tuple[int, str]]) -> None: ...
4802+
ok_rep(t)
4803+
4804+
def bad_rep(x: P[tuple[str, int]]) -> None: ...
4805+
bad_rep(t) # E: Argument 1 to "bad_rep" has incompatible type "C"; expected "P[tuple[str, int]]" \
4806+
# N: Following member(s) of "C" have conflicts: \
4807+
# N: Expected: \
4808+
# N: def rep(self) -> tuple[str, int] \
4809+
# N: Got: \
4810+
# N: def rep(self) -> C
4811+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)