Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callable with variadic fixed arguments that doesn't mean *args #1301

Open
sykire opened this issue Dec 2, 2022 · 4 comments
Open

Callable with variadic fixed arguments that doesn't mean *args #1301

sykire opened this issue Dec 2, 2022 · 4 comments
Labels
topic: other Other topics not covered

Comments

@sykire
Copy link

sykire commented Dec 2, 2022

Suppose I have a type of function called CommandHandler, that is a function that receives a command as its first parameter but then could have other parameters.
Examples

class Command:
    pass

class RegisterUser(Command):
    pass

class DeleteUser(Command):
    pass

def register_user(command: RegisterUser, db : Db) -> None:
    ...

def delete_user(command: DeleteUser, dispatch: EventDispatcher, cache: Cache) -> None:
    ...

mappings : dict[Command, CommandHandler] = {
    RegisterUser: register_user,
    DeleteUser: delete_user
}

What would be the definition of CommandHandler?

Ideally it would be:

C= TypeVar("C", bound=Command)
CommandHandler = Callable[[C, ...], None] 

But this syntax is not possible.

I've found in the documentation that Concatenate could receive ... at the end,

Concatenate is currently only valid when used as the first argument to a Callable. The last parameter to Concatenate must be a ParamSpec or ellipsis (...).

so it could be:

C= TypeVar("C", bound=Command)
CommandHandler = Callable[Concatenate[C, ...], None] 

But this returns an error from Pylance:

image

One could say Callback Protocol could solve this like:

C= TypeVar("C", bound=Command)
class CommandHandler(Protocol):
    def __call__(self, __command: C, *__args: Any) -> None: ...

But it only receives functions that has an implicit variadic argument, it doesn't work with variadic forms of the function.

So far the only way I could solve this is through the Union of many Callables, but it doesn't look like the best solution.

At the end CommandHandler can only be defined as:

C= TypeVar("C", bound=Command)
CommandHandler = Union[
    Callable[[C, Any], None],
    Callable[[C, Any, Any], None],
    Callable[[C, Any, Any, Any], None],
    Callable[[C, Any, Any, Any, Any], None],
    Callable[[C, Any, Any, Any, Any, Any], None],
    Callable[[C, Any, Any, Any, Any, Any, Any], None],
    Callable[[C, Any, Any, Any, Any, Any, Any, Any], None],
    Callable[
        [C, Any, Any, Any, Any, Any, Any, Any, Any], None
    ],
]

Related:
python/cpython#88954
https://stackoverflow.com/questions/57658879/python-type-hint-for-callable-with-variable-number-of-str-same-type-arguments

@sykire sykire added the topic: other Other topics not covered label Dec 2, 2022
@hmc-cs-mdrissi
Copy link

TypeVarTuple can be used to solve this. What you want is,

Ts = TypeVarTuple("Ts")
C = TypeVar("C", bound=Command)
Callable[[C, *Ts], None]

Minor note, most of the time if you accept a callable with return None you probably don't care about return type and should let it be object (Callable[[C, *Ts], object])

@erictraut
Copy link
Collaborator

erictraut commented Dec 2, 2022

@DrecDroid, PEP 612 appears to indicate that the last argument to Concatenate must be a ParamSpec, not an ellipsis. The section titled valid use locations indicates that the last argument for Concatenate must be a "parameter_specification_variable". This explains why pyright (the type checker upon which pylance is built) considers an ellipsis an error here.

I see that the latest Python 3.11 documentation for Concatenate indicates that an ellipsis is allowed here as well. Either I'm misinterpreting the PEP, this aspect of Concatenate was modified after the PEP was written, or the 3.11 documentation is incorrect. I'm not sure which.

@sykire
Copy link
Author

sykire commented Dec 2, 2022

@hmc-cs-mdrissi Thanks, good to know, although it appears to be a feature for 3.11.

Unpack operator in subscript requires Python 3.11 or newer Pylance

@erictraut Yes I was confused for that also.

@hmc-cs-mdrissi
Copy link

The Concatenate[P, ...] is an unresolved question in this issue.

The 3.11 typevartuple is not much of a restriction as you can use typing_extensions even on 3.7 and use typevartuple. You'll just need to use Unpack[Ts] instead of *Ts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: other Other topics not covered
Projects
None yet
Development

No branches or pull requests

3 participants