mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-20 04:29:47 +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)
|
||||
```
|
||||
|
||||
## `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
|
||||
|
|
|
|||
|
|
@ -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(_)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// to `issubclass()` and `isinstance()` calls.
|
||||
pub(super) const fn is_valid_isinstance_target(self) -> bool {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue