FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Lenient handling of *tuple[Any, ...] (part 1) (#22001) · python/mypy@4f4877e · GitHub

Repository navigation

Commit 4f4877e

Browse files
authored
Lenient handling of *tuple[Any, ...] (part 1) (#22001)
Fixes #19908 Ref #19109 Ref #18665 From very early days mypy used to handle `tuple[X, ...]` essentially as `tuple[()] | tuple[X] | tuple[X, X] | ...`. However, there is one notable exception, when `X = Any` we should use `any()` semantics instead of `all()`. That is, if subtyping (or other typeop) works for at least one number of `Any` items, we should accept it. Note I special-case both tuple types and variadic instances, so that `Foo[*tuple[Any, ...], int] <: Foo[int, int]`. Variadic instances with non-`Any` item stay strict (matching the tuple behaviour). FWIW I didn't see any issues where people complained about strict behavior in non-`Any` case. We can still change it later, but I don't think we should. This PR handles the subtyping part only. I will probably make two follow-up PRs handling inference, and special cases (like indexing, slicing, multiassing, etc). This part was even easier than I thought. The simplification comes from the part that we need to adjust the logic only when variadic `Any` appears on the left. When it appears on the right, things already work "naturally". (Also looking at architecture of relevant code I am happy how I wrote it :-))
1 parent 8d30064 commit 4f4877e

2 files changed

Lines changed: 238 additions & 28 deletions

File tree

‎mypy/subtypes.py‎

Lines changed: 102 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
UninhabitedType,
6868
UnionType,
6969
UnpackType,
70+
extend_args_for_prefix_and_suffix,
7071
find_unpack_in_list,
7172
flatten_nested_unions,
7273
get_proper_type,
@@ -509,18 +510,17 @@ def visit_instance(self, left: Instance) -> bool:
509510
if isinstance(unpacked, Instance):
510511
return self._is_subtype(left, unpacked)
511512
if left.type.has_base(right.partial_fallback.type.fullname):
513+
mapped = map_instance_to_supertype(left, right.partial_fallback.type)
514+
# Special cases to consider:
515+
# * tuple[Any, ...] instance is a (non-proper) subtype of all tuple types.
516+
# * Foo[*tuple[X, ...]] (normalized) instance is a subtype of all
517+
# tuples with appropriate fallback (e.g. for variadic NamedTuples).
512518
if not self.proper_subtype:
513-
# Special cases to consider:
514-
# * Plain tuple[Any, ...] instance is a subtype of all tuple types.
515-
# * Foo[*tuple[Any, ...]] (normalized) instance is a subtype of all
516-
# tuples with fallback to Foo (e.g. for variadic NamedTuples).
517-
mapped = map_instance_to_supertype(left, right.partial_fallback.type)
518-
if is_erased_instance(mapped):
519-
if (
520-
mapped.type.fullname == "builtins.tuple"
521-
or mapped.type.has_type_var_tuple_type
522-
):
523-
return True
519+
if is_erased_instance(mapped) and mapped.type.fullname == "builtins.tuple":
520+
return True
521+
if is_normalized_instance(mapped):
522+
if self._is_subtype(mapped, right.partial_fallback):
523+
return True
524524
return False
525525
if isinstance(right, TypeVarTupleType):
526526
# tuple[Any, ...] is like Any in the world of tuples (see special case above).
@@ -819,9 +819,12 @@ def visit_tuple_type(self, left: TupleType) -> bool:
819819
elif isinstance(right, TupleType):
820820
# If right has a variadic unpack this needs special handling. If there is a TypeVarTuple
821821
# unpack, item count must coincide. If the left has variadic unpack but right
822-
# doesn't have one, we will fall through to False down the line.
822+
# doesn't have one, we will fall through.
823823
if self.variadic_tuple_subtype(left, right):
824824
return True
825+
# The only case where variadic can be subtype of fixed is when left variadic item is Any.
826+
# Otherwise, the original left will be returned, causing fall through to False.
827+
left = self.adjust_left_if_possible(left, right)
825828
if len(left.items) != len(right.items):
826829
return False
827830
if any(not self._is_subtype(l, r) for l, r in zip(left.items, right.items)):
@@ -841,6 +844,57 @@ def visit_tuple_type(self, left: TupleType) -> bool:
841844
else:
842845
return False
843846

847+
def adjust_left_if_possible(self, left: TupleType, right: TupleType) -> TupleType:
848+
"""Adjust shape of left containing *tuple[Any, ...] to match right.
849+
850+
Note: this only works if right is fixed size (including *Ts), the variadic
851+
right are handled by the caller, currently with variadic_tuple_subtype().
852+
"""
853+
left_variadic = self.get_variadic_item(left)
854+
if left_variadic is None:
855+
return left
856+
left_unpack_index, left_item = left_variadic
857+
if not isinstance(get_proper_type(left_item), AnyType):
858+
return left
859+
right_unpack_index = find_unpack_in_list(right.items)
860+
if right_unpack_index is None:
861+
if len(left.items) > len(right.items) + 1:
862+
return left
863+
num_anys = len(right.items) - len(left.items) + 1
864+
return left.copy_modified(
865+
items=left.items[:left_unpack_index]
866+
+ [left_item] * num_anys
867+
+ left.items[left_unpack_index + 1 :]
868+
)
869+
right_unpack = right.items[right_unpack_index]
870+
assert isinstance(right_unpack, UnpackType)
871+
right_unpacked = get_proper_type(right_unpack.type)
872+
if isinstance(right_unpacked, Instance):
873+
return left
874+
right_prefix = right_unpack_index
875+
right_suffix = len(right.items) - right_prefix - 1
876+
left_prefix = left_unpack_index
877+
left_suffix = len(left.items) - left_prefix - 1
878+
if left_prefix > right_prefix or left_suffix > right_suffix:
879+
return left
880+
new_items = extend_args_for_prefix_and_suffix(
881+
tuple(left.items), right_prefix, right_suffix
882+
)
883+
return left.copy_modified(items=list(new_items))
884+
885+
def get_variadic_item(self, tup: TupleType) -> tuple[int, Type] | None:
886+
"""If this is tuple[X, *tuple[Y, ...], Z], return Y, otherwise None."""
887+
unpack_index = find_unpack_in_list(tup.items)
888+
if unpack_index is None:
889+
return None
890+
unpack = tup.items[unpack_index]
891+
assert isinstance(unpack, UnpackType)
892+
unpacked = get_proper_type(unpack.type)
893+
if not isinstance(unpacked, Instance):
894+
return None
895+
assert unpacked.type.fullname == "builtins.tuple"
896+
return unpack_index, unpacked.args[0]
897+
844898
def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool:
845899
"""Check subtyping between two potentially variadic tuples.
846900
@@ -850,18 +904,11 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool:
850904
Note: the cases where right is fixed or has *Ts unpack should be handled
851905
by the caller.
852906
"""
853-
right_unpack_index = find_unpack_in_list(right.items)
854-
if right_unpack_index is None:
907+
right_variadic = self.get_variadic_item(right)
908+
if right_variadic is None:
855909
# This case should be handled by the caller.
856910
return False
857-
right_unpack = right.items[right_unpack_index]
858-
assert isinstance(right_unpack, UnpackType)
859-
right_unpacked = get_proper_type(right_unpack.type)
860-
if not isinstance(right_unpacked, Instance):
861-
# This case should be handled by the caller.
862-
return False
863-
assert right_unpacked.type.fullname == "builtins.tuple"
864-
right_item = right_unpacked.args[0]
911+
right_unpack_index, right_item = right_variadic
865912
right_prefix = right_unpack_index
866913
right_suffix = len(right.items) - right_prefix - 1
867914
left_unpack_index = find_unpack_in_list(left.items)
@@ -883,16 +930,16 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool:
883930
return False
884931
return all(self._is_subtype(li, right_item) for li in middle)
885932
else:
886-
if len(left.items) < len(right.items):
887-
# There are some items on the left that will never have a matching length
888-
# on the right.
889-
return False
890933
left_prefix = left_unpack_index
891934
left_suffix = len(left.items) - left_prefix - 1
892935
left_unpack = left.items[left_unpack_index]
893936
assert isinstance(left_unpack, UnpackType)
894937
left_unpacked = get_proper_type(left_unpack.type)
895938
if not isinstance(left_unpacked, Instance):
939+
if len(left.items) < len(right.items):
940+
# There are some items on the left that will never have a matching length
941+
# on the right.
942+
return False
896943
# *Ts unpack can't be split, except if it is all mapped to Anys or objects.
897944
if self.is_top_type(right_item):
898945
right_prefix_types, middle, right_suffix_types = split_with_prefix_and_suffix(
@@ -914,6 +961,12 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool:
914961
# subtyping: *each* item on the left, must be a subtype of *some* item on the right.
915962
# For this we first check the "asymptotic case", i.e. that both unpacks a subtypes,
916963
# and then check subtyping for all finite overlaps.
964+
# Note: if the left item is Any we use any() semantics instead of all().
965+
use_any = isinstance(get_proper_type(left_item), AnyType)
966+
if not use_any and len(left.items) < len(right.items):
967+
# There are some items on the left that will never have a matching length
968+
# on the right.
969+
return False
917970
if not self._is_subtype(left_item, right_item):
918971
return False
919972
max_overlap = max(0, right_prefix - left_prefix, right_suffix - left_suffix)
@@ -923,8 +976,11 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool:
923976
repr_items += left.items[-left_suffix:]
924977
left_repr = left.copy_modified(items=repr_items)
925978
if not self._is_subtype(left_repr, right):
926-
return False
927-
return True
979+
if not use_any:
980+
return False
981+
elif use_any:
982+
return True
983+
return not use_any
928984

929985
def is_top_type(self, typ: Type) -> bool:
930986
if not self.proper_subtype and isinstance(get_proper_type(typ), AnyType):
@@ -2384,3 +2440,21 @@ def is_erased_instance(t: Instance) -> bool:
23842440
elif not isinstance(get_proper_type(arg), AnyType):
23852441
return False
23862442
return True
2443+
2444+
2445+
def is_normalized_instance(t: Instance) -> bool:
2446+
"""Is this instance type a normalized representation of a tuple type?
2447+
2448+
Type like class C[T, *Ts](tuple[T, *Ts]) are internally represented as
2449+
tuple types, so that e.g. C[int, str] is tuple[int, str, fallback=C[int, str]].
2450+
However, in case of a variadic argument they are normalized, similar to how
2451+
tuple[*tuple[int, ...]] (TupleType) is normalized to tuple[int, ...] (Instance).
2452+
For example, C[int, *tuple[str, ...]] is represented as an instance. This
2453+
function detects such instances, when they need special-casing.
2454+
"""
2455+
if not t.args:
2456+
return False
2457+
if not t.type.tuple_type:
2458+
return False
2459+
expanded = expand_type_by_instance(t.type.tuple_type, t)
2460+
return isinstance(expanded, Instance)

‎test-data/unit/check-typevar-tuple.test‎

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2917,3 +2917,139 @@ def g(x: K) -> None:
29172917
assert d is not None
29182918
reveal_type(d) # N: Revealed type is "tuple[K | list[int], ...] | tuple[*tuple[int | list[int], ...], int]"
29192919
[builtins fixtures/tuple.pyi]
2920+
2921+
[case testVariadicTupleSubtypingWithAnyBasic]
2922+
from typing import Any, Unpack
2923+
2924+
Ge1 = tuple[int, Unpack[tuple[Any, ...]]]
2925+
2926+
def ge1_to_0(shape: Ge1) -> tuple[()]:
2927+
return shape # E: Incompatible return value type (got "tuple[int, Unpack[tuple[Any, ...]]]", expected "tuple[()]")
2928+
def ge1_to_1(shape: Ge1) -> tuple[int]:
2929+
return shape
2930+
def ge1_to_2(shape: Ge1) -> tuple[int, int]:
2931+
return shape
2932+
[builtins fixtures/tuple.pyi]
2933+
2934+
[case testVariadicTupleSubtypingWithAnyExtra]
2935+
from typing import Any, Unpack
2936+
2937+
def foo(x: tuple[int, str]) -> None: ...
2938+
def bar(x: tuple[int, int]) -> None: ...
2939+
2940+
t1: tuple[int, Unpack[tuple[Any, ...]], str]
2941+
t2: tuple[Unpack[tuple[Any, ...]], int, str]
2942+
t3: tuple[int, str, Unpack[tuple[Any, ...]]]
2943+
t4: tuple[int, Unpack[tuple[Any, ...]], int]
2944+
foo(t1)
2945+
foo(t2)
2946+
foo(t3)
2947+
foo(t4) # E: Argument 1 to "foo" has incompatible type "tuple[int, Unpack[tuple[Any, ...]], int]"; expected "tuple[int, str]"
2948+
2949+
tt1: tuple[int, Unpack[tuple[Any, ...]], int, int]
2950+
tt2: tuple[int, int, Unpack[tuple[Any, ...]], int]
2951+
tt3: tuple[Unpack[tuple[Any, ...]], int]
2952+
tt4: tuple[Unpack[tuple[Any, ...]], str]
2953+
bar(tt1) # E: Argument 1 to "bar" has incompatible type "tuple[int, Unpack[tuple[Any, ...]], int, int]"; expected "tuple[int, int]"
2954+
bar(tt2) # E: Argument 1 to "bar" has incompatible type "tuple[int, int, Unpack[tuple[Any, ...]], int]"; expected "tuple[int, int]"
2955+
bar(tt3)
2956+
bar(tt4) # E: Argument 1 to "bar" has incompatible type "tuple[Unpack[tuple[Any, ...]], str]"; expected "tuple[int, int]"
2957+
[builtins fixtures/tuple.pyi]
2958+
2959+
[case testVariadicTupleSubtypingWithAnyExtraTypeVar]
2960+
from typing import Any, Unpack, TypeVarTuple
2961+
2962+
t1: tuple[int, Unpack[tuple[Any, ...]], int]
2963+
t2: tuple[int, int, Unpack[tuple[Any, ...]], int, int]
2964+
t3: tuple[int, int, int, Unpack[tuple[Any, ...]], int, int, int]
2965+
2966+
Ts = TypeVarTuple("Ts")
2967+
def test(x: tuple[int, int, Unpack[Ts], int, int]) -> None:
2968+
x = t1
2969+
x = t2
2970+
x = t3 # E: Incompatible types in assignment (expression has type "tuple[int, int, int, Unpack[tuple[Any, ...]], int, int, int]", variable has type "tuple[int, int, Unpack[Ts], int, int]")
2971+
[builtins fixtures/tuple.pyi]
2972+
2973+
[case testVariadicTupleSubtypingWithAnyExtraVariadic]
2974+
from typing import Any, Unpack
2975+
2976+
def foo(x: tuple[int, Unpack[tuple[int, ...]], int]) -> None: ...
2977+
2978+
t1: tuple[int, Unpack[tuple[Any, ...]]]
2979+
t2: tuple[Unpack[tuple[Any, ...]], int]
2980+
t3: tuple[Unpack[tuple[Any, ...]], str]
2981+
t4: tuple[Unpack[tuple[Any, ...]], int, int, int]
2982+
2983+
foo(t1)
2984+
foo(t2)
2985+
foo(t3) # E: Argument 1 to "foo" has incompatible type "tuple[Unpack[tuple[Any, ...]], str]"; expected "tuple[int, Unpack[tuple[int, ...]], int]"
2986+
foo(t4)
2987+
[builtins fixtures/tuple.pyi]
2988+
2989+
[case testVariadicTupleSubclassSubtypingWithAny]
2990+
from typing import Any, Generic, Unpack, TypeVar, TypeVarTuple
2991+
2992+
T = TypeVar("T")
2993+
Ts = TypeVarTuple("Ts")
2994+
class Array(tuple[Unpack[Ts]], Generic[T, Unpack[Ts]]): ...
2995+
2996+
Float2D = Array[float, int, int]
2997+
FloatND = Array[float, Unpack[tuple[Any, ...]]]
2998+
2999+
def from_2d(a: Float2D) -> FloatND:
3000+
return a
3001+
3002+
def into_2d(a: FloatND) -> Float2D:
3003+
return a
3004+
[builtins fixtures/tuple.pyi]
3005+
3006+
[case testVariadicInstanceSubtypingWithAny]
3007+
from typing import Any, Generic, Unpack, TypeVarTuple
3008+
3009+
Ts = TypeVarTuple("Ts")
3010+
class C(Generic[Unpack[Ts]]): ...
3011+
3012+
def foo(x: C[int, str]) -> None: ...
3013+
def bar(x: C[int, int]) -> None: ...
3014+
def baz(x: C[int, int, int]) -> None: ...
3015+
3016+
t1: C[int, Unpack[tuple[Any, ...]], str]
3017+
t2: C[Unpack[tuple[Any, ...]], int, str]
3018+
t3: C[int, str, Unpack[tuple[Any, ...]]]
3019+
t4: C[int, Unpack[tuple[Any, ...]], int]
3020+
foo(t1)
3021+
foo(t2)
3022+
foo(t3)
3023+
foo(t4) # E: Argument 1 to "foo" has incompatible type "C[int, Unpack[tuple[Any, ...]], int]"; expected "C[int, str]"
3024+
3025+
tt1: C[int, Unpack[tuple[Any, ...]], int, int]
3026+
tt2: C[int, int, Unpack[tuple[Any, ...]], int]
3027+
tt3: C[Unpack[tuple[Any, ...]], int]
3028+
tt4: C[Unpack[tuple[Any, ...]], str]
3029+
bar(tt1) # E: Argument 1 to "bar" has incompatible type "C[int, Unpack[tuple[Any, ...]], int, int]"; expected "C[int, int]"
3030+
bar(tt2) # E: Argument 1 to "bar" has incompatible type "C[int, int, Unpack[tuple[Any, ...]], int]"; expected "C[int, int]"
3031+
bar(tt3)
3032+
bar(tt4) # E: Argument 1 to "bar" has incompatible type "C[Unpack[tuple[Any, ...]], str]"; expected "C[int, int]"
3033+
3034+
baz(tt3)
3035+
baz(t4)
3036+
[builtins fixtures/tuple.pyi]
3037+
3038+
[case testVariadicInstanceSubtypingWithAnyVariadic]
3039+
from typing import Any, Generic, Unpack, TypeVarTuple
3040+
3041+
Ts = TypeVarTuple("Ts")
3042+
class C(Generic[Unpack[Ts]]): ...
3043+
3044+
def foo(x: C[int, Unpack[tuple[int, ...]], int]) -> None: ...
3045+
3046+
t1: C[int, Unpack[tuple[Any, ...]]]
3047+
t2: C[Unpack[tuple[Any, ...]], int]
3048+
t3: C[Unpack[tuple[Any, ...]], str]
3049+
t4: C[Unpack[tuple[Any, ...]], int, int, int]
3050+
3051+
foo(t1)
3052+
foo(t2)
3053+
foo(t3) # E: Argument 1 to "foo" has incompatible type "C[Unpack[tuple[Any, ...]], str]"; expected "C[int, Unpack[tuple[int, ...]], int]"
3054+
foo(t4)
3055+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL