diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 48df6acd30..b7e49971a0 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -147,6 +147,25 @@ def _(x: int | str | bytes): reveal_type(x) # revealed: (int & Unknown) | (str & Unknown) | (bytes & Unknown) ``` +## `classinfo` is a `typing.py` special form + +Certain special forms in `typing.py` are aliases to classes elsewhere in the standard library; these +can be used in `isinstance()` and `issubclass()` checks. We support narrowing using them: + +```py +import typing as t + +def f(x: dict[str, int] | list[str], y: object): + if isinstance(x, t.Dict): + reveal_type(x) # revealed: dict[str, int] + else: + reveal_type(x) # revealed: list[str] + + if isinstance(y, t.Callable): + # TODO: a better top-materialization for `Callable`s (https://github.com/astral-sh/ty/issues/1426) + reveal_type(y) # revealed: () -> object +``` + ## Class types ```py diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 8dc6f2f626..e89822b182 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -11,9 +11,9 @@ use crate::types::enums::{enum_member_literals, enum_metadata}; use crate::types::function::KnownFunction; use crate::types::infer::infer_same_file_expression_type; use crate::types::{ - ClassLiteral, ClassType, IntersectionBuilder, KnownClass, KnownInstanceType, SpecialFormType, - SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext, TypeVarBoundOrConstraints, - UnionBuilder, infer_expression_types, + CallableType, ClassLiteral, ClassType, IntersectionBuilder, KnownClass, KnownInstanceType, + SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext, + TypeVarBoundOrConstraints, UnionBuilder, infer_expression_types, }; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; @@ -229,6 +229,18 @@ impl ClassInfoConstraintFunction { ) } + // We don't have a good meta-type for `Callable`s right now, + // so only apply `isinstance()` narrowing, not `issubclass()` + Type::SpecialForm(SpecialFormType::Callable) + if self == ClassInfoConstraintFunction::IsInstance => + { + Some(CallableType::unknown(db).top_materialization(db)) + } + + Type::SpecialForm(special_form) => special_form + .aliased_stdlib_class() + .and_then(|class| self.generate_constraint(db, class.to_class_literal(db))), + Type::AlwaysFalsy | Type::AlwaysTruthy | Type::BooleanLiteral(_) @@ -244,7 +256,6 @@ impl ClassInfoConstraintFunction { | Type::FunctionLiteral(_) | Type::ProtocolInstance(_) | Type::PropertyInstance(_) - | Type::SpecialForm(_) | Type::LiteralString | Type::StringLiteral(_) | Type::IntLiteral(_) diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index c8e37d5143..54d9640b87 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -328,6 +328,61 @@ impl SpecialFormType { } } + /// Return `Some(KnownClass)` if this special form is an alias + /// to a standard library class. + pub(super) const fn aliased_stdlib_class(self) -> Option { + match self { + Self::List => Some(KnownClass::List), + Self::Dict => Some(KnownClass::Dict), + Self::Set => Some(KnownClass::Set), + Self::FrozenSet => Some(KnownClass::FrozenSet), + Self::ChainMap => Some(KnownClass::ChainMap), + Self::Counter => Some(KnownClass::Counter), + Self::DefaultDict => Some(KnownClass::DefaultDict), + Self::Deque => Some(KnownClass::Deque), + Self::OrderedDict => Some(KnownClass::OrderedDict), + Self::Tuple => Some(KnownClass::Tuple), + Self::Type => Some(KnownClass::Type), + + Self::AlwaysFalsy + | Self::AlwaysTruthy + | Self::Annotated + | Self::Bottom + | Self::CallableTypeOf + | Self::ClassVar + | Self::Concatenate + | Self::Final + | Self::Intersection + | Self::Literal + | Self::LiteralString + | Self::Never + | Self::NoReturn + | Self::Not + | Self::ReadOnly + | Self::Required + | Self::TypeAlias + | Self::TypeGuard + | Self::NamedTuple + | Self::NotRequired + | Self::Optional + | Self::Top + | Self::TypeIs + | Self::TypedDict + | Self::TypingSelf + | Self::Union + | Self::Unknown + | Self::TypeOf + | Self::Any + // `typing.Callable` is an alias to `collections.abc.Callable`, + // but they're both the same `SpecialFormType` in our model, + // and neither is a class in typeshed (even though the `collections.abc` one is at runtime) + | Self::Callable + | Self::Protocol + | Self::Generic + | Self::Unpack => None, + } + } + /// Return `true` if this special form is valid as the second argument /// to `issubclass()` and `isinstance()` calls. pub(super) const fn is_valid_isinstance_target(self) -> bool {