From e6528502bab914401ca94baa7671f6ede784b73d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 29 Apr 2026 12:49:34 -0400 Subject: [PATCH] [ty] Support `infer_variance` for legacy `TypeVar` (#24930) ## Summary See: https://github.com/astral-sh/ruff/pull/24927#discussion_r3162016951. --- .../mdtest/generics/legacy/variables.md | 87 +++++++++++++++++++ .../src/types/infer/builder/typevar.rs | 41 +++++---- 2 files changed, 113 insertions(+), 15 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md index da2c2e5586..8b28c90b5d 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/variables.md @@ -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 diff --git a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs index 88b872747f..af570912cf 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs @@ -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, ))) }