diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 2c4780097c43d..9d11a5be6feb9 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -64,11 +64,9 @@ def decorator(f: Callable[P2, int]) -> Callable[P2, int]: return f ``` -### Accepts only a single `name` argument +### Bounds and constraints -> The runtime should accept bounds and covariant and contravariant arguments in the declaration just -> as typing.TypeVar does, but for now we will defer the standardization of the semantics of those -> options to a later PEP. +`ParamSpec` does not allow defining bounds or constraints. ```py from typing import ParamSpec @@ -77,10 +75,82 @@ from typing import ParamSpec P1 = ParamSpec("P1", bound=int) # error: [invalid-paramspec] P2 = ParamSpec("P2", int, str) +``` + +### Variance + +Legacy `ParamSpec` accepts `covariant` and `contravariant` arguments. A `ParamSpec` with no variance +specified is invariant, and a `ParamSpec` with `infer_variance=True` uses variance inference. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable, Generic, ParamSpec + +P = ParamSpec("P") + +class InvariantParamSpec(Generic[P]): + callback: Callable[P, None] + +in_out_obj: InvariantParamSpec[object] = InvariantParamSpec[int]() # error: [invalid-assignment] +in_out_int: InvariantParamSpec[int] = InvariantParamSpec[object]() # error: [invalid-assignment] + +InP = ParamSpec("InP", contravariant=True) + +class ContravariantParamSpec(Generic[InP]): + def parameters(self) -> Callable[InP, None]: + raise NotImplementedError + +in_obj: ContravariantParamSpec[object] = ContravariantParamSpec[int]() # error: [invalid-assignment] +in_int: ContravariantParamSpec[int] = ContravariantParamSpec[object]() + +OutP = ParamSpec("OutP", covariant=True) + +class CovariantParamSpec(Generic[OutP]): + def accepts_callback(self, callback: Callable[OutP, None]) -> None: + raise NotImplementedError + +out_int: CovariantParamSpec[int] = CovariantParamSpec[object]() # error: [invalid-assignment] +out_obj: CovariantParamSpec[object] = CovariantParamSpec[int]() + +InferredInP = ParamSpec("InferredInP", infer_variance=True) + +class InferredContravariantParamSpec(Generic[InferredInP]): + def parameters(self) -> Callable[InferredInP, None]: + raise NotImplementedError + +inferred_in_obj: InferredContravariantParamSpec[object] = InferredContravariantParamSpec[int]() # error: [invalid-assignment] +inferred_in_int: InferredContravariantParamSpec[int] = InferredContravariantParamSpec[object]() + +InferredOutP = ParamSpec("InferredOutP", infer_variance=True) + +class InferredCovariantParamSpec(Generic[InferredOutP]): + def accepts_callback(self, callback: Callable[InferredOutP, None]) -> None: + raise NotImplementedError + +inferred_out_int: InferredCovariantParamSpec[int] = InferredCovariantParamSpec[object]() # error: [invalid-assignment] +inferred_out_obj: InferredCovariantParamSpec[object] = InferredCovariantParamSpec[int]() +``` + +```py +from typing import ParamSpec + +def cond() -> bool: + return True + +# error: [invalid-paramspec] +Both = ParamSpec("Both", covariant=True, contravariant=True) +# error: [invalid-paramspec] +AmbiguousCovariant = ParamSpec("AmbiguousCovariant", covariant=cond()) +# error: [invalid-paramspec] +AmbiguousContravariant = ParamSpec("AmbiguousContravariant", contravariant=cond()) # error: [invalid-paramspec] -P3 = ParamSpec("P3", covariant=True) +AmbiguousInferVariance = ParamSpec("AmbiguousInferVariance", infer_variance=cond()) # error: [invalid-paramspec] -P4 = ParamSpec("P4", contravariant=True) +CovariantAndInferred = ParamSpec("CovariantAndInferred", covariant=True, infer_variance=True) ``` ### Defaults 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 dfe79494fc655..88b872747f771 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/typevar.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/typevar.rs @@ -700,6 +700,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { |version: PythonVersion| assume_all_features || python_version >= version; 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; @@ -742,13 +745,71 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { name_param_ty = Some(self.infer_expression(&kwarg.value, TypeContext::default())); } - "bound" | "covariant" | "contravariant" | "infer_variance" => { + "bound" => { return error( &self.context, - "The variance and bound arguments for `ParamSpec` do not have defined semantics yet", + "The `bound` argument for `ParamSpec` is not supported", call_expr, ); } + "infer_variance" => { + if !have_features_from(PythonVersion::PY312) { + error( + &self.context, + "The `infer_variance` parameter of `typing.ParamSpec` was added in Python 3.12", + kwarg, + ); + } + match self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + { + Truthiness::AlwaysTrue => infer_variance = true, + Truthiness::AlwaysFalse => {} + Truthiness::Ambiguous => { + return error( + &self.context, + "The `infer_variance` parameter of `ParamSpec` \ + cannot have an ambiguous truthiness", + &kwarg.value, + ); + } + } + } + "covariant" => { + match self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + { + Truthiness::AlwaysTrue => covariant = true, + Truthiness::AlwaysFalse => {} + Truthiness::Ambiguous => { + return error( + &self.context, + "The `covariant` parameter of `ParamSpec` \ + cannot have an ambiguous truthiness", + &kwarg.value, + ); + } + } + } + "contravariant" => { + match self + .infer_expression(&kwarg.value, TypeContext::default()) + .bool(db) + { + Truthiness::AlwaysTrue => contravariant = true, + Truthiness::AlwaysFalse => {} + Truthiness::Ambiguous => { + return error( + &self.context, + "The `contravariant` parameter of `ParamSpec` \ + cannot have an ambiguous truthiness", + &kwarg.value, + ); + } + } + } "default" => { if !have_features_from(PythonVersion::PY313) { // We don't return here; this error is informational since this will error @@ -776,6 +837,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + let variance = match (covariant, contravariant, infer_variance) { + (true, true, _) => { + return error( + &self.context, + "A `ParamSpec` cannot be both covariant and contravariant", + call_expr, + ); + } + (true, false, true) | (false, true, true) => { + return error( + &self.context, + "A `ParamSpec` 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(|| { arguments .find_positional(0) @@ -832,7 +914,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { TypeVarKind::ParamSpec, ); Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new( - db, identity, None, None, default, + db, identity, None, variance, default, ))) } diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index c7be1e569541d..5f4afd84e769f 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -712,7 +712,7 @@ impl<'db> BoundTypeVarInstance<'db> { db, self.typevar(db).identity(db), Some(TypeVarBoundOrConstraintsEvaluation::Eager(upper_bound)), - None, // ParamSpecs cannot have explicit variance + self.typevar(db).explicit_variance(db), None, // `P.args` and `P.kwargs` cannot have defaults even though `P` can ); @@ -739,7 +739,7 @@ impl<'db> BoundTypeVarInstance<'db> { db, self.typevar(db).identity(db), None, // Remove the upper bound set by `with_paramspec_attr` - None, // ParamSpecs cannot have explicit variance + self.typevar(db).explicit_variance(db), None, // `P.args` and `P.kwargs` cannot have defaults even though `P` can ), self.binding_context(db),