diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md index 6eb475715a485..17fde60063e88 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/isinstance.md @@ -211,3 +211,100 @@ def f( else: reveal_type(d) # revealed: P & ~AlwaysFalsy ``` + +## Narrowing if an object of type `Any` or `Unknown` is used as the second argument + +In order to preserve the gradual guarantee, we intersect with the type of the second argument if the +type of the second argument is a dynamic type: + +```py +from typing import Any +from something_unresolvable import SomethingUnknown # error: [unresolved-import] + +class Foo: ... + +def f(a: Foo, b: Any): + if isinstance(a, SomethingUnknown): + reveal_type(a) # revealed: Foo & Unknown + + if isinstance(a, b): + reveal_type(a) # revealed: Foo & Any +``` + +## Narrowing if an object with an intersection/union/TypeVar type is used as the second argument + +If an intersection with only positive members is used as the second argument, and all positive +members of the intersection are valid arguments for the second argument to `isinstance()`, we +intersect with each positive member of the intersection: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any +from ty_extensions import Intersection + +class Foo: ... + +class Bar: + attribute: int + +class Baz: + attribute: str + +def f(x: Foo, y: Intersection[type[Bar], type[Baz]], z: type[Any]): + if isinstance(x, y): + reveal_type(x) # revealed: Foo & Bar & Baz + + if isinstance(x, z): + reveal_type(x) # revealed: Foo & Any +``` + +The same if a union type is used: + +```py +def g(x: Foo, y: type[Bar | Baz]): + if isinstance(x, y): + reveal_type(x) # revealed: (Foo & Bar) | (Foo & Baz) +``` + +And even if a `TypeVar` is used, providing it has valid upper bounds/constraints: + +```py +from typing import TypeVar + +T = TypeVar("T", bound=type[Bar]) + +def h_old_syntax(x: Foo, y: T) -> T: + if isinstance(x, y): + reveal_type(x) # revealed: Foo & Bar + reveal_type(x.attribute) # revealed: int + + return y + +def h[U: type[Bar | Baz]](x: Foo, y: U) -> U: + if isinstance(x, y): + reveal_type(x) # revealed: (Foo & Bar) | (Foo & Baz) + reveal_type(x.attribute) # revealed: int | str + + return y +``` + +Or even a tuple of tuple of typevars that have intersection bounds... + +```py +from ty_extensions import Intersection + +class Spam: ... +class Eggs: ... +class Ham: ... +class Mushrooms: ... + +def i[T: Intersection[type[Bar], type[Baz | Spam]], U: (type[Eggs], type[Ham])](x: Foo, y: T, z: U) -> tuple[T, U]: + if isinstance(x, (y, (z, Mushrooms))): + reveal_type(x) # revealed: (Foo & Bar & Baz) | (Foo & Bar & Spam) | (Foo & Eggs) | (Foo & Ham) | (Foo & Mushrooms) + + return (y, z) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md index 6f8d8e82a9f3f..ce77126d32156 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -278,3 +278,82 @@ def _(x: type[UsesMeta1], y: type[UsesMeta2]): else: reveal_type(y) # revealed: type[UsesMeta2] ``` + +## Narrowing if an object with an intersection/union/TypeVar type is used as the second argument + +If an intersection with only positive members is used as the second argument, and all positive +members of the intersection are valid arguments for the second argument to `isinstance()`, we +intersect with each positive member of the intersection: + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Any, ClassVar +from ty_extensions import Intersection + +class Foo: ... + +class Bar: + attribute: ClassVar[int] + +class Baz: + attribute: ClassVar[str] + +def f(x: type[Foo], y: Intersection[type[Bar], type[Baz]], z: type[Any]): + if issubclass(x, y): + reveal_type(x) # revealed: type[Foo] & type[Bar] & type[Baz] + + if issubclass(x, z): + reveal_type(x) # revealed: type[Foo] & Any +``` + +The same if a union type is used: + +```py +def g(x: type[Foo], y: type[Bar | Baz]): + if issubclass(x, y): + reveal_type(x) # revealed: (type[Foo] & type[Bar]) | (type[Foo] & type[Baz]) +``` + +And even if a `TypeVar` is used, providing it has valid upper bounds/constraints: + +```py +from typing import TypeVar + +T = TypeVar("T", bound=type[Bar]) + +def h_old_syntax(x: type[Foo], y: T) -> T: + if issubclass(x, y): + reveal_type(x) # revealed: type[Foo] & type[Bar] + reveal_type(x.attribute) # revealed: int + + return y + +def h[U: type[Bar | Baz]](x: type[Foo], y: U) -> U: + if issubclass(x, y): + reveal_type(x) # revealed: (type[Foo] & type[Bar]) | (type[Foo] & type[Baz]) + reveal_type(x.attribute) # revealed: int | str + + return y +``` + +Or even a tuple of tuple of typevars that have intersection bounds... + +```py +from ty_extensions import Intersection + +class Spam: ... +class Eggs: ... +class Ham: ... +class Mushrooms: ... + +def i[T: Intersection[type[Bar], type[Baz | Spam]], U: (type[Eggs], type[Ham])](x: type[Foo], y: T, z: U) -> tuple[T, U]: + if issubclass(x, (y, (z, Mushrooms))): + # revealed: (type[Foo] & type[Bar] & type[Baz]) | (type[Foo] & type[Bar] & type[Spam]) | (type[Foo] & type[Eggs]) | (type[Foo] & type[Ham]) | (type[Foo] & type[Mushrooms]) + reveal_type(x) + + return (y, z) +``` diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 09517ef03e0c6..91296446f7705 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -9,8 +9,8 @@ use crate::semantic_index::predicate::{ use crate::types::function::KnownFunction; use crate::types::infer::infer_same_file_expression_type; use crate::types::{ - IntersectionBuilder, KnownClass, SubclassOfType, Truthiness, Type, UnionBuilder, - infer_expression_types, + ClassLiteral, ClassType, IntersectionBuilder, KnownClass, SubclassOfInner, SubclassOfType, + Truthiness, Type, TypeVarBoundOrConstraints, UnionBuilder, infer_expression_types, }; use ruff_db::parsed::{ParsedModuleRef, parsed_module}; @@ -167,9 +167,13 @@ impl ClassInfoConstraintFunction { /// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604 /// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type. fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option> { - let constraint_fn = |class| match self { - ClassInfoConstraintFunction::IsInstance => Type::instance(db, class), - ClassInfoConstraintFunction::IsSubclass => SubclassOfType::from(db, class), + let constraint_fn = |class: ClassLiteral<'db>| match self { + ClassInfoConstraintFunction::IsInstance => { + Type::instance(db, class.default_specialization(db)) + } + ClassInfoConstraintFunction::IsSubclass => { + SubclassOfType::from(db, class.default_specialization(db)) + } }; match classinfo { @@ -186,13 +190,70 @@ impl ClassInfoConstraintFunction { if class_literal.is_known(db, KnownClass::Any) { None } else { - Some(constraint_fn(class_literal.default_specialization(db))) + Some(constraint_fn(class_literal)) + } + } + Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { + SubclassOfInner::Class(ClassType::NonGeneric(class)) => Some(constraint_fn(class)), + // It's not valid to use a generic alias as the second argument to `isinstance()` or `issubclass()`, + // e.g. `isinstance(x, list[int])` fails at runtime. + SubclassOfInner::Class(ClassType::Generic(_)) => None, + SubclassOfInner::Dynamic(dynamic) => Some(Type::Dynamic(dynamic)), + }, + Type::Dynamic(_) => Some(classinfo), + Type::Intersection(intersection) => { + if intersection.negative(db).is_empty() { + let mut builder = IntersectionBuilder::new(db); + for element in intersection.positive(db) { + builder = builder.add_positive(self.generate_constraint(db, *element)?); + } + Some(builder.build()) + } else { + // TODO: can we do better here? + None } } - Type::SubclassOf(subclass_of_ty) => { - subclass_of_ty.subclass_of().into_class().map(constraint_fn) + Type::Union(union) => { + let mut builder = UnionBuilder::new(db); + for element in union.elements(db) { + builder = builder.add(self.generate_constraint(db, *element)?); + } + Some(builder.build()) } - _ => None, + Type::TypeVar(type_var) => match type_var.bound_or_constraints(db)? { + TypeVarBoundOrConstraints::UpperBound(bound) => self.generate_constraint(db, bound), + TypeVarBoundOrConstraints::Constraints(constraints) => { + self.generate_constraint(db, Type::Union(constraints)) + } + }, + + // It's not valid to use a generic alias as the second argument to `isinstance()` or `issubclass()`, + // e.g. `isinstance(x, list[int])` fails at runtime. + Type::GenericAlias(_) => None, + + Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::BooleanLiteral(_) + | Type::BoundMethod(_) + | Type::BoundSuper(_) + | Type::BytesLiteral(_) + | Type::Callable(_) + | Type::DataclassDecorator(_) + | Type::Never + | Type::MethodWrapper(_) + | Type::ModuleLiteral(_) + | Type::FunctionLiteral(_) + | Type::ProtocolInstance(_) + | Type::PropertyInstance(_) + | Type::SpecialForm(_) + | Type::NominalInstance(_) + | Type::LiteralString + | Type::StringLiteral(_) + | Type::IntLiteral(_) + | Type::KnownInstance(_) + | Type::TypeIs(_) + | Type::WrapperDescriptor(_) + | Type::DataclassTransformer(_) => None, } } }