Skip to content

Commit 473a82b

Browse files
Merge 8f98f40 into ea4b406
2 parents ea4b406 + 8f98f40 commit 473a82b

3 files changed

Lines changed: 163 additions & 11 deletions

File tree

crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,9 @@ def decorator(f: Callable[P2, int]) -> Callable[P2, int]:
6464
return f
6565
```
6666

67-
### Accepts only a single `name` argument
67+
### Bounds and constraints
6868

69-
> The runtime should accept bounds and covariant and contravariant arguments in the declaration just
70-
> as typing.TypeVar does, but for now we will defer the standardization of the semantics of those
71-
> options to a later PEP.
69+
`ParamSpec` does not allow defining bounds or constraints.
7270

7371
```py
7472
from typing import ParamSpec
@@ -77,10 +75,82 @@ from typing import ParamSpec
7775
P1 = ParamSpec("P1", bound=int)
7876
# error: [invalid-paramspec]
7977
P2 = ParamSpec("P2", int, str)
78+
```
79+
80+
### Variance
81+
82+
Legacy `ParamSpec` accepts `covariant` and `contravariant` arguments. A `ParamSpec` with no variance
83+
specified is invariant, and a `ParamSpec` with `infer_variance=True` uses variance inference.
84+
85+
```toml
86+
[environment]
87+
python-version = "3.12"
88+
```
89+
90+
```py
91+
from typing import Callable, Generic, ParamSpec
92+
93+
P = ParamSpec("P")
94+
95+
class InvariantParamSpec(Generic[P]):
96+
callback: Callable[P, None]
97+
98+
in_out_obj: InvariantParamSpec[object] = InvariantParamSpec[int]() # error: [invalid-assignment]
99+
in_out_int: InvariantParamSpec[int] = InvariantParamSpec[object]() # error: [invalid-assignment]
100+
101+
InP = ParamSpec("InP", contravariant=True)
102+
103+
class ContravariantParamSpec(Generic[InP]):
104+
def parameters(self) -> Callable[InP, None]:
105+
raise NotImplementedError
106+
107+
in_obj: ContravariantParamSpec[object] = ContravariantParamSpec[int]() # error: [invalid-assignment]
108+
in_int: ContravariantParamSpec[int] = ContravariantParamSpec[object]()
109+
110+
OutP = ParamSpec("OutP", covariant=True)
111+
112+
class CovariantParamSpec(Generic[OutP]):
113+
def accepts_callback(self, callback: Callable[OutP, None]) -> None:
114+
raise NotImplementedError
115+
116+
out_int: CovariantParamSpec[int] = CovariantParamSpec[object]() # error: [invalid-assignment]
117+
out_obj: CovariantParamSpec[object] = CovariantParamSpec[int]()
118+
119+
InferredInP = ParamSpec("InferredInP", infer_variance=True)
120+
121+
class InferredContravariantParamSpec(Generic[InferredInP]):
122+
def parameters(self) -> Callable[InferredInP, None]:
123+
raise NotImplementedError
124+
125+
inferred_in_obj: InferredContravariantParamSpec[object] = InferredContravariantParamSpec[int]() # error: [invalid-assignment]
126+
inferred_in_int: InferredContravariantParamSpec[int] = InferredContravariantParamSpec[object]()
127+
128+
InferredOutP = ParamSpec("InferredOutP", infer_variance=True)
129+
130+
class InferredCovariantParamSpec(Generic[InferredOutP]):
131+
def accepts_callback(self, callback: Callable[InferredOutP, None]) -> None:
132+
raise NotImplementedError
133+
134+
inferred_out_int: InferredCovariantParamSpec[int] = InferredCovariantParamSpec[object]() # error: [invalid-assignment]
135+
inferred_out_obj: InferredCovariantParamSpec[object] = InferredCovariantParamSpec[int]()
136+
```
137+
138+
```py
139+
from typing import ParamSpec
140+
141+
def cond() -> bool:
142+
return True
143+
144+
# error: [invalid-paramspec]
145+
Both = ParamSpec("Both", covariant=True, contravariant=True)
146+
# error: [invalid-paramspec]
147+
AmbiguousCovariant = ParamSpec("AmbiguousCovariant", covariant=cond())
148+
# error: [invalid-paramspec]
149+
AmbiguousContravariant = ParamSpec("AmbiguousContravariant", contravariant=cond())
80150
# error: [invalid-paramspec]
81-
P3 = ParamSpec("P3", covariant=True)
151+
AmbiguousInferVariance = ParamSpec("AmbiguousInferVariance", infer_variance=cond())
82152
# error: [invalid-paramspec]
83-
P4 = ParamSpec("P4", contravariant=True)
153+
CovariantAndInferred = ParamSpec("CovariantAndInferred", covariant=True, infer_variance=True)
84154
```
85155

86156
### Defaults

crates/ty_python_semantic/src/types/infer/builder/typevar.rs

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
700700
|version: PythonVersion| assume_all_features || python_version >= version;
701701

702702
let mut default = None;
703+
let mut covariant = false;
704+
let mut contravariant = false;
705+
let mut infer_variance = false;
703706
let mut name_param_ty = None;
704707
let mut name_param_node = None;
705708

@@ -742,13 +745,71 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
742745
name_param_ty =
743746
Some(self.infer_expression(&kwarg.value, TypeContext::default()));
744747
}
745-
"bound" | "covariant" | "contravariant" | "infer_variance" => {
748+
"bound" => {
746749
return error(
747750
&self.context,
748-
"The variance and bound arguments for `ParamSpec` do not have defined semantics yet",
751+
"The `bound` argument for `ParamSpec` is not supported",
749752
call_expr,
750753
);
751754
}
755+
"infer_variance" => {
756+
if !have_features_from(PythonVersion::PY312) {
757+
error(
758+
&self.context,
759+
"The `infer_variance` parameter of `typing.ParamSpec` was added in Python 3.12",
760+
kwarg,
761+
);
762+
}
763+
match self
764+
.infer_expression(&kwarg.value, TypeContext::default())
765+
.bool(db)
766+
{
767+
Truthiness::AlwaysTrue => infer_variance = true,
768+
Truthiness::AlwaysFalse => {}
769+
Truthiness::Ambiguous => {
770+
return error(
771+
&self.context,
772+
"The `infer_variance` parameter of `ParamSpec` \
773+
cannot have an ambiguous truthiness",
774+
&kwarg.value,
775+
);
776+
}
777+
}
778+
}
779+
"covariant" => {
780+
match self
781+
.infer_expression(&kwarg.value, TypeContext::default())
782+
.bool(db)
783+
{
784+
Truthiness::AlwaysTrue => covariant = true,
785+
Truthiness::AlwaysFalse => {}
786+
Truthiness::Ambiguous => {
787+
return error(
788+
&self.context,
789+
"The `covariant` parameter of `ParamSpec` \
790+
cannot have an ambiguous truthiness",
791+
&kwarg.value,
792+
);
793+
}
794+
}
795+
}
796+
"contravariant" => {
797+
match self
798+
.infer_expression(&kwarg.value, TypeContext::default())
799+
.bool(db)
800+
{
801+
Truthiness::AlwaysTrue => contravariant = true,
802+
Truthiness::AlwaysFalse => {}
803+
Truthiness::Ambiguous => {
804+
return error(
805+
&self.context,
806+
"The `contravariant` parameter of `ParamSpec` \
807+
cannot have an ambiguous truthiness",
808+
&kwarg.value,
809+
);
810+
}
811+
}
812+
}
752813
"default" => {
753814
if !have_features_from(PythonVersion::PY313) {
754815
// We don't return here; this error is informational since this will error
@@ -776,6 +837,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
776837
}
777838
}
778839

840+
let variance = match (covariant, contravariant, infer_variance) {
841+
(true, true, _) => {
842+
return error(
843+
&self.context,
844+
"A `ParamSpec` cannot be both covariant and contravariant",
845+
call_expr,
846+
);
847+
}
848+
(true, false, true) | (false, true, true) => {
849+
return error(
850+
&self.context,
851+
"A `ParamSpec` cannot specify variance when `infer_variance=True`",
852+
call_expr,
853+
);
854+
}
855+
(true, false, false) => Some(TypeVarVariance::Covariant),
856+
(false, true, false) => Some(TypeVarVariance::Contravariant),
857+
(false, false, false) => Some(TypeVarVariance::Invariant),
858+
(false, false, true) => None,
859+
};
860+
779861
let Some(name_param_ty) = name_param_ty.or_else(|| {
780862
arguments
781863
.find_positional(0)
@@ -832,7 +914,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
832914
TypeVarKind::ParamSpec,
833915
);
834916
Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new(
835-
db, identity, None, None, default,
917+
db, identity, None, variance, default,
836918
)))
837919
}
838920

crates/ty_python_semantic/src/types/typevar.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ impl<'db> BoundTypeVarInstance<'db> {
712712
db,
713713
self.typevar(db).identity(db),
714714
Some(TypeVarBoundOrConstraintsEvaluation::Eager(upper_bound)),
715-
None, // ParamSpecs cannot have explicit variance
715+
self.typevar(db).explicit_variance(db),
716716
None, // `P.args` and `P.kwargs` cannot have defaults even though `P` can
717717
);
718718

@@ -739,7 +739,7 @@ impl<'db> BoundTypeVarInstance<'db> {
739739
db,
740740
self.typevar(db).identity(db),
741741
None, // Remove the upper bound set by `with_paramspec_attr`
742-
None, // ParamSpecs cannot have explicit variance
742+
self.typevar(db).explicit_variance(db),
743743
None, // `P.args` and `P.kwargs` cannot have defaults even though `P` can
744744
),
745745
self.binding_context(db),

0 commit comments

Comments
 (0)