Skip to main content

notificationcenter

tests coverage python

This is a Python port of NotificationCenter from macOS. It is a faithful reimplementation of Apple's NSNotificationCenter (the Foundation/Cocoa class, exposed in Swift simply as NotificationCenter) for Python: a tiny, dependency-free, thread-safe, singleton one-to-many notification bus. One object announces that something happened and any number of others react, without either side knowing about the other. The one deliberate improvement over the original is that notification names must be Enum members rather than strings, so they are typo-proof, autocompletable, and self-documenting.

pip install notifcenter        # distribution name
import notificationcenter          # import name

The plain notificationcenter name on PyPI belongs to an abandoned 2016 package, so the distribution ships as notifcenter while the import stays notificationcenter.

Requirements: Python 3.6+ and zero third-party dependencies (only the standard-library enum and threading). The code itself is even 3.5-clean; 3.6 is the declared floor purely because older tooling is impractical.

Why

Use it when one object needs to tell many others that something happened, without knowing who they are or what they will do. The poster and the observers stay fully decoupled: the poster just announces, and observers react. This is exactly the role NSNotificationCenter plays on macOS and iOS, brought over to Python.

Quick start

from enum import Enum
from notificationcenter import NotificationCenter

class DeviceNotification(Enum):
    will_move = "will_move"
    did_move  = "did_move"

class Logger:
    def __init__(self):
        NotificationCenter().add_observer(self, self.on_move, DeviceNotification.did_move)

    def on_move(self, notification):
        print("moved to", notification.user_info["position"])

logger = Logger()

# somewhere else, with no reference to logger:
NotificationCenter().post_notification(
    DeviceNotification.did_move, notifying_object=some_device,
    user_info={"position": (1, 2, 3)},
)
# -> moved to (1, 2, 3)

API

NotificationCenter() always returns the same singleton instance.

Method Purpose
add_observer(observer, method, notification_name=None, observed_object=None) Register method to be called with a Notification when notification_name is posted. If observed_object is given, only notifications from that specific object are forwarded.
remove_observer(observer, notification_name=None, observed_object=None) Stop notifying observer. Omit notification_name to remove it from every notification.
post_notification(notification_name, notifying_object, user_info=None) Announce notification_name, invoking every matching observer's callback.
observers_count() Total number of registered observer entries.
clear() Remove all observers.

Each callback receives a Notification with these attributes: .name is the Enum member that was posted, .object is the object that posted it (the notifying_object), and .user_info is the optional dict of extra data.

Notification names must be Enum members; passing a string raises ValueError.

Common patterns

Observe only one sender. Pass observed_object to hear a notification only when it comes from a specific object, ignoring the same notification from others:

NotificationCenter().add_observer(
    self, self.on_move, DeviceNotification.did_move, observed_object=my_stage
)

Clean up on teardown. An observer that outlives its usefulness keeps getting called (and keeps the object alive). Remove it when you are done, which is the most common observer-pattern bug:

def close(self):
    NotificationCenter().remove_observer(self)   # unregister from every notification

Update a GUI safely. Callbacks run on the thread that posted the notification, which may be a worker thread. Most GUI toolkits, Tkinter included, are not thread-safe, and you must not call them from another thread, not even root.after, which is itself a Tk call. The portable way is to hand the data to the main thread through a thread-safe queue.Queue that the main thread drains on a timer:

import queue

class PositionLabel:
    def __init__(self, root, label):
        self.root, self.label = root, label
        self._inbox = queue.Queue()
        NotificationCenter().add_observer(self, self.on_move, DeviceNotification.did_move)
        self.root.after(100, self._drain)          # polling loop, started on the main thread

    def on_move(self, notification):               # may run on a worker thread
        self._inbox.put(notification.user_info)    # Queue is thread-safe; no Tk call here

    def _drain(self):                              # always runs on the main thread
        try:
            while True:
                self.label.config(text=str(self._inbox.get_nowait()))
        except queue.Empty:
            pass
        self.root.after(100, self._drain)

Some toolkits do provide a genuinely thread-safe cross-thread post (wxPython's wx.CallAfter, Qt's queued signals) and let you skip the queue; Tkinter is the outlier that does not.

Thread safety

All operations are guarded by a re-entrant lock, so you can add and remove observers and post notifications from multiple threads. Observer callbacks, however, run synchronously on the thread that posted the notification, and they run while that lock is held, so keep them quick and non-blocking. If a callback needs to update a GUI, do not call the toolkit from that thread; use the queue-and-drain pattern shown under "Update a GUI safely" above.

Testing

100% line and branch coverage (17 tests, including doctests in the module). The CI enforces it, so coverage falling under 100% breaks the build.

pip install -e ".[dev]"
coverage run -m pytest -q --doctest-modules src tests
coverage report -m        # fails if coverage drops below 100%

License

MIT © Daniel C. Côté

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

notifcenter-1.0.0.tar.gz (9.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

notifcenter-1.0.0-py3-none-any.whl (9.7 kB view details)

Uploaded Python 3

File details

Details for the file notifcenter-1.0.0.tar.gz.

File metadata

  • Download URL: notifcenter-1.0.0.tar.gz
  • Upload date:
  • Size: 9.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for notifcenter-1.0.0.tar.gz
Algorithm Hash digest
SHA256 9294bd40ac3e0e927de678389b9ef13b788609f4a0b379b8e74cabca1a8ae026
MD5 21d02feae613460b74f6bf3e15f874cb
BLAKE2b-256 95d8b5972db575849b35b750f01102688f3ba4ee782b027bf20d563643325585

See more details on using hashes here.

File details

Details for the file notifcenter-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: notifcenter-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 9.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.2

File hashes

Hashes for notifcenter-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cd52cf1651eaa6fa6327ac49bb3e4044dab74d590285b59d2ae50a86f182e91f
MD5 962e7ce9477ad6049a364ef0fa70304c
BLAKE2b-256 002ceab955a6fff7e8c668096044b3af7fd5e176a94dcec647df20fe5b7bd9de

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page