mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:44:56 +00:00
[ty] Improve isinstance()
truthiness analysis for generic types (#19668)
This commit is contained in:
parent
d8151f0239
commit
18aae21b9a
3 changed files with 111 additions and 5 deletions
|
@ -152,7 +152,8 @@ s = SubclassOfA()
|
||||||
reveal_type(isinstance(s, SubclassOfA)) # revealed: Literal[True]
|
reveal_type(isinstance(s, SubclassOfA)) # revealed: Literal[True]
|
||||||
reveal_type(isinstance(s, A)) # revealed: Literal[True]
|
reveal_type(isinstance(s, A)) # revealed: Literal[True]
|
||||||
|
|
||||||
def _(x: A | B):
|
def _(x: A | B, y: list[int]):
|
||||||
|
reveal_type(isinstance(y, list)) # revealed: Literal[True]
|
||||||
reveal_type(isinstance(x, A)) # revealed: bool
|
reveal_type(isinstance(x, A)) # revealed: bool
|
||||||
|
|
||||||
if isinstance(x, A):
|
if isinstance(x, A):
|
||||||
|
|
|
@ -238,3 +238,103 @@ def match_non_exhaustive(x: A | B | C):
|
||||||
# this diagnostic is correct: the inferred type of `x` is `B & ~A & ~C`
|
# this diagnostic is correct: the inferred type of `x` is `B & ~A & ~C`
|
||||||
assert_never(x) # error: [type-assertion-failure]
|
assert_never(x) # error: [type-assertion-failure]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `isinstance` checks with generics
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import assert_never
|
||||||
|
|
||||||
|
class A[T]: ...
|
||||||
|
class ASub[T](A[T]): ...
|
||||||
|
class B[T]: ...
|
||||||
|
class C[T]: ...
|
||||||
|
class D: ...
|
||||||
|
class E: ...
|
||||||
|
class F: ...
|
||||||
|
|
||||||
|
def if_else_exhaustive(x: A[D] | B[E] | C[F]):
|
||||||
|
if isinstance(x, A):
|
||||||
|
pass
|
||||||
|
elif isinstance(x, B):
|
||||||
|
pass
|
||||||
|
elif isinstance(x, C):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# TODO: both of these are false positives (https://github.com/astral-sh/ty/issues/456)
|
||||||
|
no_diagnostic_here # error: [unresolved-reference]
|
||||||
|
assert_never(x) # error: [type-assertion-failure]
|
||||||
|
|
||||||
|
# TODO: false-positive diagnostic (https://github.com/astral-sh/ty/issues/456)
|
||||||
|
def if_else_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: # error: [invalid-return-type]
|
||||||
|
if isinstance(x, A):
|
||||||
|
return 0
|
||||||
|
elif isinstance(x, B):
|
||||||
|
return 1
|
||||||
|
elif isinstance(x, C):
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def if_else_non_exhaustive(x: A[D] | B[E] | C[F]):
|
||||||
|
if isinstance(x, A):
|
||||||
|
pass
|
||||||
|
elif isinstance(x, C):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
this_should_be_an_error # error: [unresolved-reference]
|
||||||
|
|
||||||
|
# this diagnostic is correct: the inferred type of `x` is `B[E] & ~A[D] & ~C[F]`
|
||||||
|
assert_never(x) # error: [type-assertion-failure]
|
||||||
|
|
||||||
|
def match_exhaustive(x: A[D] | B[E] | C[F]):
|
||||||
|
match x:
|
||||||
|
case A():
|
||||||
|
pass
|
||||||
|
case B():
|
||||||
|
pass
|
||||||
|
case C():
|
||||||
|
pass
|
||||||
|
case _:
|
||||||
|
# TODO: both of these are false positives (https://github.com/astral-sh/ty/issues/456)
|
||||||
|
no_diagnostic_here # error: [unresolved-reference]
|
||||||
|
assert_never(x) # error: [type-assertion-failure]
|
||||||
|
|
||||||
|
# TODO: false-positive diagnostic (https://github.com/astral-sh/ty/issues/456)
|
||||||
|
def match_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: # error: [invalid-return-type]
|
||||||
|
match x:
|
||||||
|
case A():
|
||||||
|
return 0
|
||||||
|
case B():
|
||||||
|
return 1
|
||||||
|
case C():
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def match_non_exhaustive(x: A[D] | B[E] | C[F]):
|
||||||
|
match x:
|
||||||
|
case A():
|
||||||
|
pass
|
||||||
|
case C():
|
||||||
|
pass
|
||||||
|
case _:
|
||||||
|
this_should_be_an_error # error: [unresolved-reference]
|
||||||
|
|
||||||
|
# this diagnostic is correct: the inferred type of `x` is `B[E] & ~A[D] & ~C[F]`
|
||||||
|
assert_never(x) # error: [type-assertion-failure]
|
||||||
|
|
||||||
|
# This function might seem a bit silly, but it's a pattern that exists in real-world code!
|
||||||
|
# see https://github.com/bokeh/bokeh/blob/adef0157284696ce86961b2089c75fddda53c15c/src/bokeh/core/property/container.py#L130-L140
|
||||||
|
def no_invalid_return_diagnostic_here_either[T](x: A[T]) -> ASub[T]:
|
||||||
|
if isinstance(x, A):
|
||||||
|
if isinstance(x, ASub):
|
||||||
|
return x
|
||||||
|
else:
|
||||||
|
return ASub()
|
||||||
|
else:
|
||||||
|
# We *would* emit a diagnostic here complaining that it's an invalid `return` statement
|
||||||
|
# ...except that we (correctly) infer that this branch is unreachable, so the complaint
|
||||||
|
# is null and void (and therefore we don't emit a diagnostic)
|
||||||
|
return x
|
||||||
|
```
|
||||||
|
|
|
@ -76,9 +76,9 @@ use crate::types::narrow::ClassInfoConstraintFunction;
|
||||||
use crate::types::signatures::{CallableSignature, Signature};
|
use crate::types::signatures::{CallableSignature, Signature};
|
||||||
use crate::types::visitor::any_over_type;
|
use crate::types::visitor::any_over_type;
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
BoundMethodType, CallableType, ClassLiteral, ClassType, DeprecatedInstance, DynamicType,
|
BoundMethodType, CallableType, ClassBase, ClassLiteral, ClassType, DeprecatedInstance,
|
||||||
KnownClass, Truthiness, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance,
|
DynamicType, KnownClass, Truthiness, Type, TypeMapping, TypeRelation, TypeTransformer,
|
||||||
UnionBuilder, all_members, walk_type_mapping,
|
TypeVarInstance, UnionBuilder, all_members, walk_type_mapping,
|
||||||
};
|
};
|
||||||
use crate::{Db, FxOrderSet, ModuleName, resolve_module};
|
use crate::{Db, FxOrderSet, ModuleName, resolve_module};
|
||||||
|
|
||||||
|
@ -901,7 +901,12 @@ fn is_instance_truthiness<'db>(
|
||||||
if let Type::NominalInstance(instance) = ty {
|
if let Type::NominalInstance(instance) = ty {
|
||||||
if instance
|
if instance
|
||||||
.class
|
.class
|
||||||
.is_subclass_of(db, ClassType::NonGeneric(class))
|
.iter_mro(db)
|
||||||
|
.filter_map(ClassBase::into_class)
|
||||||
|
.any(|c| match c {
|
||||||
|
ClassType::Generic(c) => c.origin(db) == class,
|
||||||
|
ClassType::NonGeneric(c) => c == class,
|
||||||
|
})
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue