Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ s = SubclassOfA()
reveal_type(isinstance(s, SubclassOfA)) # 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

if isinstance(x, A):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`
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
```
13 changes: 9 additions & 4 deletions crates/ty_python_semantic/src/types/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ use crate::types::narrow::ClassInfoConstraintFunction;
use crate::types::signatures::{CallableSignature, Signature};
use crate::types::visitor::any_over_type;
use crate::types::{
BoundMethodType, CallableType, ClassLiteral, ClassType, DeprecatedInstance, DynamicType,
KnownClass, Truthiness, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance,
UnionBuilder, all_members, walk_type_mapping,
BoundMethodType, CallableType, ClassBase, ClassLiteral, ClassType, DeprecatedInstance,
DynamicType, KnownClass, Truthiness, Type, TypeMapping, TypeRelation, TypeTransformer,
TypeVarInstance, UnionBuilder, all_members, walk_type_mapping,
};
use crate::{Db, FxOrderSet, ModuleName, resolve_module};

Expand Down Expand Up @@ -901,7 +901,12 @@ fn is_instance_truthiness<'db>(
if let Type::NominalInstance(instance) = ty {
if instance
.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;
}
Expand Down
Loading