Skip to content

Commit fc660b2

Browse files
committed
Add on_dispatched event
This event is fired when a notification was sent to the notifications server. This is less useful as class-level callback, but very useful with `Notification`, because it allows for a better separation of concerns (separates creating and sending notifications).
1 parent ff32314 commit fc660b2

File tree

13 files changed

+77
-0
lines changed

13 files changed

+77
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ async def main() -> None:
9191
reply_field=ReplyField(
9292
on_replied=lambda text: print("Brutus replied:", text),
9393
),
94+
on_dispatched=lambda: print("Notification showing"),
9495
on_clicked=lambda: print("Notification clicked"),
9596
on_dismissed=lambda: print("Notification dismissed"),
9697
sound=DEFAULT_SOUND,

examples/eventloop.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async def main() -> None:
2727
button_title="Send",
2828
on_replied=lambda text: print("Brutus replied:", text),
2929
),
30+
on_dispatched=lambda: print("Notification showing"),
3031
on_clicked=lambda: print("Notification clicked"),
3132
on_dismissed=lambda: print("Notification dismissed"),
3233
sound=DEFAULT_SOUND,

examples/eventloop_handlers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
asyncio.set_event_loop_policy(EventLoopPolicy())
1212

1313

14+
def on_dispatched(identifier: str) -> None:
15+
print(f"Notification '{identifier}' is showing now")
16+
17+
1418
def on_clicked(identifier: str) -> None:
1519
print(f"Notification '{identifier}' was clicked")
1620

@@ -29,6 +33,7 @@ def on_replied(identifier: str, reply: str) -> None:
2933

3034
async def main() -> None:
3135
notifier = DesktopNotifier(app_name="Sample App")
36+
notifier.on_dispatched = on_dispatched
3237
notifier.on_clicked = on_clicked
3338
notifier.on_dismissed = on_dismissed
3439
notifier.on_button_pressed = on_button_pressed

examples/synchronous.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
button_title="Send",
2424
on_replied=lambda text: print("Brutus replied:", text),
2525
),
26+
on_dispatched=lambda: print("Notification showing"),
2627
on_clicked=lambda: print("Notification clicked"),
2728
on_dismissed=lambda: print("Notification dismissed"),
2829
sound=DEFAULT_SOUND,

src/desktop_notifier/backends/base.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def __init__(self, app_name: str) -> None:
2828
self.app_name = app_name
2929
self._notification_cache: dict[str, Notification] = dict()
3030

31+
self.on_dispatched: Callable[[str], Any] | None = None
3132
self.on_clicked: Callable[[str], Any] | None = None
3233
self.on_dismissed: Callable[[str], Any] | None = None
3334
self.on_button_pressed: Callable[[str, str], Any] | None = None
@@ -67,6 +68,8 @@ async def send(self, notification: Notification) -> None:
6768
logger.debug("Notification sent: %s", notification)
6869
self._notification_cache[notification.identifier] = notification
6970

71+
self.handle_dispatched(notification.identifier, notification)
72+
7073
def _clear_notification_from_cache(self, identifier: str) -> Notification | None:
7174
"""
7275
Removes the notification from our cache. Should be called by backends when the
@@ -142,6 +145,14 @@ async def get_capabilities(self) -> frozenset[Capability]:
142145
"""
143146
...
144147

148+
def handle_dispatched(
149+
self, identifier: str, notification: Notification | None = None
150+
) -> None:
151+
if notification and notification.on_dispatched:
152+
notification.on_dispatched()
153+
elif self.on_dispatched:
154+
self.on_dispatched(identifier)
155+
145156
def handle_clicked(
146157
self, identifier: str, notification: Notification | None = None
147158
) -> None:

src/desktop_notifier/backends/dbus.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ async def get_capabilities(self) -> frozenset[Capability]:
260260
Capability.TITLE,
261261
Capability.TIMEOUT,
262262
Capability.URGENCY,
263+
Capability.ON_DISPATCHED,
263264
}
264265

265266
# Capabilities supported by some notification servers.

src/desktop_notifier/backends/macos.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ async def get_capabilities(self) -> frozenset[Capability]:
398398
Capability.MESSAGE,
399399
Capability.BUTTONS,
400400
Capability.REPLY_FIELD,
401+
Capability.ON_DISPATCHED,
401402
Capability.ON_CLICKED,
402403
Capability.ON_DISMISSED,
403404
Capability.SOUND,

src/desktop_notifier/backends/winrt.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ async def get_capabilities(self) -> frozenset[Capability]:
298298
Capability.ICON,
299299
Capability.BUTTONS,
300300
Capability.REPLY_FIELD,
301+
Capability.ON_DISPATCHED,
301302
Capability.ON_CLICKED,
302303
Capability.ON_DISMISSED,
303304
Capability.THREAD,

src/desktop_notifier/common.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ class Notification:
221221
"""Text field shown on an interactive notification. This can be used for example
222222
for messaging apps to reply directly from the notification."""
223223

