-
Notifications
You must be signed in to change notification settings - Fork 714
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
Enhanced typing of subscribe()
#847
Enhanced typing of subscribe()
#847
Conversation
I have some doubts whether this will work with multiple event types (ie. unions). Have you checked that? |
TLDR;+1:
+0:
-1:
So I suppose that leaves me -1 (for now, I will spend some time to see if I can improve this).
I did, this example has no type error: from examples import scheduler
from apscheduler import JobAcquired, JobReleased
def handler(event: JobAcquired | JobReleased) -> None:
...
scheduler.subscribe(handler, {JobAcquired, JobReleased})
reveal_type({JobAcquired, JobReleased}) # Revealed type is "builtins.set[builtins.type]" mypy narrows these down to Instead, using reveal_type({TaskAdded, TaskUpdated})
# Revealed type is "builtins.set[def (*, timestamp: Any =, task_id: builtins.str) -> apscheduler._events.DataStoreEvent] For these, mypy reduces them to a set of callables that return a from examples import scheduler
from apscheduler import DataStoreEvent, TaskAdded, TaskUpdated
def handler(event: DataStoreEvent) -> None:
...
scheduler.subscribe(handler, {TaskAdded, TaskUpdated}) B/c It would be less surprising if it would allow: def handler(event: TaskAdded | TaskUpdated) -> None:
.... However, in any case, type narrowing is going to be required in a multi-event type callback. |
Thanks for taking the time to investigate. I'll mull it over once more. |
This is the nicest I've been able to get the behavior (diff is against this branch, and only adjusts the async scheduler interface, other interfaces would have to be updated also): diff --git a/src/apscheduler/_schedulers/async_.py b/src/apscheduler/_schedulers/async_.py
index 6c5ae94..b975c90 100644
--- a/src/apscheduler/_schedulers/async_.py
+++ b/src/apscheduler/_schedulers/async_.py
@@ -11,7 +11,7 @@ from functools import partial
from inspect import isbuiltin, isclass, ismethod, ismodule
from logging import Logger, getLogger
from types import TracebackType
-from typing import Any, Callable, Iterable, Mapping, cast
+from typing import Any, Callable, Iterable, Mapping, cast, overload
from uuid import UUID, uuid4
import anyio
@@ -215,10 +215,30 @@ class AsyncScheduler:
await self.data_store.cleanup()
self.logger.info("Cleaned up expired job results and finished schedules")
+ @overload
def subscribe(
self,
callback: Callable[[T_Event], Any],
- event_types: type[T_Event] | Iterable[type[T_Event]] | None = None,
+ event_types: type[T_Event],
+ **kwargs: Any,
+ ) -> Subscription:
+ ...
+
+ @overload
+ def subscribe(
+ self,
+ callback: Callable[[Event], Any],
+ event_types: Iterable[type[Event]],
+ *,
+ one_shot: bool = ...,
+ is_async: bool = ...,
+ ) -> Subscription:
+ ...
+
+ def subscribe(
+ self,
+ callback: Callable[[T_Event], Any] | Callable[[Event], Any],
+ event_types: type[T_Event] | Iterable[type[Event]] | None = None,
*,
one_shot: bool = False,
is_async: bool = True, With this pattern it allows the callback to share the type of a single event type subscription, for example, this type checks fine: def handler(event: JobAcquired) -> None:
...
scheduler.subscribe(handler, JobAcquired) And for the iterable of types it behaves the same as it does now, i.e., this is an error: def handler(event: TaskAdded | TaskUpdated) -> None:
...
scheduler.subscribe(handler, {TaskAdded, TaskUpdated})
# error: Argument 1 to "subscribe" of "AsyncScheduler" has incompatible type "Callable[[TaskAdded | TaskUpdated], None]"; expected "Callable[[Event], Any]" [arg-type] This is no error: def handler(event: Event) -> None:
...
scheduler.subscribe(handler, {TaskAdded, TaskUpdated}) Its not a bad middle ground as it removes the need to have a redundant type narrowing or type ignore where the handler should only receive a single type, and leaves the rest of the behavior as it is. We could go further and define overloads for def handler(event: DataStoreEvent) -> None:
...
scheduler.subscribe(handler, {TaskAdded, TaskUpdated}) But given that neither of those types define any attributes that are common across all of their sub types, it wouldn't be of any benefit. |
It sounds like it's at least an improvement over the existing typing. Any idea if the test failures are caused by these changes? |
Hm, looks more like an incompatibility between pytest 8 and pytest-lazyfixture. |
Yup, looks like: TvoroG/pytest-lazy-fixture#65 |
6bfd75f
to
f2b6f3a
Compare
This PR adds type var `T_Event` and uses it to type the `subscribe()` interfaces. Fixes an issue where subscribing a handler would raise a type error if the handler is typed to receive a subclass of `Event`. Allows type checkers to verify that the handler passed to `subscribe()` can support the event types it is assigned to handle. Closes agronholm#846
for more information, see https://pre-commit.ci
Relaxes the signatures of methods on `MQTTEventBroker` that are registered as callbacks on the mqtt client so that they will work as either v1 or v2 client style callbacks. Adds a tox env to explicitly test against paho-mqtt v1 (`$ tox -e paho-mqtt-v1`).
This commit keeps the behavior of the subscribe methods as they are for the case where a subscription is added for an iterable of event types. However, for the case where a subscription is added for a single event type, it allows the callable to be typed to receive an instance of that event type, removing the need to type-narrow in that case.
f2b6f3a
to
b4db236
Compare
Alright, finally got around to merging this. Thanks! |
This PR adds type var
T_Event
and uses it to type thesubscribe()
interfaces.Fixes an issue where subscribing a handler would raise a type error if the handler is typed to receive a subclass of
Event
.Allows type checkers to verify that the handler passed to
subscribe()
can support the event types it is assigned to handle.Closes #846