diff --git a/mypy/checker.py b/mypy/checker.py index 1b57ef780104..1dc87f8e2ab2 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7388,6 +7388,10 @@ def conditional_types( # Expression is never of any type in proposed_type_ranges return UninhabitedType(), default else: + if isinstance(current_type, TypeVarType): + # narrowing T`-1 to int should *really* return T`-1 with a narrowed upper bound! + proposed_type = current_type.copy_modified(upper_bound=proposed_type) + # we can only restrict when the type is precise, not bounded proposed_precise_type = UnionType.make_union( [ diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 4fd3f8ff98ca..b47462058f89 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1899,7 +1899,7 @@ def restrict_subtype_away(t: Type, s: Type) -> Type: """Return t minus s for runtime type assertions. If we can't determine a precise result, return a supertype of the - ideal result (just t is a valid result). + ideal result (i.e. just t is a valid result). This is used for type inference of runtime type checks such as isinstance(). Currently, this just removes elements of a union type. @@ -1914,6 +1914,11 @@ def restrict_subtype_away(t: Type, s: Type) -> Type: if (isinstance(get_proper_type(item), AnyType) or not covers_at_runtime(item, s)) ] return UnionType.make_union(new_items) + elif isinstance(p_t, TypeVarType): + # TODO: should this check if `p_t.upper_bound` is covered at runtime? + # NOTE: I don't think I'm allowed to return proper types like this. But + # I don't know what else I could do. + return p_t.copy_modified(upper_bound=restrict_subtype_away(p_t.upper_bound, s)) elif covers_at_runtime(t, s): return UninhabitedType() else: diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 983cb8454a05..5dd5ea3437da 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -6685,11 +6685,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 b7ee38b69d00..e8dd7c0ebfd8 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1821,13 +1821,15 @@ if issubclass(fm, Baz): from typing import TypeVar class A: pass -class B(A): pass +class B(A): + z: int 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.z) # N: Revealed type is "builtins.int" + reveal_type(x) # N: Revealed type is "T`-1" else: reveal_type(x) # N: Revealed type is "T`-1" reveal_type(x) # N: Revealed type is "T`-1" @@ -2892,3 +2894,19 @@ if hasattr(mod, "y"): [file mod.py] def __getattr__(attr: str) -> str: ... [builtins fixtures/module.pyi] + +[case testFunctionCanReturnNarrowedTypeVar] +from typing import TypeVar + +T = TypeVar("T") + +def f(x: T) -> T: + if isinstance(x, int): + reveal_type(x) # N: Revealed type is "T`-1" + x + 42 + return x + else: + reveal_type(x) # N: Revealed type is "T`-1" + x + 42 # E: Unsupported left operand type for + ("T") + return x +[builtins fixtures/isinstancelist.pyi]