Skip to content

Commit cda163d

Browse files
authored
Clarify variance convention for Parameters (#16302)
Fixes #16296 In my big refactoring I flipped the variance convention for the `Parameters` type, but I did it inconsistently in one place. After working some more with ParamSpecs, it now seems to me the original convention is easier to remember. I also now explicitly put it in the type docstring.
1 parent 341929b commit cda163d

File tree

6 files changed

+47
-17
lines changed

6 files changed

+47
-17
lines changed

mypy/constraints.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -692,11 +692,8 @@ def visit_parameters(self, template: Parameters) -> list[Constraint]:
692692
return self.infer_against_any(template.arg_types, self.actual)
693693
if type_state.infer_polymorphic and isinstance(self.actual, Parameters):
694694
# For polymorphic inference we need to be able to infer secondary constraints
695-
# in situations like [x: T] <: P <: [x: int]. Note we invert direction, since
696-
# this function expects direction between callables.
697-
return infer_callable_arguments_constraints(
698-
template, self.actual, neg_op(self.direction)
699-
)
695+
# in situations like [x: T] <: P <: [x: int].
696+
return infer_callable_arguments_constraints(template, self.actual, self.direction)
700697
raise RuntimeError("Parameters cannot be constrained to")
701698

702699
# Non-leaf types
@@ -1128,7 +1125,7 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]:
11281125
)
11291126
)
11301127
if param_spec_target is not None:
1131-
res.append(Constraint(param_spec, neg_op(self.direction), param_spec_target))
1128+
res.append(Constraint(param_spec, self.direction, param_spec_target))
11321129
if extra_tvars:
11331130
for c in res:
11341131
c.extra_tvars += cactual.variables

mypy/join.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,10 +350,13 @@ def visit_parameters(self, t: Parameters) -> ProperType:
350350
if isinstance(self.s, Parameters):
351351
if len(t.arg_types) != len(self.s.arg_types):
352352
return self.default(self.s)
353+
from mypy.meet import meet_types
354+
353355
return t.copy_modified(
354-
# Note that since during constraint inference we already treat whole ParamSpec as
355-
# contravariant, we should join individual items, not meet them like for Callables
356-
arg_types=[join_types(s_a, t_a) for s_a, t_a in zip(self.s.arg_types, t.arg_types)]
356+
arg_types=[
357+
meet_types(s_a, t_a) for s_a, t_a in zip(self.s.arg_types, t.arg_types)
358+
],
359+
arg_names=combine_arg_names(self.s, t),
357360
)
358361
else:
359362
return self.default(self.s)
@@ -754,7 +757,9 @@ def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType:
754757
)
755758

756759

757-
def combine_arg_names(t: CallableType, s: CallableType) -> list[str | None]:
760+
def combine_arg_names(
761+
t: CallableType | Parameters, s: CallableType | Parameters
762+
) -> list[str | None]:
758763
"""Produces a list of argument names compatible with both callables.
759764
760765
For example, suppose 't' and 's' have the following signatures:

mypy/meet.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -708,10 +708,10 @@ def visit_parameters(self, t: Parameters) -> ProperType:
708708
if isinstance(self.s, Parameters):
709709
if len(t.arg_types) != len(self.s.arg_types):
710710
return self.default(self.s)
711+
from mypy.join import join_types
712+
711713
return t.copy_modified(
712-
# Note that since during constraint inference we already treat whole ParamSpec as
713-
# contravariant, we should meet individual items, not join them like for Callables
714-
arg_types=[meet_types(s_a, t_a) for s_a, t_a in zip(self.s.arg_types, t.arg_types)]
714+
arg_types=[join_types(s_a, t_a) for s_a, t_a in zip(self.s.arg_types, t.arg_types)]
715715
)
716716
else:
717717
return self.default(self.s)

mypy/subtypes.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -654,8 +654,6 @@ def visit_unpack_type(self, left: UnpackType) -> bool:
654654

655655
def visit_parameters(self, left: Parameters) -> bool:
656656
if isinstance(self.right, Parameters):
657-
# TODO: direction here should be opposite, this function expects
658-
# order of callables, while parameters are contravariant.
659657
return are_parameters_compatible(
660658
left,
661659
self.right,

mypy/types.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1562,7 +1562,10 @@ class FormalArgument(NamedTuple):
15621562
class Parameters(ProperType):
15631563
"""Type that represents the parameters to a function.
15641564
1565-
Used for ParamSpec analysis."""
1565+
Used for ParamSpec analysis. Note that by convention we handle this
1566+
type as a Callable without return type, not as a "tuple with names",
1567+
so that it behaves contravariantly, in particular [x: int] <: [int].
1568+
"""
15661569

15671570
__slots__ = (
15681571
"arg_types",

test-data/unit/check-parameter-specification.test

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1403,7 +1403,7 @@ def wrong_name_constructor(b: bool) -> SomeClass:
14031403
func(SomeClass, constructor)
14041404
reveal_type(func(SomeClass, wrong_constructor)) # N: Revealed type is "def (a: Never) -> __main__.SomeClass"
14051405
reveal_type(func_regular(SomeClass, wrong_constructor)) # N: Revealed type is "def (Never) -> __main__.SomeClass"
1406-
func(SomeClass, wrong_name_constructor) # E: Argument 1 to "func" has incompatible type "Type[SomeClass]"; expected "Callable[[Never], SomeClass]"
1406+
reveal_type(func(SomeClass, wrong_name_constructor)) # N: Revealed type is "def (Never) -> __main__.SomeClass"
14071407
[builtins fixtures/paramspec.pyi]
14081408

14091409
[case testParamSpecInTypeAliasBasic]
@@ -2059,3 +2059,30 @@ def test2(x: int, y: int) -> str: ...
20592059
reveal_type(call(test1, 1)) # N: Revealed type is "builtins.str"
20602060
reveal_type(call(test2, 1, 2)) # N: Revealed type is "builtins.str"
20612061
[builtins fixtures/paramspec.pyi]
2062+
2063+
[case testParamSpecCorrectParameterNameInference]
2064+
from typing import Callable, Protocol
2065+
from typing_extensions import ParamSpec, Concatenate
2066+
2067+
def a(i: int) -> None: ...
2068+
def b(__i: int) -> None: ...
2069+
2070+
class WithName(Protocol):
2071+
def __call__(self, i: int) -> None: ...
2072+
NoName = Callable[[int], None]
2073+
2074+
def f1(__fn: WithName, i: int) -> None: ...
2075+
def f2(__fn: NoName, i: int) -> None: ...
2076+
2077+
P = ParamSpec("P")
2078+
def d(f: Callable[P, None], fn: Callable[Concatenate[Callable[P, None], P], None]) -> Callable[P, None]:
2079+
def inner(*args: P.args, **kwargs: P.kwargs) -> None:
2080+
fn(f, *args, **kwargs)
2081+
return inner
2082+
2083+
reveal_type(d(a, f1)) # N: Revealed type is "def (i: builtins.int)"
2084+
reveal_type(d(a, f2)) # N: Revealed type is "def (i: builtins.int)"
2085+
reveal_type(d(b, f1)) # E: Cannot infer type argument 1 of "d" \
2086+
# N: Revealed type is "def (*Any, **Any)"
2087+
reveal_type(d(b, f2)) # N: Revealed type is "def (builtins.int)"
2088+
[builtins fixtures/paramspec.pyi]

0 commit comments

Comments
 (0)