mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 12:16:43 +00:00
[ty] Support isinstance() and issubclass() narrowing when the second argument is a typing.py stdlib alias (#21391)
## Summary A followup to https://github.com/astral-sh/ruff/pull/21386 ## Test Plan New mdtests added
This commit is contained in:
parent
4373974dd9
commit
43297d3455
3 changed files with 89 additions and 4 deletions
|
|
@ -147,6 +147,25 @@ def _(x: int | str | bytes):
|
||||||
reveal_type(x) # revealed: (int & Unknown) | (str & Unknown) | (bytes & Unknown)
|
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
|
## Class types
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@ use crate::types::enums::{enum_member_literals, enum_metadata};
|
||||||
use crate::types::function::KnownFunction;
|
use crate::types::function::KnownFunction;
|
||||||
use crate::types::infer::infer_same_file_expression_type;
|
use crate::types::infer::infer_same_file_expression_type;
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
ClassLiteral, ClassType, IntersectionBuilder, KnownClass, KnownInstanceType, SpecialFormType,
|
CallableType, ClassLiteral, ClassType, IntersectionBuilder, KnownClass, KnownInstanceType,
|
||||||
SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext, TypeVarBoundOrConstraints,
|
SpecialFormType, SubclassOfInner, SubclassOfType, Truthiness, Type, TypeContext,
|
||||||
UnionBuilder, infer_expression_types,
|
TypeVarBoundOrConstraints, UnionBuilder, infer_expression_types,
|
||||||
};
|
};
|
||||||
|
|
||||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
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::AlwaysFalsy
|
||||||
| Type::AlwaysTruthy
|
| Type::AlwaysTruthy
|
||||||
| Type::BooleanLiteral(_)
|
| Type::BooleanLiteral(_)
|
||||||
|
|
@ -244,7 +256,6 @@ impl ClassInfoConstraintFunction {
|
||||||
| Type::FunctionLiteral(_)
|
| Type::FunctionLiteral(_)
|
||||||
| Type::ProtocolInstance(_)
|
| Type::ProtocolInstance(_)
|
||||||
| Type::PropertyInstance(_)
|
| Type::PropertyInstance(_)
|
||||||
| Type::SpecialForm(_)
|
|
||||||
| Type::LiteralString
|
| Type::LiteralString
|
||||||
| Type::StringLiteral(_)
|
| Type::StringLiteral(_)
|
||||||
| Type::IntLiteral(_)
|
| Type::IntLiteral(_)
|
||||||
|
|
|
||||||
|
|
@ -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<KnownClass> {
|
||||||
|
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
|
/// Return `true` if this special form is valid as the second argument
|
||||||
/// to `issubclass()` and `isinstance()` calls.
|
/// to `issubclass()` and `isinstance()` calls.
|
||||||
pub(super) const fn is_valid_isinstance_target(self) -> bool {
|
pub(super) const fn is_valid_isinstance_target(self) -> bool {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue