Substitution of ParamSpec in Concatenate produces weird results:
>>> import typing
>>> P = typing.ParamSpec('P')
>>> typing.Concatenate[str, P][int]
typing.Concatenate[str, int]
>>> typing.Concatenate[str, P][[int]]
typing.Concatenate[str, (<class 'int'>,)]
>>> typing.Concatenate[str, P][typing.Concatenate[int, P]]
typing.Concatenate[str, typing.Concatenate[int, ~P]]
But all these results are invalid:
>>> typing.Concatenate[str, int]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/serhiy/py/cpython/Lib/typing.py", line 309, in inner
return func(*args, **kwds)
^^^^^^^^^^^^^^^^^^^
File "/home/serhiy/py/cpython/Lib/typing.py", line 400, in __getitem__
return self._getitem(self, parameters)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/serhiy/py/cpython/Lib/typing.py", line 595, in Concatenate
raise TypeError("The last parameter to Concatenate should be a "
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: The last parameter to Concatenate should be a ParamSpec variable.
>>> typing.Concatenate[str, (int,)]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/serhiy/py/cpython/Lib/typing.py", line 309, in inner
return func(*args, **kwds)
^^^^^^^^^^^^^^^^^^^
File "/home/serhiy/py/cpython/Lib/typing.py", line 400, in __getitem__
return self._getitem(self, parameters)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/serhiy/py/cpython/Lib/typing.py", line 595, in Concatenate
raise TypeError("The last parameter to Concatenate should be a "
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: The last parameter to Concatenate should be a ParamSpec variable.
>>> typing.Concatenate[str, [int]]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/serhiy/py/cpython/Lib/typing.py", line 309, in inner
return func(*args, **kwds)
^^^^^^^^^^^^^^^^^^^
File "/home/serhiy/py/cpython/Lib/typing.py", line 400, in __getitem__
return self._getitem(self, parameters)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/serhiy/py/cpython/Lib/typing.py", line 595, in Concatenate
raise TypeError("The last parameter to Concatenate should be a "
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: The last parameter to Concatenate should be a ParamSpec variable.
>>> typing.Concatenate[str, typing.Concatenate[int, P]]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/serhiy/py/cpython/Lib/typing.py", line 309, in inner
return func(*args, **kwds)
^^^^^^^^^^^^^^^^^^^
File "/home/serhiy/py/cpython/Lib/typing.py", line 400, in __getitem__
return self._getitem(self, parameters)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/serhiy/py/cpython/Lib/typing.py", line 595, in Concatenate
raise TypeError("The last parameter to Concatenate should be a "
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: The last parameter to Concatenate should be a ParamSpec variable.
I expect that
1. The last parameter to Concatenate can be a Concatenate. Inner Concatenate should merge with the external one:
Concatenate[str, Concatenate[int, P]] -> Concatenate[str, int, P]
Concatenate[str, P][Concatenate[int, P]] -> Concatenate[str, int, P]
2. The last parameter to Concatenate can be a list of types. The result should be a list of types.
Concatenate[str, [int, dict]] -> [str, int, dict]
Concatenate[str, P][[int, dict]] -> [str, int, dict]
3. The last parameter to Concatenate can be an ellipsis.
Concatenate[str, ...]
Concatenate[str, P][...] -> Concatenate[str, ...]
|
Should Concatenate support substitution to begin with? PEP 612 doesn't say anything, and I am fairly certain it's a special typing form, not a generic. So I don't really understand what it means to substitute Concatenate.
Then again, Callable with a nested Concatenate can be substituted, and we currently implement that by using Concatenate's substitution behavior as proxy. But again, the meaning isn't clear to me.
I also noticed this strange part in PEP 612 about user-defined generic classes:
"`Generic[P]` makes a class generic on `parameters_expressions` (when P is a ParamSpec)":
...
class X(Generic[T, P]):
f: Callable[P, int]
x: T
def f(x: X[int, Concatenate[int, P_2]]) -> str: ... # Accepted (KJ: What?)
...
The grammar for `parameters_expression` is:
parameters_expression ::=
| "..."
| "[" [ type_expression ("," type_expression)* ] "]"
| parameter_specification_variable
| concatenate "["
type_expression ("," type_expression)* ","
parameter_specification_variable
"]"
I'm very confused. Does this mean Concatenate is valid when substituting user generics? Maybe I should ask the PEP authors?
My general sense when I implemented the PEP was that it was intended primarily for static type checking only. IMO, runtime correctness wasn't its concern.
|
Concatenate[int, ...] I would interpret as a function signature with first argument int, followed by arbitrary arguments afterwards. I haven't run into this case, but ... is allowed in other spots Paramspec is allowed currently.
P = Paramspec("P")
class Foo(Generic[P]):
...
Foo[...] # Allowed
Callable[..., None] # Allowed
Are there any other places a paramspec is allowed? Generic type argument, first argument to callable, last to concatenate, anything else?
I'm unaware of any type checking use case for Concatenate[int, ...]. You can practically get same thing by using a paramspec variable without using it elsewhere so that it captures, but then effectively discards that information.
def use_func1(f: Callable[Concatenate[int, P], None]) -> None:
...
def use_func2(f: Callable[Concatenate[int, ...], None]) -> None:
...
feels like those two signatures should encode same information.
|