Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pyrefly/lib/alt/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
115 changes: 115 additions & 0 deletions pyrefly/lib/test/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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])
"#,
);
Loading