From 84e3837ffdb9d40366738c1639c18b2788b32b15 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Apr 2024 01:34:33 +0200 Subject: [PATCH 01/19] Keep `TypeVar` arguments when narrowing generic subclasses with `isinstance` and `issubclass`. --- mypy/checker.py | 115 +++++++++++++++++++++- test-data/unit/check-narrowing.test | 146 ++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5d243195d50f..0dfe183f55d1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7454,7 +7454,10 @@ def conditional_types( ] ) remaining_type = restrict_subtype_away(current_type, proposed_precise_type) - return proposed_type, remaining_type + proposed_type_with_data = _transfer_type_var_args_from_current_to_proposed( + current_type, proposed_type + ) + return proposed_type_with_data, remaining_type else: # An isinstance check, but we don't understand the type return current_type, default @@ -7478,6 +7481,116 @@ def conditional_types_to_typemaps( return cast(Tuple[TypeMap, TypeMap], tuple(maps)) +def _transfer_type_var_args_from_current_to_proposed(current: Type, proposed: Type) -> Type: + """Check if the current type is among the bases of the proposed type. If so, try to transfer + the type variable arguments of the current type's instance to a copy of the proposed type's + instance. This increases information when narrowing generic classes so that, for example, + Sequence[int] is narrowed to List[int] instead of List[Any].""" + + def _get_instance_path_from_current_to_proposed( + this: Instance, target: TypeInfo + ) -> list[Instance] | None: + """Search for the current type among the bases of the proposed type and return the + "instance path" from the current to proposed type. Or None, if the current type is not a + nominal super type. At most one path is returned, which means there is no special handling + of (inconsistent) multiple inheritance.""" + if target == this.type: + return [this] + for base in this.type.bases: + path = _get_instance_path_from_current_to_proposed(base, target) + if path is not None: + path.append(this) + return path + return None + + # Handle "tuple of Instance" cases, e.g. `isinstance(x, (A, B))`: + proposed = get_proper_type(proposed) + if isinstance(proposed, UnionType): + items = [ + _transfer_type_var_args_from_current_to_proposed(current, item) + for item in flatten_nested_unions(proposed.items) + ] + return make_simplified_union(items) + + # Otherwise handle only Instances: + if not isinstance(proposed, Instance): + return proposed + + # Handle union cases like `a: A[int] | A[str]; isinstance(a, B)`: + current = get_proper_type(current) + if isinstance(current, UnionType): + items = [ + _transfer_type_var_args_from_current_to_proposed(item, proposed) + for item in flatten_nested_unions(current.items) + ] + return make_simplified_union(items) + + # Here comes the main logic: + if isinstance(current, Instance): + + # Only consider nominal subtyping: + instances = _get_instance_path_from_current_to_proposed(proposed, current.type) + if instances is None: + return proposed + assert len(instances) > 0 # shortest case: proposed type is current type + + # Make a list of the proposed type's type variable arguments that allows to replace each + # `Any` with one type variable argument or multiple type variable tuple arguments of the + # current type: + proposed_args: list[Type | tuple[Type, ...]] = list(proposed.args) + + # Try to transfer each type variable argument from the current to the base type separately: + for pos1, typevar1 in enumerate(instances[0].args): + if isinstance(typevar1, UnpackType): + typevar1 = typevar1.type + if not isinstance(typevar1, (TypeVarType, TypeVarTupleType)): + continue + # Find the position of the intermediate types' and finally the proposed type's + # related type variable (if not available, `pos2` becomes `None`): + for instance in instances[1:]: + pos2: int | None = None + for pos2, typevar2 in enumerate(instance.type.defn.type_vars): + if typevar1 == typevar2: + if instance.type.has_type_var_tuple_type: + assert (prefix := instance.type.type_var_tuple_prefix) is not None + if pos2 > prefix: + pos2 += len(instance.args) - len(instance.type.defn.type_vars) + typevar1 = instance.args[pos2] + if isinstance(typevar1, UnpackType): + typevar1 = typevar1.type + break + else: + pos2 = None + break + + # Transfer the current type's type variable argument or type variable tuple arguments: + if pos2 is not None: + if current.type.has_type_var_tuple_type: + assert (prefix := current.type.type_var_tuple_prefix) is not None + assert (suffix := current.type.type_var_tuple_suffix) is not None + if pos1 < prefix: + proposed_args[pos2] = current.args[pos1] + elif pos1 == prefix: + proposed_args[pos2] = current.args[prefix:len(current.args) - suffix] + else: + middle = len(current.args) - prefix - suffix + proposed_args[pos2] = current.args[pos1 + middle - 1] + else: + proposed_args[pos2] = current.args[pos1] + + # Combine all type variable and type variable tuple arguments to a flat list: + flattened_proposed_args: list[Type] = [] + for arg in proposed_args: + if isinstance(arg, tuple): + flattened_proposed_args.extend(arg) + else: + flattened_proposed_args.append(arg) + + return proposed.copy_modified(args=flattened_proposed_args) + + return proposed + + def gen_unique_name(base: str, table: SymbolTable) -> str: """Generate a name that does not appear in table by appending numbers to base.""" if base not in table: diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 4d117687554e..84d71ca93c62 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2089,3 +2089,149 @@ if isinstance(x, (Z, NoneType)): # E: Subclass of "X" and "Z" cannot exist: "Z" reveal_type(x) # E: Statement is unreachable [builtins fixtures/isinstance.pyi] + +[case testKeepTypeVarArgsWhenNarrowingGenericsWithIsInstance] +from typing import Generic, Sequence, Tuple, TypeVar, Union + +s: Sequence[str] +if isinstance(s, tuple): + reveal_type(s) # N: Revealed type is "builtins.tuple[builtins.str, ...]" +else: + reveal_type(s) # N: Revealed type is "typing.Sequence[builtins.str]" +if isinstance(s, list): + reveal_type(s) # N: Revealed type is "builtins.list[builtins.str]" +else: + reveal_type(s) # N: Revealed type is "typing.Sequence[builtins.str]" + +t1: Tuple[str, int] +if isinstance(t1, tuple): + reveal_type(t1) # N: Revealed type is "Tuple[builtins.str, builtins.int]" +else: + reveal_type(t1) + +t2: Tuple[str, ...] +if isinstance(t2, tuple): + reveal_type(t2) # N: Revealed type is "builtins.tuple[builtins.str, ...]" +else: + reveal_type(t2) + +T1 = TypeVar("T1") +T2 = TypeVar("T2") +class A(Generic[T1]): ... +class B(A[T1], Generic[T1, T2]):... +a: A[str] +if isinstance(a, B): + reveal_type(a) # N: Revealed type is "__main__.B[builtins.str, Any]" +else: + reveal_type(a) # N: Revealed type is "__main__.A[builtins.str]" +class C(A[str], Generic[T1]):... +if isinstance(a, C): + reveal_type(a) # N: Revealed type is "__main__.C[Any]" +else: + reveal_type(a) # N: Revealed type is "__main__.A[builtins.str]" + +class AA(Generic[T1]): ... +class BB(A[T1], AA[T1], Generic[T1, T2]):... +aa: Union[A[int], Union[AA[str], AA[int]]] +if isinstance(aa, BB): + reveal_type(aa) # N: Revealed type is "Union[__main__.BB[builtins.int, Any], __main__.BB[builtins.str, Any]]" +else: + reveal_type(aa) # N: Revealed type is "Union[__main__.A[builtins.int], __main__.AA[builtins.str], __main__.AA[builtins.int]]" + +T3 = TypeVar("T3") +T4 = TypeVar("T4") +T5 = TypeVar("T5") +T6 = TypeVar("T6") +T7 = TypeVar("T7") +T8 = TypeVar("T8") +T9 = TypeVar("T9") +T10 = TypeVar("T10") +T11 = TypeVar("T11") +class A1(Generic[T1, T2]): ... +class A2(Generic[T3, T4]): ... +class B1(A1[T5, T6]):... +class B2(A2[T7, T8]):... +class C1(B1[T9, T10], B2[T11, T9]):... +a2: A2[str, int] +if isinstance(a2, C1): + reveal_type(a2) # N: Revealed type is "__main__.C1[builtins.int, Any, builtins.str]" +else: + reveal_type(a2) # N: Revealed type is "__main__.A2[builtins.str, builtins.int]" +[builtins fixtures/tuple.pyi] + +[case testKeepTypeVarArgsWhenNarrowingGenericsWithIsInstanceAndTuples] +from typing import Generic, TypeVar, Union + +T1 = TypeVar("T1") +T2 = TypeVar("T2") +class A(Generic[T1]): ... +class B(A[T1], Generic[T1, T2]):... +class C(A[T2], Generic[T1, T2]):... +a: Union[A[str], A[int]] +if isinstance(a, (B, C)): + reveal_type(a) # N: Revealed type is "Union[__main__.B[builtins.str, Any], __main__.C[Any, builtins.str], __main__.B[builtins.int, Any], __main__.C[Any, builtins.int]]" +else: + reveal_type(a) # N: Revealed type is "Union[__main__.A[builtins.str], __main__.A[builtins.int]]" +[builtins fixtures/isinstance.pyi] + + +[case testKeepTypeVarArgsWhenNarrowingGenericsWithIsSubclass] +from typing import Generic, Sequence, Type, TypeVar + +T1 = TypeVar("T1") +T2 = TypeVar("T2") +class A(Generic[T1]): ... +class B(A[T1], Generic[T1, T2]):... +a: Type[A[str]] +if issubclass(a, B): + reveal_type(a) # N: Revealed type is "Type[__main__.B[builtins.str, Any]]" +else: + reveal_type(a) # N: Revealed type is "Type[__main__.A[builtins.str]]" +class C(A[str], Generic[T1]):... +if issubclass(a, C): + reveal_type(a) # N: Revealed type is "Type[__main__.C[Any]]" +else: + reveal_type(a) # N: Revealed type is "Type[__main__.A[builtins.str]]" +[builtins fixtures/isinstance.pyi] + +[case testKeepTypeVarTupleArgsWhenNarrowingGenericsWithIsInstance] +from typing import Generic, Sequence, Tuple, TypeVar +from typing_extensions import TypeVarTuple, Unpack + +TP = TypeVarTuple("TP") +class A(Generic[Unpack[TP]]): ... +class B(A[Unpack[TP]]): ... +a: A[str, int] +if isinstance(a, B): + reveal_type(a) # N: Revealed type is "__main__.B[builtins.str, builtins.int]" +else: + reveal_type(a) # N: Revealed type is "__main__.A[builtins.str, builtins.int]" + +def f1(a: A[*Tuple[str, ...]]): + if isinstance(a, B): + reveal_type(a) # N: Revealed type is "__main__.B[Unpack[builtins.tuple[builtins.str, ...]]]" + +T = TypeVar("T") +def f2(a: A[T, str, T]): + if isinstance(a, B): + reveal_type(a) # N: Revealed type is "__main__.B[T`-1, builtins.str, T`-1]" + +T1 = TypeVar("T1") +T2 = TypeVar("T2") +T3 = TypeVar("T3") +T4 = TypeVar("T4") +T5 = TypeVar("T5") +T6 = TypeVar("T6") +class C(Generic[T1, Unpack[TP], T2]): ... +class D(C[T1, Unpack[TP], T2], Generic[T2, T4, T6, Unpack[TP], T5, T3, T1]): ... +class E(D[T1, T2, float, Unpack[TP], float, T3, T4]): ... +c: C[int, str, int, str] +if isinstance(c, E): + reveal_type(c) # N: Revealed type is "__main__.E[builtins.str, Any, builtins.str, builtins.int, Any, builtins.int]" +else: + reveal_type(c) # N: Revealed type is "__main__.C[builtins.int, builtins.str, builtins.int, builtins.str]" + +class F(E[T1, T2, str, int, T3, T4]): ... +if isinstance(c, F): + reveal_type(c) # N: Revealed type is "__main__.F[builtins.str, Any, Any, builtins.int]" +[builtins fixtures/tuple.pyi] From dbcfa1c8f84f01f23addca25fb588cba76913d0f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Apr 2024 07:54:51 +0000 Subject: [PATCH 02/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0dfe183f55d1..3cc331c9bb26 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7571,7 +7571,7 @@ def _get_instance_path_from_current_to_proposed( if pos1 < prefix: proposed_args[pos2] = current.args[pos1] elif pos1 == prefix: - proposed_args[pos2] = current.args[prefix:len(current.args) - suffix] + proposed_args[pos2] = current.args[prefix : len(current.args) - suffix] else: middle = len(current.args) - prefix - suffix proposed_args[pos2] = current.args[pos1 + middle - 1] From 098e066bc676fd76a84fdde2b8862a4dd369a507 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Apr 2024 10:01:54 +0200 Subject: [PATCH 03/19] Remove a cast that is now redundant. --- mypy/types.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mypy/types.py b/mypy/types.py index b4209e9debf4..181fdd5e0ba8 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3578,8 +3578,6 @@ def flatten_nested_unions( """Flatten nested unions in a type list.""" if not isinstance(types, list): typelist = list(types) - else: - typelist = cast("list[Type]", types) # Fast path: most of the time there is nothing to flatten if not any(isinstance(t, (TypeAliasType, UnionType)) for t in typelist): # type: ignore[misc] From 892c74fcf61597c81888f7b15b1ee2f86974076f Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Apr 2024 10:05:45 +0200 Subject: [PATCH 04/19] fix: Remove a cast that is now redundant. --- mypy/types.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/types.py b/mypy/types.py index 181fdd5e0ba8..09e69a7e92ab 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3577,14 +3577,14 @@ def flatten_nested_unions( ) -> list[Type]: """Flatten nested unions in a type list.""" if not isinstance(types, list): - typelist = list(types) + types = list(types) # Fast path: most of the time there is nothing to flatten - if not any(isinstance(t, (TypeAliasType, UnionType)) for t in typelist): # type: ignore[misc] - return typelist + if not any(isinstance(t, (TypeAliasType, UnionType)) for t in types): # type: ignore[misc] + return types flat_items: list[Type] = [] - for t in typelist: + for t in types: tp = get_proper_type(t) if handle_type_alias_type else t if isinstance(tp, ProperType) and isinstance(tp, UnionType): flat_items.extend( From c24cc53bb2c902e88f04bde3242358ff6c32661e Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Apr 2024 10:43:50 +0200 Subject: [PATCH 05/19] Use Unpack in testKeepTypeVarTupleArgsWhenNarrowingGenericsWithIsInstance for consistency with Python < 3.11 --- test-data/unit/check-narrowing.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 84d71ca93c62..554ecf75558f 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2207,7 +2207,7 @@ if isinstance(a, B): else: reveal_type(a) # N: Revealed type is "__main__.A[builtins.str, builtins.int]" -def f1(a: A[*Tuple[str, ...]]): +def f1(a: A[Unpack[Tuple[str, ...]]]): if isinstance(a, B): reveal_type(a) # N: Revealed type is "__main__.B[Unpack[builtins.tuple[builtins.str, ...]]]" From d0136739e1c41102f156c5a20b5572e0ea272c55 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Apr 2024 10:48:03 +0200 Subject: [PATCH 06/19] fix "local variable 'pos2' referenced before assignment" --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 3cc331c9bb26..9efc3f8d02b8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7547,8 +7547,8 @@ def _get_instance_path_from_current_to_proposed( continue # Find the position of the intermediate types' and finally the proposed type's # related type variable (if not available, `pos2` becomes `None`): + pos2: int | None = pos1 for instance in instances[1:]: - pos2: int | None = None for pos2, typevar2 in enumerate(instance.type.defn.type_vars): if typevar1 == typevar2: if instance.type.has_type_var_tuple_type: From a94db4ad9b40498165fcfbecdf3b287ccc2701b9 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Apr 2024 11:30:05 +0200 Subject: [PATCH 07/19] restart job --- mypy/checker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/checker.py b/mypy/checker.py index 126eab7640bb..27bdb72b63e5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7679,6 +7679,7 @@ def builtin_item_type(tp: Type) -> Type | None: "_collections_abc.dict_keys", "typing.KeysView", ]: + restart job if not tp.args: # TODO: fix tuple in lib-stub/builtins.pyi (it should be generic). return None From a700294c4d6771d0f55f61a5354514ae3f0e56eb Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Fri, 5 Apr 2024 11:30:14 +0200 Subject: [PATCH 08/19] Revert "restart job" This reverts commit a94db4ad9b40498165fcfbecdf3b287ccc2701b9. --- mypy/checker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 27bdb72b63e5..126eab7640bb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7679,7 +7679,6 @@ def builtin_item_type(tp: Type) -> Type | None: "_collections_abc.dict_keys", "typing.KeysView", ]: - restart job if not tp.args: # TODO: fix tuple in lib-stub/builtins.pyi (it should be generic). return None From 73da8b6a9715f8a000e1d17dfb39957b1e67cc6c Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 6 Apr 2024 01:08:53 +0200 Subject: [PATCH 09/19] remove empty line --- test-data/unit/check-narrowing.test | 1 - 1 file changed, 1 deletion(-) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 554ecf75558f..dccd12cb799f 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2174,7 +2174,6 @@ else: reveal_type(a) # N: Revealed type is "Union[__main__.A[builtins.str], __main__.A[builtins.int]]" [builtins fixtures/isinstance.pyi] - [case testKeepTypeVarArgsWhenNarrowingGenericsWithIsSubclass] from typing import Generic, Sequence, Type, TypeVar From 325f19072a96608a2a37555b626a81f77a71961c Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 6 Apr 2024 01:11:49 +0200 Subject: [PATCH 10/19] avoid unnecessary union members when proposed type is current type (without crashing pattern matching) --- mypy/checker.py | 4 ++-- test-data/unit/check-narrowing.test | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 126eab7640bb..97eae10c816e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7560,7 +7560,7 @@ def _get_instance_path_from_current_to_proposed( for pos1, typevar1 in enumerate(instances[0].args): if isinstance(typevar1, UnpackType): typevar1 = typevar1.type - if not isinstance(typevar1, (TypeVarType, TypeVarTupleType)): + if (len(instances) > 1) and not isinstance(typevar1, (TypeVarType, TypeVarTupleType)): continue # Find the position of the intermediate types' and finally the proposed type's # related type variable (if not available, `pos2` becomes `None`): @@ -7581,7 +7581,7 @@ def _get_instance_path_from_current_to_proposed( break # Transfer the current type's type variable argument or type variable tuple arguments: - if pos2 is not None: + if (pos2 is not None) and isinstance(proposed_args[pos2], (AnyType, UnpackType)): if current.type.has_type_var_tuple_type: assert (prefix := current.type.type_var_tuple_prefix) is not None assert (suffix := current.type.type_var_tuple_suffix) is not None diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index dccd12cb799f..b08570c5d855 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2234,3 +2234,13 @@ class F(E[T1, T2, str, int, T3, T4]): ... if isinstance(c, F): reveal_type(c) # N: Revealed type is "__main__.F[builtins.str, Any, Any, builtins.int]" [builtins fixtures/tuple.pyi] + +[case testKeepTypeVarArgsWhenNarrowingGenericsWithIsInstanceMappingIterableOverlap] +# flags: --python-version 3.12 +# see PR 17099 +from typing import Iterable + +def f(x: dict[str, str] | Iterable[bytes]) -> None: + if isinstance(x, dict): + reveal_type(x) # N: Revealed type is "Union[builtins.dict[builtins.str, builtins.str], builtins.dict[builtins.bytes, Any]]" +[builtins fixtures/dict.pyi] From bb63532b828f440cf8535539c6448fa9db87f24a Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 6 Apr 2024 01:16:55 +0200 Subject: [PATCH 11/19] fix: avoid unnecessary union members when proposed type is current type (without crashing pattern matching) --- mypy/checker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 97eae10c816e..1bc3affc37eb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7581,7 +7581,10 @@ def _get_instance_path_from_current_to_proposed( break # Transfer the current type's type variable argument or type variable tuple arguments: - if (pos2 is not None) and isinstance(proposed_args[pos2], (AnyType, UnpackType)): + if ( + (pos2 is not None) and + isinstance(get_proper_type(proposed_args[pos2]), (AnyType, UnpackType)) + ): if current.type.has_type_var_tuple_type: assert (prefix := current.type.type_var_tuple_prefix) is not None assert (suffix := current.type.type_var_tuple_suffix) is not None From 18bae8357bbc7f665a277d3a96e8e4c0e363eb36 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:17:26 +0000 Subject: [PATCH 12/19] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1bc3affc37eb..2d4ae8ffc514 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7581,9 +7581,8 @@ def _get_instance_path_from_current_to_proposed( break # Transfer the current type's type variable argument or type variable tuple arguments: - if ( - (pos2 is not None) and - isinstance(get_proper_type(proposed_args[pos2]), (AnyType, UnpackType)) + if (pos2 is not None) and isinstance( + get_proper_type(proposed_args[pos2]), (AnyType, UnpackType) ): if current.type.has_type_var_tuple_type: assert (prefix := current.type.type_var_tuple_prefix) is not None From afa54504d9cbadd36a6f0345f6a4dd8d4ebebe68 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 6 Apr 2024 01:28:53 +0200 Subject: [PATCH 13/19] fix: avoid unnecessary union members when proposed type is current type (without crashing pattern matching) --- mypy/checker.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2d4ae8ffc514..6a4e90e37544 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7569,8 +7569,8 @@ def _get_instance_path_from_current_to_proposed( for pos2, typevar2 in enumerate(instance.type.defn.type_vars): if typevar1 == typevar2: if instance.type.has_type_var_tuple_type: - assert (prefix := instance.type.type_var_tuple_prefix) is not None - if pos2 > prefix: + assert (pre := instance.type.type_var_tuple_prefix) is not None + if pos2 > pre: pos2 += len(instance.args) - len(instance.type.defn.type_vars) typevar1 = instance.args[pos2] if isinstance(typevar1, UnpackType): @@ -7581,21 +7581,22 @@ def _get_instance_path_from_current_to_proposed( break # Transfer the current type's type variable argument or type variable tuple arguments: - if (pos2 is not None) and isinstance( - get_proper_type(proposed_args[pos2]), (AnyType, UnpackType) - ): - if current.type.has_type_var_tuple_type: - assert (prefix := current.type.type_var_tuple_prefix) is not None - assert (suffix := current.type.type_var_tuple_suffix) is not None - if pos1 < prefix: - proposed_args[pos2] = current.args[pos1] - elif pos1 == prefix: - proposed_args[pos2] = current.args[prefix : len(current.args) - suffix] + if pos2 is not None: + proposed_arg = proposed_args[pos2] + assert not isinstance(proposed_arg, tuple) + if isinstance(get_proper_type(proposed_arg), (AnyType, UnpackType)): + if current.type.has_type_var_tuple_type: + assert (pre := current.type.type_var_tuple_prefix) is not None + assert (suf := current.type.type_var_tuple_suffix) is not None + if pos1 < pre: + proposed_args[pos2] = current.args[pos1] + elif pos1 == pre: + proposed_args[pos2] = current.args[pre : len(current.args) - suf] + else: + middle = len(current.args) - pre - suf + proposed_args[pos2] = current.args[pos1 + middle - 1] else: - middle = len(current.args) - prefix - suffix - proposed_args[pos2] = current.args[pos1 + middle - 1] - else: - proposed_args[pos2] = current.args[pos1] + proposed_args[pos2] = current.args[pos1] # Combine all type variable and type variable tuple arguments to a flat list: flattened_proposed_args: list[Type] = [] From 9769638b7520282c9be46f1c9647d0f0bcd8ee13 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 6 Apr 2024 11:03:47 +0200 Subject: [PATCH 14/19] apply one more union flattening to avoid (good or bad?) changes in narrowing revealed by Mypy primer and pysparsk --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6a4e90e37544..d6e87feba49c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7606,7 +7606,7 @@ def _get_instance_path_from_current_to_proposed( else: flattened_proposed_args.append(arg) - return proposed.copy_modified(args=flattened_proposed_args) + return proposed.copy_modified(args=flatten_nested_unions(flattened_proposed_args)) return proposed From 674117522de3b6ca53d16664aaa49029698e469f Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Sat, 6 Apr 2024 12:38:00 +0200 Subject: [PATCH 15/19] fix: apply one more union flattening to avoid (good or bad?) changes in narrowing revealed by Mypy primer and pysparsk --- mypy/checker.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index d6e87feba49c..6b1eff8d404a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7605,8 +7605,12 @@ def _get_instance_path_from_current_to_proposed( flattened_proposed_args.extend(arg) else: flattened_proposed_args.append(arg) + # Some later checks seem to expect flattened unions: + for arg_ in flattened_proposed_args: + if isinstance(arg_ := get_proper_type(arg_), UnionType): + arg_.items = flatten_nested_unions(arg_.items) - return proposed.copy_modified(args=flatten_nested_unions(flattened_proposed_args)) + return proposed.copy_modified(args=flattened_proposed_args) return proposed From 463c7d452e2931febc362562666ff075bb8d0ff0 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 16 Apr 2024 20:07:55 +0200 Subject: [PATCH 16/19] Change test name testKeepTypeVarArgsWhenNarrowingGenericsWithIsInstanceAndTuples to testKeepTypeVarArgsWhenNarrowingGenericsInUnionsWithIsInstance --- test-data/unit/check-narrowing.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index b08570c5d855..06b76f2aa2f4 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2159,7 +2159,7 @@ else: reveal_type(a2) # N: Revealed type is "__main__.A2[builtins.str, builtins.int]" [builtins fixtures/tuple.pyi] -[case testKeepTypeVarArgsWhenNarrowingGenericsWithIsInstanceAndTuples] +[case testKeepTypeVarArgsWhenNarrowingGenericsInUnionsWithIsInstance] from typing import Generic, TypeVar, Union T1 = TypeVar("T1") From 6195de14004619747b19a900bcd68185d48f28ba Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 16 Apr 2024 20:26:45 +0200 Subject: [PATCH 17/19] special handling of TupleType --- mypy/checker.py | 5 +++++ test-data/unit/check-narrowing.test | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 6b1eff8d404a..1fd93c0010f8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7542,6 +7542,11 @@ def _get_instance_path_from_current_to_proposed( ] return make_simplified_union(items) + # Special handling for trivial "tuple is tuple" cases (handling tuple subclasses seems + # complicated, especially as long as `builtins.tuple` is not variadic): + if isinstance(current, TupleType) and (proposed.type.fullname == "builtins.tuple"): + return current + # Here comes the main logic: if isinstance(current, Instance): diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 06b76f2aa2f4..2e74a92377b1 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2174,6 +2174,18 @@ else: reveal_type(a) # N: Revealed type is "Union[__main__.A[builtins.str], __main__.A[builtins.int]]" [builtins fixtures/isinstance.pyi] +[case testKeepTypeVarArgsWhenNarrowingTupleTypeToTuple] +from typing import Sequence, Tuple, Union + +class A: ... +class B: ... +x: Union[Tuple[A], Tuple[A, B], Tuple[B, ...], Sequence[Tuple[A]]] +if isinstance(x, tuple): + reveal_type(x) # N: Revealed type is "Union[Tuple[__main__.A], Tuple[__main__.A, __main__.B], builtins.tuple[__main__.B, ...], builtins.tuple[Tuple[__main__.A], ...]]" +else: + reveal_type(x) # N: Revealed type is "typing.Sequence[Tuple[__main__.A]]" +[builtins fixtures/tuple.pyi] + [case testKeepTypeVarArgsWhenNarrowingGenericsWithIsSubclass] from typing import Generic, Sequence, Type, TypeVar From 116b70ec9dfc84781a665e6532602a3ec79ea7d0 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 16 Apr 2024 21:09:41 +0200 Subject: [PATCH 18/19] restart Mypy primer --- mypy/checker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 1fd93c0010f8..e625e1702f1e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7542,6 +7542,8 @@ def _get_instance_path_from_current_to_proposed( ] return make_simplified_union(items) + restart mypy primer + # Special handling for trivial "tuple is tuple" cases (handling tuple subclasses seems # complicated, especially as long as `builtins.tuple` is not variadic): if isinstance(current, TupleType) and (proposed.type.fullname == "builtins.tuple"): From ac4b9dc71cf899a02d6909f4c16ed11bc059efff Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Tue, 16 Apr 2024 21:10:02 +0200 Subject: [PATCH 19/19] Revert "restart Mypy primer" This reverts commit 116b70ec9dfc84781a665e6532602a3ec79ea7d0. --- mypy/checker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index e625e1702f1e..1fd93c0010f8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7542,8 +7542,6 @@ def _get_instance_path_from_current_to_proposed( ] return make_simplified_union(items) - restart mypy primer - # Special handling for trivial "tuple is tuple" cases (handling tuple subclasses seems # complicated, especially as long as `builtins.tuple` is not variadic): if isinstance(current, TupleType) and (proposed.type.fullname == "builtins.tuple"):