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(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)
|
||||
}
|
||||
|
||||
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>> {
|
||||
match self {
|
||||
Type::ClassLiteral(class_type) => Some(class_type),
|
||||
|
|
|
|||
|
|
@ -3647,6 +3647,31 @@ impl<'db> BindingError<'db> {
|
|||
expected_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
|
||||
// here (see `diagnostic::is_invalid_typed_dict_literal`). However, we may have
|
||||
// silenced diagnostics during overload evaluation, and rely on the assignability
|
||||
|
|
|
|||
|
|
@ -1764,6 +1764,7 @@ impl KnownFunction {
|
|||
Type::KnownInstance(KnownInstanceType::UnionType(_)) => {
|
||||
fn find_invalid_elements<'db>(
|
||||
db: &'db dyn Db,
|
||||
function: KnownFunction,
|
||||
ty: Type<'db>,
|
||||
invalid_elements: &mut Vec<Type<'db>>,
|
||||
) {
|
||||
|
|
@ -1771,9 +1772,19 @@ impl KnownFunction {
|
|||
Type::ClassLiteral(_) => {}
|
||||
Type::NominalInstance(instance)
|
||||
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)) => {
|
||||
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),
|
||||
|
|
@ -1781,7 +1792,7 @@ impl KnownFunction {
|
|||
}
|
||||
|
||||
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)) =
|
||||
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
|
||||
pub(super) const fn repr(self) -> &'static str {
|
||||
match self {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue