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 @@ -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]()
```

Comment thread
charliermarsh marked this conversation as resolved.
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
Expand Down
41 changes: 26 additions & 15 deletions crates/ty_python_semantic/src/types/infer/builder/typevar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 => {
Expand All @@ -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(|| {
Expand Down Expand Up @@ -1168,7 +1179,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
db,
identity,
bound_or_constraints,
Some(variance),
variance,
default,
)))
}
Expand Down
Loading