mirror of
https://github.com/astral-sh/ruff.git
synced 2026-05-06 08:56:57 -04:00
[ty] Support infer_variance for legacy TypeVar (#24930)
## Summary See: https://github.com/astral-sh/ruff/pull/24927#discussion_r3162016951.
This commit is contained in:
@@ -435,6 +435,93 @@ from typing import TypeVar
|
||||
T = TypeVar("T", covariant=True, contravariant=True)
|
||||
```
|
||||
|
||||
### Infer variance
|
||||
|
||||
For a `TypeVar` with `infer_variance=True`, we infer covariance when the type variable only appears
|
||||
in return positions, contravariance when it only appears in parameter positions, and invariance when
|
||||
it appears in both positions.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
OutT = TypeVar("OutT", infer_variance=True)
|
||||
|
||||
class Source(Generic[OutT]):
|
||||
def get(self) -> OutT:
|
||||
raise NotImplementedError
|
||||
|
||||
source_int: Source[int] = Source[object]() # error: [invalid-assignment]
|
||||
source_obj: Source[object] = Source[int]()
|
||||
|
||||
InT = TypeVar("InT", infer_variance=True)
|
||||
|
||||
class Sink(Generic[InT]):
|
||||
def send(self, value: InT) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
sink_obj: Sink[object] = Sink[int]() # error: [invalid-assignment]
|
||||
sink_int: Sink[int] = Sink[object]()
|
||||
```
|
||||
|
||||
Both assignments are errors when the type variable is inferred to be invariant:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T", infer_variance=True)
|
||||
|
||||
class Box(Generic[T]):
|
||||
value: T
|
||||
|
||||
box_int: Box[int] = Box[object]() # error: [invalid-assignment]
|
||||
box_obj: Box[object] = Box[int]() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
> A generic class that uses the traditional syntax may include combinations of type variables with
|
||||
> explicit and inferred variance.
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
ExplicitOutT = TypeVar("ExplicitOutT", covariant=True)
|
||||
InferredInT = TypeVar("InferredInT", infer_variance=True)
|
||||
|
||||
class Mixed(Generic[ExplicitOutT, InferredInT]):
|
||||
def get(self) -> ExplicitOutT:
|
||||
raise NotImplementedError
|
||||
|
||||
def send(self, value: InferredInT) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
mixed_covariant: Mixed[object, int] = Mixed[int, int]()
|
||||
mixed_not_covariant: Mixed[int, int] = Mixed[object, int]() # error: [invalid-assignment]
|
||||
mixed_contravariant: Mixed[int, int] = Mixed[int, object]()
|
||||
mixed_not_contravariant: Mixed[int, object] = Mixed[int, int]() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
Variance cannot be specified explicitly when variance inference is requested:
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
# snapshot: invalid-legacy-type-variable
|
||||
CovariantAndInferred = TypeVar("CovariantAndInferred", covariant=True, infer_variance=True)
|
||||
```
|
||||
|
||||
```snapshot
|
||||
error[invalid-legacy-type-variable]: A `TypeVar` cannot specify variance when `infer_variance=True`
|
||||
--> src/mdtest_snippet.py:48:24
|
||||
|
|
||||
48 | CovariantAndInferred = TypeVar("CovariantAndInferred", covariant=True, infer_variance=True)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
```
|
||||
|
||||
### Boolean parameters must be unambiguous
|
||||
|
||||
```py
|
||||
|
||||
@@ -950,6 +950,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
let mut default = None;
|
||||
let mut covariant = false;
|
||||
let mut contravariant = false;
|
||||
let mut infer_variance = false;
|
||||
let mut name_param_ty = None;
|
||||
let mut name_param_node = None;
|
||||
|
||||
@@ -1042,18 +1043,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
kwarg,
|
||||
);
|
||||
}
|
||||
// TODO support `infer_variance` in legacy TypeVars
|
||||
if self
|
||||
match self
|
||||
.infer_expression(&kwarg.value, TypeContext::default())
|
||||
.bool(db)
|
||||
.is_ambiguous()
|
||||
{
|
||||
return error(
|
||||
&self.context,
|
||||
"The `infer_variance` parameter of `TypeVar` \
|
||||
cannot have an ambiguous truthiness",
|
||||
&kwarg.value,
|
||||
);
|
||||
Truthiness::AlwaysTrue => infer_variance = true,
|
||||
Truthiness::AlwaysFalse => {}
|
||||
Truthiness::Ambiguous => {
|
||||
return error(
|
||||
&self.context,
|
||||
"The `infer_variance` parameter of `TypeVar` \
|
||||
cannot have an ambiguous truthiness",
|
||||
&kwarg.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
name => {
|
||||
@@ -1071,17 +1074,25 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
}
|
||||
|
||||
let variance = match (covariant, contravariant) {
|
||||
(true, true) => {
|
||||
let variance = match (covariant, contravariant, infer_variance) {
|
||||
(true, true, _) => {
|
||||
return error(
|
||||
&self.context,
|
||||
"A `TypeVar` cannot be both covariant and contravariant",
|
||||
call_expr,
|
||||
);
|
||||
}
|
||||
(true, false) => TypeVarVariance::Covariant,
|
||||
(false, true) => TypeVarVariance::Contravariant,
|
||||
(false, false) => TypeVarVariance::Invariant,
|
||||
(true, false, true) | (false, true, true) => {
|
||||
return error(
|
||||
&self.context,
|
||||
"A `TypeVar` cannot specify variance when `infer_variance=True`",
|
||||
call_expr,
|
||||
);
|
||||
}
|
||||
(true, false, false) => Some(TypeVarVariance::Covariant),
|
||||
(false, true, false) => Some(TypeVarVariance::Contravariant),
|
||||
(false, false, false) => Some(TypeVarVariance::Invariant),
|
||||
(false, false, true) => None,
|
||||
};
|
||||
|
||||
let Some(name_param_ty) = name_param_ty.or_else(|| {
|
||||
@@ -1168,7 +1179,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
db,
|
||||
identity,
|
||||
bound_or_constraints,
|
||||
Some(variance),
|
||||
variance,
|
||||
default,
|
||||
)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user