mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +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):
|
elif isinstance(x, C):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# TODO: both of these are false positives (https://github.com/astral-sh/ty/issues/456)
|
no_diagnostic_here
|
||||||
no_diagnostic_here # error: [unresolved-reference]
|
assert_never(x)
|
||||||
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:
|
||||||
def if_else_exhaustive_no_assertion(x: A[D] | B[E] | C[F]) -> int: # error: [invalid-return-type]
|
|
||||||
if isinstance(x, A):
|
if isinstance(x, A):
|
||||||
return 0
|
return 0
|
||||||
elif isinstance(x, B):
|
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)
|
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::{
|
use crate::types::{
|
||||||
ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType,
|
ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType,
|
||||||
DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
|
DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
|
||||||
IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, NormalizedVisitor,
|
IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind,
|
||||||
PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping, TypeRelation,
|
NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeMapping,
|
||||||
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, UnionBuilder,
|
TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams,
|
||||||
VarianceInferable, declaration_type, infer_definition_types,
|
UnionBuilder, VarianceInferable, declaration_type, infer_definition_types,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
Db, FxIndexMap, FxOrderSet, Program,
|
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
|
/// 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
|
/// returned unchanged. For a non-specialized generic class, we return a generic alias that
|
||||||
/// applies the default specialization to the class's typevars.
|
/// 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>> {
|
fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option<Type<'db>> {
|
||||||
let constraint_fn = |class: ClassLiteral<'db>| match self {
|
let constraint_fn = |class: ClassLiteral<'db>| match self {
|
||||||
ClassInfoConstraintFunction::IsInstance => {
|
ClassInfoConstraintFunction::IsInstance => {
|
||||||
Type::instance(db, class.default_specialization(db))
|
Type::instance(db, class.top_materialization(db))
|
||||||
}
|
}
|
||||||
ClassInfoConstraintFunction::IsSubclass => {
|
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