From 702310a1aae3bcff244d54e360edc2b39ac8e538 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Mon, 5 Jun 2023 22:59:09 -0400 Subject: [PATCH 1/5] Narrow TypeVar to bounded TypeVar --- mypy/checker.py | 2 ++ test-data/unit/check-classes.test | 4 ++-- test-data/unit/check-isinstance.test | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index c1c31538b7de..5c558a4f7d11 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6863,6 +6863,8 @@ def conditional_types( if not type_range.is_upper_bound ] ) + if isinstance(current_type, TypeVarType): + proposed_type = current_type.copy_modified(upper_bound=proposed_type) remaining_type = restrict_subtype_away(current_type, proposed_precise_type) return proposed_type, remaining_type else: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index c2eddbc597a0..648a6c7ac091 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -6648,11 +6648,11 @@ class C(Generic[T]): def meth(self, cls: Type[T]) -> None: if not issubclass(cls, Sub): return - reveal_type(cls) # N: Revealed type is "Type[__main__.Sub]" + reveal_type(cls) # N: Revealed type is "Type[T`1]" def other(self, cls: Type[T]) -> None: if not issubclass(cls, Sub): return - reveal_type(cls) # N: Revealed type is "Type[__main__.Sub]" + reveal_type(cls) # N: Revealed type is "Type[T`1]" [builtins fixtures/isinstancelist.pyi] diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 2d010b8ba38d..ed25ad2a6d4f 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1827,9 +1827,12 @@ T = TypeVar('T', bound=A) def f(x: T) -> None: if isinstance(x, B): - reveal_type(x) # N: Revealed type is "__main__.B" + reveal_type(x) # N: Revealed type is "T`-1" + x1: T = x + b: B = x else: reveal_type(x) # N: Revealed type is "T`-1" + x2: T = x reveal_type(x) # N: Revealed type is "T`-1" [builtins fixtures/isinstance.pyi] From d1364145656da2a4af66ca202cc4eb1706fb00ca Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Mon, 5 Jun 2023 23:08:15 -0400 Subject: [PATCH 2/5] extra check for unreachable code --- test-data/unit/check-isinstance.test | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index ed25ad2a6d4f..4f46520371cc 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1818,6 +1818,7 @@ if issubclass(fm, Baz): [builtins fixtures/isinstance.pyi] [case testIsinstanceAndNarrowTypeVariable] +# flags: --warn-unreachable from typing import TypeVar class A: pass @@ -1830,6 +1831,8 @@ def f(x: T) -> None: reveal_type(x) # N: Revealed type is "T`-1" x1: T = x b: B = x + elif isinstance(x, int): + return # E: Statement is unreachable else: reveal_type(x) # N: Revealed type is "T`-1" x2: T = x From cc3714bc44def9cec7d37764b067ef89ced88563 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Tue, 6 Jun 2023 09:44:53 -0400 Subject: [PATCH 3/5] testIsinstanceAndNarrowTypeVariable: stricter check --- test-data/unit/check-isinstance.test | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 4f46520371cc..91f23f764826 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1823,6 +1823,7 @@ from typing import TypeVar class A: pass class B(A): pass +class C: pass T = TypeVar('T', bound=A) @@ -1830,7 +1831,9 @@ def f(x: T) -> None: if isinstance(x, B): reveal_type(x) # N: Revealed type is "T`-1" x1: T = x + a: A = x b: B = x + c: C = x # E: Incompatible types in assignment (expression has type "T", variable has type "C") elif isinstance(x, int): return # E: Statement is unreachable else: From f3edd218275cefc1aea2efde15e14608ce200700 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Tue, 6 Jun 2023 10:53:18 -0400 Subject: [PATCH 4/5] TypeStrVisitor: show narrowed upper bound --- mypy/checker.py | 5 ++++- mypy/types.py | 9 +++++++-- test-data/unit/check-classes.test | 4 ++-- test-data/unit/check-isinstance.test | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 5c558a4f7d11..8c918c77cea1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6864,7 +6864,10 @@ def conditional_types( ] ) if isinstance(current_type, TypeVarType): - proposed_type = current_type.copy_modified(upper_bound=proposed_type) + assert not current_type.values # constrained TypeVars should not reach here + proposed_type = current_type.copy_modified( + values=[], upper_bound=proposed_type, narrowed=True + ) remaining_type = restrict_subtype_away(current_type, proposed_precise_type) return proposed_type, remaining_type else: diff --git a/mypy/types.py b/mypy/types.py index 53f21e8c0222..6705ef506e6e 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -576,7 +576,7 @@ def has_default(self) -> bool: class TypeVarType(TypeVarLikeType): """Type that refers to a type variable.""" - __slots__ = ("values", "variance") + __slots__ = ("values", "variance", "narrowed") values: list[Type] # Value restriction, empty list if no restriction variance: int @@ -592,11 +592,14 @@ def __init__( variance: int = INVARIANT, line: int = -1, column: int = -1, + *, + narrowed: bool = False, ) -> None: super().__init__(name, fullname, id, upper_bound, default, line, column) assert values is not None, "No restrictions must be represented by empty list" self.values = values self.variance = variance + self.narrowed = narrowed def copy_modified( self, @@ -607,6 +610,7 @@ def copy_modified( id: Bogus[TypeVarId | int] = _dummy, line: int = _dummy_int, column: int = _dummy_int, + narrowed: Bogus[bool] = _dummy, **kwargs: Any, ) -> TypeVarType: return TypeVarType( @@ -619,6 +623,7 @@ def copy_modified( variance=self.variance, line=self.line if line == _dummy_int else line, column=self.column if column == _dummy_int else column, + narrowed=self.narrowed if narrowed is _dummy else narrowed, ) def accept(self, visitor: TypeVisitor[T]) -> T: @@ -3047,7 +3052,7 @@ def visit_type_var(self, t: TypeVarType) -> str: else: # Named type variable type. s = f"{t.name}`{t.id}" - if self.id_mapper and t.upper_bound: + if (self.id_mapper or t.narrowed) and t.upper_bound: s += f"(upper_bound={t.upper_bound.accept(self)})" return s diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 648a6c7ac091..21df6f6acfc9 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -6648,11 +6648,11 @@ class C(Generic[T]): def meth(self, cls: Type[T]) -> None: if not issubclass(cls, Sub): return - reveal_type(cls) # N: Revealed type is "Type[T`1]" + reveal_type(cls) # N: Revealed type is "Type[T`1(upper_bound=__main__.Sub)]" def other(self, cls: Type[T]) -> None: if not issubclass(cls, Sub): return - reveal_type(cls) # N: Revealed type is "Type[T`1]" + reveal_type(cls) # N: Revealed type is "Type[T`1(upper_bound=__main__.Sub)]" [builtins fixtures/isinstancelist.pyi] diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 91f23f764826..badcfac8ae43 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1829,7 +1829,7 @@ T = TypeVar('T', bound=A) def f(x: T) -> None: if isinstance(x, B): - reveal_type(x) # N: Revealed type is "T`-1" + reveal_type(x) # N: Revealed type is "T`-1(upper_bound=__main__.B)" x1: T = x a: A = x b: B = x From b84e3f83b7e20713c9dd92a2fb1a303159e81cc4 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Tue, 6 Jun 2023 10:54:07 -0400 Subject: [PATCH 5/5] add failing testIsinstanceAndNarrowTypeVariableIntersection --- test-data/unit/check-isinstance.test | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index badcfac8ae43..0b4afcb36909 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1842,6 +1842,21 @@ def f(x: T) -> None: reveal_type(x) # N: Revealed type is "T`-1" [builtins fixtures/isinstance.pyi] +[case testIsinstanceAndNarrowTypeVariableIntersection-xfail] +# flags: --warn-unreachable +from typing import TypeVar + +class A: pass +class B: pass + +T = TypeVar('T', bound=A) + +def f(x: T) -> None: + if isinstance(x, B): + reveal_type(x) # N: Revealed type is "T`-1(upper_bound=Union[__main__.A, __main__.B])" + +[builtins fixtures/isinstance.pyi] + [case testIsinstanceAndTypeType] from typing import Type def f(x: Type[int]) -> None: