typing_extensions icon indicating copy to clipboard operation
typing_extensions copied to clipboard

Generic with ParamSpec raise TypeError

Open omaxx opened this issue 2 years ago • 3 comments

I create generic class with specifying __init__ arguments through ParamSpec:

from typing import Generic
from typing_extensions import ParamSpec

P = ParamSpec('P')

class CLS(Generic[P]):
    def __init__(self, *args: P.args, **kwargs: P.kwargs):
        ...

class ONE(CLS[int]):
    ...

class TWO(CLS[int, int]):
    ...

# should pass type checks:
ONE(1)
TWO(1, 2)
# should fail type checks:
ONE(1, 2)
TWO(1)

mypy type checking works as expected, but during runtime I got next error:

Traceback (most recent call last):
  File "temp/issue.py", line 13, in <module>
    class TWO(CLS[int, int]):
  File "/usr/local/Cellar/[email protected]/3.9.16/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 277, in inner
    return func(*args, **kwds)
  File "/usr/local/Cellar/[email protected]/3.9.16/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 1004, in __class_getitem__
    _check_generic(cls, params, len(cls.__parameters__))
  File "/usr/local/lib/python3.9/site-packages/typing_extensions.py", line 109, in _check_generic
    raise TypeError(f"Too {'many' if alen > elen else 'few'} parameters for {cls};"
TypeError: Too many parameters for <class '__main__.CLS'>; actual 2, expected 1

Is it a bug? It works fine in 3.10 and 3.11

omaxx avatar Mar 12 '23 21:03 omaxx

I believe this is covered by the note in the README that says "Certain PEP 612 special cases in user-defined Generics are also not available." We could create a typing_extensions.Generic to add support for this though, if we can't hack it into typing.Generic.

JelleZijlstra avatar Mar 12 '23 22:03 JelleZijlstra

It works with that hack:

def define_func(func: Callable[P, None]) -> Callable[P, CLS[P]]:
    def _func(*args: P.args, **kwargs: P.kwargs) -> CLS[P]:
        return CLS(*args, **kwargs)
    return _func

def single(x: int) -> None: pass
def double(x: int, y: int) -> None: pass

ONE = define_func(single)
TWO = define_func(double)

ONE(1)
TWO(1, 2)
ONE(1, 2)
TWO(1)

omaxx avatar Mar 13 '23 00:03 omaxx

I added a reference to this limitation to the docs in #171. I'd still accept a PR fixing it.

JelleZijlstra avatar May 22 '23 00:05 JelleZijlstra