mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 03:48:29 +00:00
[ty] Silence false-positive diagnostics when using typing.Dict or typing.Callable as the second argument to isinstance() (#21386)
This commit is contained in:
parent
bd8812127d
commit
03bd0619e9
5 changed files with 132 additions and 2 deletions
|
|
@ -162,3 +162,38 @@ def _(x: A | B, y: list[int]):
|
||||||
reveal_type(x) # revealed: B & ~A
|
reveal_type(x) # revealed: B & ~A
|
||||||
reveal_type(isinstance(x, B)) # revealed: Literal[True]
|
reveal_type(isinstance(x, B)) # revealed: Literal[True]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Certain special forms in the typing module are not instances of `type`, so are strictly-speaking
|
||||||
|
disallowed as the second argument to `isinstance()` according to typeshed's annotations. However, at
|
||||||
|
runtime they work fine as the second argument, and we implement that special case in ty:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import typing as t
|
||||||
|
|
||||||
|
# no errors emitted for any of these:
|
||||||
|
isinstance("", t.Dict)
|
||||||
|
isinstance("", t.List)
|
||||||
|
isinstance("", t.Set)
|
||||||
|
isinstance("", t.FrozenSet)
|
||||||
|
isinstance("", t.Tuple)
|
||||||
|
isinstance("", t.ChainMap)
|
||||||
|
isinstance("", t.Counter)
|
||||||
|
isinstance("", t.Deque)
|
||||||
|
isinstance("", t.OrderedDict)
|
||||||
|
isinstance("", t.Callable)
|
||||||
|
isinstance("", t.Type)
|
||||||
|
isinstance("", t.Callable | t.Deque)
|
||||||
|
|
||||||
|
# `Any` is valid in `issubclass()` calls but not `isinstance()` calls
|
||||||
|
issubclass(list, t.Any)
|
||||||
|
issubclass(list, t.Any | t.Dict)
|
||||||
|
```
|
||||||
|
|
||||||
|
But for other special forms that are not permitted as the second argument, we still emit an error:
|
||||||
|
|
||||||
|
```py
|
||||||
|
isinstance("", t.TypeGuard) # error: [invalid-argument-type]
|
||||||
|
isinstance("", t.ClassVar) # error: [invalid-argument-type]
|
||||||
|
isinstance("", t.Final) # error: [invalid-argument-type]
|
||||||
|
isinstance("", t.Any) # error: [invalid-argument-type]
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1028,6 +1028,13 @@ impl<'db> Type<'db> {
|
||||||
any_over_type(db, self, &|ty| matches!(ty, Type::TypeVar(_)), false)
|
any_over_type(db, self, &|ty| matches!(ty, Type::TypeVar(_)), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn as_special_form(self) -> Option<SpecialFormType> {
|
||||||
|
match self {
|
||||||
|
Type::SpecialForm(special_form) => Some(special_form),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) const fn as_class_literal(self) -> Option<ClassLiteral<'db>> {
|
pub(crate) const fn as_class_literal(self) -> Option<ClassLiteral<'db>> {
|
||||||
match self {
|
match self {
|
||||||
Type::ClassLiteral(class_type) => Some(class_type),
|
Type::ClassLiteral(class_type) => Some(class_type),
|
||||||
|
|
|
||||||
|
|
@ -3647,6 +3647,31 @@ impl<'db> BindingError<'db> {
|
||||||
expected_ty,
|
expected_ty,
|
||||||
provided_ty,
|
provided_ty,
|
||||||
} => {
|
} => {
|
||||||
|
// Certain special forms in the typing module are aliases for classes
|
||||||
|
// elsewhere in the standard library. These special forms are not instances of `type`,
|
||||||
|
// and you cannot use them in place of their aliased classes in *all* situations:
|
||||||
|
// for example, `dict()` succeeds at runtime, but `typing.Dict()` fails. However,
|
||||||
|
// they *can* all be used as the second argument to `isinstance` and `issubclass`.
|
||||||
|
// We model that specific aspect of their behaviour here.
|
||||||
|
//
|
||||||
|
// This is implemented as a special case in call-binding machinery because overriding
|
||||||
|
// typeshed's signatures for `isinstance()` and `issubclass()` would be complex and
|
||||||
|
// error-prone, due to the fact that they are annotated with recursive type aliases.
|
||||||
|
if parameter.index == 1
|
||||||
|
&& *argument_index == Some(1)
|
||||||
|
&& matches!(
|
||||||
|
callable_ty
|
||||||
|
.as_function_literal()
|
||||||
|
.and_then(|function| function.known(context.db())),
|
||||||
|
Some(KnownFunction::IsInstance | KnownFunction::IsSubclass)
|
||||||
|
)
|
||||||
|
&& provided_ty
|
||||||
|
.as_special_form()
|
||||||
|
.is_some_and(SpecialFormType::is_valid_isinstance_target)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments
|
// TODO: Ideally we would not emit diagnostics for `TypedDict` literal arguments
|
||||||
// here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have
|
// here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have
|
||||||
// silenced diagnostics during overload evaluation, and rely on the assignability
|
// silenced diagnostics during overload evaluation, and rely on the assignability
|
||||||
|
|
|
||||||
|
|
@ -1764,6 +1764,7 @@ impl KnownFunction {
|
||||||
Type::KnownInstance(KnownInstanceType::UnionType(_)) => {
|
Type::KnownInstance(KnownInstanceType::UnionType(_)) => {
|
||||||
fn find_invalid_elements<'db>(
|
fn find_invalid_elements<'db>(
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
|
function: KnownFunction,
|
||||||
ty: Type<'db>,
|
ty: Type<'db>,
|
||||||
invalid_elements: &mut Vec<Type<'db>>,
|
invalid_elements: &mut Vec<Type<'db>>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -1771,9 +1772,19 @@ impl KnownFunction {
|
||||||
Type::ClassLiteral(_) => {}
|
Type::ClassLiteral(_) => {}
|
||||||
Type::NominalInstance(instance)
|
Type::NominalInstance(instance)
|
||||||
if instance.has_known_class(db, KnownClass::NoneType) => {}
|
if instance.has_known_class(db, KnownClass::NoneType) => {}
|
||||||
|
Type::SpecialForm(special_form)
|
||||||
|
if special_form.is_valid_isinstance_target() => {}
|
||||||
|
// `Any` can be used in `issubclass()` calls but not `isinstance()` calls
|
||||||
|
Type::SpecialForm(SpecialFormType::Any)
|
||||||
|
if function == KnownFunction::IsSubclass => {}
|
||||||
Type::KnownInstance(KnownInstanceType::UnionType(union)) => {
|
Type::KnownInstance(KnownInstanceType::UnionType(union)) => {
|
||||||
for element in union.elements(db) {
|
for element in union.elements(db) {
|
||||||
find_invalid_elements(db, *element, invalid_elements);
|
find_invalid_elements(
|
||||||
|
db,
|
||||||
|
function,
|
||||||
|
*element,
|
||||||
|
invalid_elements,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => invalid_elements.push(ty),
|
_ => invalid_elements.push(ty),
|
||||||
|
|
@ -1781,7 +1792,7 @@ impl KnownFunction {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut invalid_elements = vec![];
|
let mut invalid_elements = vec![];
|
||||||
find_invalid_elements(db, *second_argument, &mut invalid_elements);
|
find_invalid_elements(db, self, *second_argument, &mut invalid_elements);
|
||||||
|
|
||||||
let Some((first_invalid_element, other_invalid_elements)) =
|
let Some((first_invalid_element, other_invalid_elements)) =
|
||||||
invalid_elements.split_first()
|
invalid_elements.split_first()
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,58 @@ impl SpecialFormType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
match self {
|
||||||
|
Self::Callable
|
||||||
|
| Self::ChainMap
|
||||||
|
| Self::Counter
|
||||||
|
| Self::DefaultDict
|
||||||
|
| Self::Deque
|
||||||
|
| Self::FrozenSet
|
||||||
|
| Self::Dict
|
||||||
|
| Self::List
|
||||||
|
| Self::OrderedDict
|
||||||
|
| Self::Set
|
||||||
|
| Self::Tuple
|
||||||
|
| Self::Type
|
||||||
|
| Self::Protocol
|
||||||
|
| Self::Generic => true,
|
||||||
|
|
||||||
|
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 // can be used in `issubclass()` but not `isinstance()`.
|
||||||
|
| Self::Unpack => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the repr of the symbol at runtime
|
/// Return the repr of the symbol at runtime
|
||||||
pub(super) const fn repr(self) -> &'static str {
|
pub(super) const fn repr(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue