Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1747,6 +1747,28 @@ def check_callable_call(
lambda i: self.accept(args[i]),
)

if callee.special_sig == "tuple" and len(args) == 1:
with self.msg.filter_errors():
arg_type = get_proper_type(self.accept(args[0]))
# Give precise constructor signature for situations like this:
# class Shape[*Ts](tuple[*Ts]): ...
# Shape((1, 2))
# The argument type is the same as return type, but with builtins.tuple fallback.
if isinstance(arg_type, TupleType):
assert isinstance(callee.ret_type, ProperType)
if isinstance(callee.ret_type, TupleType):
# Actual type argument is ignored by tuple_fallback() in this case.
any_type = AnyType(TypeOfAny.special_form)
new_arg_type = callee.ret_type.copy_modified(
fallback=self.chk.named_generic_type("builtins.tuple", [any_type])
)
callee = callee.copy_modified(arg_types=[new_arg_type])
elif isinstance(callee.ret_type, Instance):
new_arg_type = map_instance_to_supertype(
callee.ret_type, self.chk.lookup_typeinfo("builtins.tuple")
)
callee = callee.copy_modified(arg_types=[new_arg_type])

if callee.is_generic():
need_refresh = any(
isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in callee.variables
Expand Down
16 changes: 10 additions & 6 deletions mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1224,8 +1224,9 @@ def infer_against_overloaded(
def visit_tuple_type(self, template: TupleType) -> list[Constraint]:
actual = self.actual
unpack_index = find_unpack_in_list(template.items)
is_varlength_tuple = (
isinstance(actual, Instance) and actual.type.fullname == "builtins.tuple"
# TODO: we may need to be careful with direction to avoid spurious constraints.
is_varlength_tuple = isinstance(actual, Instance) and actual.type.has_base(
"builtins.tuple"
)

if isinstance(actual, TupleType) or is_varlength_tuple:
Expand All @@ -1237,23 +1238,26 @@ def visit_tuple_type(self, template: TupleType) -> list[Constraint]:
unpack_type = template.items[unpack_index]
assert isinstance(unpack_type, UnpackType)
unpacked_type = get_proper_type(unpack_type.type)
assert isinstance(actual, Instance) # ensured by is_varlength_tuple == True
mapped = map_instance_to_supertype(
actual, actual.type.get_base("builtins.tuple")
)
if isinstance(unpacked_type, TypeVarTupleType):
res = [
Constraint(type_var=unpacked_type, op=self.direction, target=actual)
Constraint(type_var=unpacked_type, op=self.direction, target=mapped)
]
else:
assert (
isinstance(unpacked_type, Instance)
and unpacked_type.type.fullname == "builtins.tuple"
)
res = infer_constraints(unpacked_type, actual, self.direction)
assert isinstance(actual, Instance) # ensured by is_varlength_tuple == True
res = infer_constraints(unpacked_type, mapped, self.direction)
for i, ti in enumerate(template.items):
if i == unpack_index:
# This one we just handled above.
continue
# For Tuple[T, *Ts, S] <: tuple[X, ...] infer also T <: X and S <: X.
res.extend(infer_constraints(ti, actual.args[0], self.direction))
res.extend(infer_constraints(ti, mapped.args[0], self.direction))
return res
else:
assert isinstance(actual, TupleType)
Expand Down
6 changes: 6 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4103,6 +4103,12 @@ def has_base(self, fullname: str) -> bool:
return True
return False

def get_base(self, fullname: str) -> TypeInfo:
for cls in self.mro:
if cls.fullname == fullname:
return cls
assert False, f"Missing base {fullname} for {self.fullname}"

def direct_base_classes(self) -> list[TypeInfo]:
"""Return a direct base classes.

Expand Down
6 changes: 6 additions & 0 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@ def type_object_type(
assert isinstance(method.type, FunctionLike) # is_valid_constructor() ensures this
t = method.type
result = type_object_type_from_function(t, info, method.info, fallback, is_new)
# Tuple constructor in typeshed is imprecise (and precise one is impossible to express),
# so we special-case constructors for tuple types. Note we skip the tuple class itself
# as a micro-optimization, since it is unlikely one would write tuple((1, 2)).
if method.info.fullname == "builtins.tuple" and info.fullname != "builtins.tuple":
assert isinstance(result, CallableType)
result = result.copy_modified(special_sig="tuple")
# Only write cached result is strict_optional=True, otherwise we may get
# inconsistent behaviour because of union simplification.
if allow_cache and state.strict_optional:
Expand Down
2 changes: 1 addition & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2149,7 +2149,7 @@ class CallableType(FunctionLike):
# specified by the user?
"special_sig", # Non-None for signatures that require special handling
# (currently only values are 'dict' for a signature similar to
# 'dict' and 'partial' for a `functools.partial` evaluation)
# 'dict' and 'partial' for a `functools.partial` evaluation, and 'tuple')
"from_type_type", # Was this callable generated by analyzing Type[...]
# instantiation?
"is_bound", # Is this a bound method?
Expand Down
2 changes: 1 addition & 1 deletion mypyc/test-data/run-tuples.test
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ def test_user_defined() -> None:

[file copysubclass.py]
from typing import Any
class subc(tuple[Any]):
class subc(tuple[Any, ...]):
pass

[file userdefinedtuple.py]
Expand Down
30 changes: 30 additions & 0 deletions test-data/unit/check-typevar-tuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -2773,3 +2773,33 @@ from typing import Protocol, Unpack
class C(Protocol[Unpack]): # E: Free type variable expected in Protocol[...]
pass
[builtins fixtures/tuple.pyi]

[case testGenericTupleSubclassConstructor]
from typing import TypeVar, TypeVarTuple, Unpack

T = TypeVar("T")
S = TypeVar("S")
Ts = TypeVarTuple("Ts")

class S1(tuple[T, S]): ...
class S2(tuple[Unpack[Ts]]): ...

reveal_type(S1((1, ""))) # N: Revealed type is "tuple[builtins.int, builtins.str, fallback=__main__.S1[builtins.int, builtins.str]]"
reveal_type(S1((1, "", 2))) # N: Revealed type is "tuple[Never, Never, fallback=__main__.S1[Never, Never]]" \
# E: Argument 1 to "S1" has incompatible type "tuple[int, str, int]"; expected "tuple[Never, Never]"

a1: S1[int, str] = S1((1, ""))
b1: S1[int, str] = S1((1, 1)) # E: Argument 1 to "S1" has incompatible type "tuple[int, int]"; expected "tuple[int, str]"
c1: S1[int, str] = S1((1, "", 2)) # E: Argument 1 to "S1" has incompatible type "tuple[int, str, int]"; expected "tuple[int, str]"

reveal_type(S2(())) # N: Revealed type is "tuple[(), fallback=__main__.S2[()]]"
reveal_type(S2((1, ""))) # N: Revealed type is "tuple[Literal[1]?, Literal['']?, fallback=__main__.S2[Literal[1]?, Literal['']?]]"
reveal_type(S2((1, "", 2))) # N: Revealed type is "tuple[Literal[1]?, Literal['']?, Literal[2]?, fallback=__main__.S2[Literal[1]?, Literal['']?, Literal[2]?]]"

a2: S2[int, str] = S2((1, ""))
b2: S2[int, str] = S2((1, 1)) # E: Argument 1 to "S2" has incompatible type "tuple[int, int]"; expected "tuple[int, str]"
c2: S2[int, str] = S2((1, "", 2)) # E: Argument 1 to "S2" has incompatible type "tuple[int, str, int]"; expected "tuple[int, str]"

t: S2[Unpack[tuple[int, ...]]] = S2((1, 2))
t_bad: S2[Unpack[tuple[int, ...]]] = S2((1, "")) # E: Argument 1 to "S2" has incompatible type "tuple[int, str]"; expected "tuple[int, ...]"
[builtins fixtures/tuple.pyi]
Loading