mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:14:52 +00:00
[ty] Narrow specialized generics using isinstance() (#20256)
Closes astral-sh/ty#456. Part of astral-sh/ty#994. After all the foundational work, this is only a small change, but let's see if it exposes any unresolved issues.
This commit is contained in:
parent
670fffef37
commit
08c1d3660c
4 changed files with 106 additions and 11 deletions
|
@ -281,12 +281,10 @@ def if_else_exhaustive(x: A[D] | B[E] | C[F]):
|
|||
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]
|
||||
no_diagnostic_here
|
||||
assert_never(x)
|
||||
|
||||
# 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]
|
||||
def if_else_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int:
|
||||
if isinstance(x, A):
|
||||
return 0
|
||||
elif isinstance(x, B):
|
||||
|
|
|
@ -308,3 +308,88 @@ def i[T: Intersection[type[Bar], type[Baz | Spam]], U: (type[Eggs], type[Ham])](
|
|||
|
||||
return (y, z)
|
||||
```
|
||||
|
||||
## Narrowing with generics
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Narrowing to a generic class using `isinstance()` uses the top materialization of the generic. With
|
||||
a covariant generic, this is equivalent to using the upper bound of the type parameter (by default,
|
||||
`object`):
|
||||
|
||||
```py
|
||||
class Covariant[T]:
|
||||
def get(self) -> T:
|
||||
raise NotImplementedError
|
||||
|
||||
def _(x: object):
|
||||
if isinstance(x, Covariant):
|
||||
reveal_type(x) # revealed: Covariant[object]
|
||||
reveal_type(x.get()) # revealed: object
|
||||
```
|
||||
|
||||
Similarly, contravariant type parameters use their lower bound of `Never`:
|
||||
|
||||
```py
|
||||
class Contravariant[T]:
|
||||
def push(self, x: T) -> None: ...
|
||||
|
||||
def _(x: object):
|
||||
if isinstance(x, Contravariant):
|
||||
reveal_type(x) # revealed: Contravariant[Never]
|
||||
# error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Never`, found `Literal[42]`"
|
||||
x.push(42)
|
||||
```
|
||||
|
||||
Invariant generics are trickiest. The top materialization, conceptually the type that includes all
|
||||
instances of the generic class regardless of the type parameter, cannot be represented directly in
|
||||
the type system, so we represent it with the internal `Top[]` special form.
|
||||
|
||||
```py
|
||||
class Invariant[T]:
|
||||
def push(self, x: T) -> None: ...
|
||||
def get(self) -> T:
|
||||
raise NotImplementedError
|
||||
|
||||
def _(x: object):
|
||||
if isinstance(x, Invariant):
|
||||
reveal_type(x) # revealed: Top[Invariant[Unknown]]
|
||||
reveal_type(x.get()) # revealed: object
|
||||
# error: [invalid-argument-type] "Argument to bound method `push` is incorrect: Expected `Never`, found `Literal[42]`"
|
||||
x.push(42)
|
||||
```
|
||||
|
||||
When more complex types are involved, the `Top[]` type may get simplified away.
|
||||
|
||||
```py
|
||||
def _(x: list[int] | set[str]):
|
||||
if isinstance(x, list):
|
||||
reveal_type(x) # revealed: list[int]
|
||||
else:
|
||||
reveal_type(x) # revealed: set[str]
|
||||
```
|
||||
|
||||
Though if the types involved are not disjoint bases, we necessarily keep a more complex type.
|
||||
|
||||
```py
|
||||
def _(x: Invariant[int] | Covariant[str]):
|
||||
if isinstance(x, Invariant):
|
||||
reveal_type(x) # revealed: Invariant[int] | (Covariant[str] & Top[Invariant[Unknown]])
|
||||
else:
|
||||
reveal_type(x) # revealed: Covariant[str] & ~Top[Invariant[Unknown]]
|
||||
```
|
||||
|
||||
The behavior of `issubclass()` is similar.
|
||||
|
||||
```py
|
||||
def _(x: type[object], y: type[object], z: type[object]):
|
||||
if issubclass(x, Covariant):
|
||||
reveal_type(x) # revealed: type[Covariant[object]]
|
||||
if issubclass(y, Contravariant):
|
||||
reveal_type(y) # revealed: type[Contravariant[Never]]
|
||||
if issubclass(z, Invariant):
|
||||
reveal_type(z) # revealed: type[Top[Invariant[Unknown]]]
|
||||
```
|
||||
|
|
|
@ -29,10 +29,10 @@ use crate::types::typed_dict::typed_dict_params_from_class_def;
|
|||
use crate::types::{
|
||||
ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType,
|
||||
DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
|
||||
IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, NormalizedVisitor,
|
||||
PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation,
|
||||
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, UnionBuilder,
|
||||
VarianceInferable, declaration_type, infer_definition_types,
|
||||
IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind,
|
||||
NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping,
|
||||
TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams,
|
||||
UnionBuilder, VarianceInferable, declaration_type, infer_definition_types,
|
||||
};
|
||||
use crate::{
|
||||
Db, FxIndexMap, FxOrderSet, Program,
|
||||
|
@ -1470,6 +1470,18 @@ impl<'db> ClassLiteral<'db> {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> {
|
||||
self.apply_specialization(db, |generic_context| {
|
||||
generic_context
|
||||
.default_specialization(db, self.known(db))
|
||||
.materialize_impl(
|
||||
db,
|
||||
MaterializationKind::Top,
|
||||
&ApplyTypeMappingVisitor::default(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the default specialization of this class. For non-generic classes, the class is
|
||||
/// returned unchanged. For a non-specialized generic class, we return a generic alias that
|
||||
/// applies the default specialization to the class's typevars.
|
||||
|
|
|
@ -174,10 +174,10 @@ impl ClassInfoConstraintFunction {
|
|||
fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option<Type<'db>> {
|
||||
let constraint_fn = |class: ClassLiteral<'db>| match self {
|
||||
ClassInfoConstraintFunction::IsInstance => {
|
||||
Type::instance(db, class.default_specialization(db))
|
||||
Type::instance(db, class.top_materialization(db))
|
||||
}
|
||||
ClassInfoConstraintFunction::IsSubclass => {
|
||||
SubclassOfType::from(db, class.default_specialization(db))
|
||||
SubclassOfType::from(db, class.top_materialization(db))
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue