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 @@ -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
Expand All @@ -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
Expand Down
88 changes: 85 additions & 3 deletions crates/ty_python_semantic/src/types/infer/builder/typevar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is all it takes to support infer_variance=True, we should make this change for regular legacy typevars right away! (But in separate PR.)

};

let Some(name_param_ty) = name_param_ty.or_else(|| {
arguments
.find_positional(0)
Expand Down Expand Up @@ -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,
)))
}

Expand Down
4 changes: 2 additions & 2 deletions crates/ty_python_semantic/src/types/typevar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);

Expand All @@ -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),
Expand Down
Loading