I have a class that may contain a function that will be called elsewhere. Depending on what's calling it, it could have a variable number of arguments. One owner might be calling it with the arguments (event, int, str)
and another might be calling it with (event, str, bool, dict)
. There are validations that occur later to ensure that the signature matches what is needed by the owner. For type hinting purposes, I need to ensure that the signature being passed matches anything starting with our Event
object, with anything after being fine and dandy. As a result, functions like def foo(event: Event, a: int, b: bool)
and def (event: ClickEvent, c: dict, *args, **kwargs) -> typing.Coroutine[Any, Any, str]
are both absolutely valid.
Given the following example:
from typing import *
from dataclasses import dataclass
PARAMS = ParamSpec("PARAMS")
@dataclass
class Event:
field1: int
field2: bool
name: str
def get_name(self) -> str:
return self.name
class SomeCaller:
...
class Element:
...
class ClickEvent(Event):
def __init__(self, field1: int, field2: int, target: str):
super().__init__(field1=field1, field2=field2, name="click")
self.__target = target
@property
def target(self) -> str:
return self.__target
# The type hint in question
HANDLER = Callable[
[
Event,
PARAMS
], Union[Any, Coroutine]
]
def control(event: Event, *args, **kwargs) -> bool:
pass
def false_control(event: ClickEvent, *args, **kwargs) -> bool:
pass
async def async_function(event: Event, arg1: int, arg2: int, *args, **kwargs):
return 9
def function(event: ClickEvent, arg1: int, arg2: int, *args, **kwargs):
return 7
def other_function(event: Event, caller: SomeCaller, element: Element):
return 8
class EventHandlerWhatsit:
def __init__(self, handler: HANDLER):
self.__handler = handler
control_value = EventHandlerWhatsit(control)
false_control_value = EventHandlerWhatsit(false_control)
async_function_value = EventHandlerWhatsit(async_function)
function_value = EventHandlerWhatsit(function)
other_function_value = EventHandlerWhatsit(other_function)
def main():
print("This works")
if __name__ == "__main__":
main()
Typing hinting warnings appear on the declaration of false_control_value
, async_function_value
, function_value
, and other_function_value
, all with warnings like Expected type '(Event, ParamSpec("PARAMS")) -> Coroutine | Any' (matched generic type '(Event, ParamSpec("PARAMS")) -> Coroutine | Any'), got '(event: Event, arg1: int, arg2: int, args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any' instead
. The declaration of control_value
presents no issue. The assignment of self.__handler = handler
within the initializer in EventHandlerWhatsit
also shows a warning of Expected type '(Event, ParamSpec("PARAMS")) -> Coroutine | Any', got '(Event, ParamSpec("PARAMS")) -> Coroutine | Any' instead
, which I find odd.
All it needs to indicate is "Something that may be called as long as it starts with a parameter that is a subclass of Event
". The names don't matter. I've played with the definition of HANDLER
in all sorts of ways, such as defining *args
and **kwargs
as Tuple[Any, ...]
and Dict[str, Any]
(with and without Optional
), with the extra parameters, and still end up with the same sorts of warnings.
I'm stuck in python 3.8 and I'm editing in PyCharm, which shows the warnings.
Any ideas?
EDIT:
@Daniil Fajnberg and @SUTerliakov provided perfect answers in the comments:
HANDLER = Callable[
Concatenate[
Event,
PARAMS
], Union[Any, Coroutine]
]
Concatenate
allows the annotation to match with slightly different values than what is in the input definition.
Take the following invalid code:
def test_inner(arg: Callable[[str, int, P], Any]):
pass
def test_input(i: str, j: int, q: str = None, *args, **kwargs):
pass
def test_outer():
test_inner(test_input)
The linter will trigger a warning because the existence of the optional q
parameter does not fit the expectation of the arg
parameter in test_inner
. Changing the definition of arg
in test_inner
to look like arg: Callable[Concatenate[str, int, P], Any]
, however, and the linter is just fine.
A word of warning: ParamSpec
and Concatenate
were introduced in Python 3.10. If you need to use an older version due to environment constraints, use the typing_extensions
package to provide that functionality.
@Daniil Fajnberg and @SUTerliakov provided perfect answers in the comments:
HANDLER = Callable[
Concatenate[
Event,
PARAMS
], Union[Any, Coroutine]
]
Concatenate
allows the annotation to match with slightly different values than what is in the input definition.
Take the following invalid code:
def test_inner(arg: Callable[[str, int, P], Any]):
pass
def test_input(i: str, j: int, q: str = None, *args, **kwargs):
pass
def test_outer():
test_inner(test_input)
The linter will trigger a warning because the existence of the optional q
parameter does not fit the expectation of the arg
parameter in test_inner
. Changing the definition of arg
in test_inner
to look like arg: Callable[Concatenate[str, int, P], Any]
, however, and the linter is just fine.
A word of warning: ParamSpec
and Concatenate
were introduced in Python 3.10. If you need to use an older version due to environment constraints, use the typing_extensions
package to provide that functionality.