diff --git a/pyrefly/lib/alt/narrow.rs b/pyrefly/lib/alt/narrow.rs index 39a3ed23c6..b30ce859ca 100644 --- a/pyrefly/lib/alt/narrow.rs +++ b/pyrefly/lib/alt/narrow.rs @@ -437,6 +437,18 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> { }; if narrow_heterogeneous_tuple { Some(self.instantiate_type_var_tuple()) + } else if matches!(right, Type::ClassDef(c) if c == self.stdlib.builtins_type().class_object()) + { + // `isinstance(x, type)` narrows `x` to its class-object part. When `x` is already a + // type-expression value, that part is `type[inner]`: `type[int]` stays precise and + // gradual `type[Any]` stays gradual. + match left { + Type::Type(_) => Some((TParams::empty(), left.clone())), + Type::TypeForm(inner) => { + Some((TParams::empty(), self.heap.mk_type_of((**inner).clone()))) + } + _ => self.unwrap_class_object_silently(right), + } } else { self.unwrap_class_object_silently(right) } diff --git a/pyrefly/lib/test/narrow.rs b/pyrefly/lib/test/narrow.rs index a35777546d..e67d5af64d 100644 --- a/pyrefly/lib/test/narrow.rs +++ b/pyrefly/lib/test/narrow.rs @@ -3205,3 +3205,118 @@ def f(x: tuple[int, str] | int): assert_type(x, tuple[int, str]) "#, ); + +testcase!( + test_isinstance_type_preserves_type_arg, + r#" +from typing import reveal_type +def f(value: type[int] | str): + if isinstance(value, type): + reveal_type(value) # E: revealed type: type[int] +"#, +); + +testcase!( + test_isinstance_type_then_issubclass_typeform, + r#" +from typing import reveal_type +from typing_extensions import TypeForm +def f(value: TypeForm[object]): + if isinstance(value, type) and issubclass(value, int): + reveal_type(value) # E: revealed type: type[int] +"#, +); + +testcase!( + test_isinstance_custom_metaclass_preserved, + r#" +from typing import reveal_type +class Meta(type): + meta_attr: int +def f(value: object): + if isinstance(value, Meta): + reveal_type(value) # E: revealed type: Meta + reveal_type(value.meta_attr) # E: revealed type: int +"#, +); + +testcase!( + test_isinstance_type_else_keeps_non_class, + r#" +from typing import reveal_type +def f(value: type[int] | str): + if isinstance(value, type): + reveal_type(value) # E: revealed type: type[int] + else: + reveal_type(value) # E: revealed type: str +"#, +); + +testcase!( + test_isinstance_type_union_of_type_forms, + r#" +from typing import reveal_type +def f(value: type[int] | type[str] | bytes): + if isinstance(value, type): + reveal_type(value) # E: revealed type: type[int | str] +"#, +); + +testcase!( + test_isinstance_type_preserves_subclass_arg, + r#" +from typing import reveal_type +def f(value: type[bool] | str): + if isinstance(value, type): + reveal_type(value) # E: revealed type: type[bool] +"#, +); + +testcase!( + test_isinstance_type_tuple_with_class, + r#" +from typing import reveal_type +def f(value: type[int] | str | int): + if isinstance(value, (type, int)): + reveal_type(value) # E: revealed type: int | type[int] +"#, +); + +testcase!( + test_isinstance_bare_type_member, + r#" +from typing import reveal_type +def f(value: type | str): + if isinstance(value, type): + reveal_type(value) # E: revealed type: type[Any] +"#, +); + +testcase!( + test_isinstance_type_keeps_gradual_inputs, + r#" +from typing import Any, reveal_type +def from_object(value: object) -> None: + if isinstance(value, type): + reveal_type(value) # E: revealed type: type + value(1, 2, 3) +def from_type_any(value: type[Any]) -> None: + if isinstance(value, type): + reveal_type(value) # E: revealed type: type[Any] + value(1, 2, 3) +"#, +); + +testcase!( + test_issubclass_type_keeps_subclass_semantics, + r#" +from typing import assert_type +def f(x: type[int] | type[type]) -> None: + # issubclass(x, type) asks whether x subclasses builtins.type (is a metaclass), + # so type[int] is excluded and only type[type] survives. + if issubclass(x, type): + assert_type(x, type[type]) + else: + assert_type(x, type[int]) +"#, +);