224+
on_dispatched: Callable[[], Any] | None = None
225+
"""Method to call when the notification was sent to the notifications server for display"""
226+
224227
on_clicked: Callable[[], Any] | None = None
225228
"""Method to call when the notification is clicked"""
226229

@@ -289,6 +292,9 @@ class Capability(Enum):
289292
ATTACHMENT = auto()
290293
"""Supports notification attachments. Allowed file types vary by platform."""
291294

295+
ON_DISPATCHED = auto()
296+
"""Supports on-dispatched callbacks"""
297+
292298
ON_CLICKED = auto()
293299
"""Supports on-clicked callbacks"""
294300

src/desktop_notifier/main.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ async def send(
216216
icon: Icon | None = None,
217217
buttons: Sequence[Button] = (),
218218
reply_field: ReplyField | None = None,
219+
on_dispatched: Callable[[], Any] | None = None,
219220
on_clicked: Callable[[], Any] | None = None,
220221
on_dismissed: Callable[[], Any] | None = None,
221222
attachment: Attachment | None = None,
@@ -239,6 +240,7 @@ async def send(
239240
icon=icon,
240241
buttons=tuple(buttons),
241242
reply_field=reply_field,
243+
on_dispatched=on_dispatched,
242244
on_clicked=on_clicked,
243245
on_dismissed=on_dismissed,
244246
attachment=attachment,
@@ -275,6 +277,22 @@ async def get_capabilities(self) -> frozenset[Capability]:
275277
self._capabilities = await self._backend.get_capabilities()
276278
return self._capabilities
277279

280+
@property
281+
def on_dispatched(self) -> Callable[[str], Any] | None:
282+
"""
283+
A method to call when a notification is sent to the notifications server
284+
285+
The method must take the notification identifier as a single argument.
286+
287+
If the notification itself already specifies an on_dispatched handler, it will
288+
be used instead of the class-level handler.
289+
"""
290+
return self._backend.on_dispatched
291+
292+
@on_dispatched.setter
293+
def on_dispatched(self, handler: Callable[[str], Any] | None) -> None:
294+
self._backend.on_dispatched = handler
295+
278296
@property
279297
def on_clicked(self) -> Callable[[str], Any] | None:
280298
"""

src/desktop_notifier/sync.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def send(
8888
icon: Icon | None = None,
8989
buttons: Sequence[Button] = (),
9090
reply_field: ReplyField | None = None,
91+
on_dispatched: Callable[[], Any] | None = None,
9192
on_clicked: Callable[[], Any] | None = None,
9293
on_dismissed: Callable[[], Any] | None = None,
9394
attachment: Attachment | None = None,
@@ -103,6 +104,7 @@ def send(
103104
icon=icon,
104105
buttons=tuple(buttons),
105106
reply_field=reply_field,
107+
on_dispatched=on_dispatched,
106108
on_clicked=on_clicked,
107109
on_dismissed=on_dismissed,
108110
attachment=attachment,

tests/test_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ async def test_send(notifier: DesktopNotifier) -> None:
5757
button_title="Send",
5858
on_replied=lambda text: print("Brutus replied:", text),
5959
),
60+
on_dispatched=lambda: print("Notification showing"),
6061
on_clicked=lambda: print("Notification clicked"),
6162
on_dismissed=lambda: print("Notification dismissed"),
6263
sound=DEFAULT_SOUND,

tests/test_callbacks.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ async def check_supported(notifier: DesktopNotifier, capability: Capability) ->
2828
pytest.skip(f"{notifier} not supported by backend")
2929

3030

31+
@pytest.mark.asyncio
32+
async def test_dispatched_callback_called(notifier: DesktopNotifier) -> None:
33+
class_handler = Mock()
34+
notification_handler = Mock()
35+
notifier.on_dispatched = class_handler
36+
notification = Notification(
37+
title="Julius Caesar",
38+
message="Et tu, Brute?",
39+
on_dispatched=notification_handler,
40+
)
41+
42+
await notifier.send_notification(notification)
43+
44+
class_handler.assert_not_called()
45+
notification_handler.assert_called_once()
46+
47+
3148
@pytest.mark.asyncio
3249
async def test_clicked_callback_called(notifier: DesktopNotifier) -> None:
3350
await check_supported(notifier, Capability.ON_CLICKED)
@@ -135,6 +152,17 @@ async def test_replied_callback_called(notifier: DesktopNotifier) -> None:
135152
notification_handler.assert_called_with("A notification response")
136153

137154

155+
@pytest.mark.asyncio
156+
async def test_dispatched_fallback_handler_called(notifier: DesktopNotifier) -> None:
157+
class_handler = Mock()
158+
notifier.on_dispatched = class_handler
159+
notification = Notification(title="Julius Caesar", message="Et tu, Brute?")
160+
161+
identifier = await notifier.send_notification(notification)
162+
163+
class_handler.assert_called_with(identifier)
164+
165+
138166
@pytest.mark.asyncio
139167
async def test_clicked_fallback_handler_called(notifier: DesktopNotifier) -> None:
140168
await check_supported(notifier, Capability.ON_CLICKED)

0 commit comments

Comments
 (0)