diff --git a/CHANGES b/CHANGES index 97f6e328c..3e552ec95 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,7 @@ Incompatible changes when ``:inherited-members:`` and ``:special-members:`` are given. * #6830: py domain: ``meta`` fields in info-field-list becomes reserved. They are not displayed on output document now +* The structure of ``sphinx.events.EventManager.listeners`` has changed Deprecated ---------- @@ -36,6 +37,8 @@ Features added * #6558: glossary: emit a warning for duplicated glossary entry * #6558: std domain: emit a warning for duplicated generic objects * #6830: py domain: Add new event: :event:`object-description-transform` +* Support priority of event handlers. For more detail, see + :py:meth:`.Sphinx.connect()` Bugs fixed ---------- diff --git a/sphinx/application.py b/sphinx/application.py index 515d962dc..fbc637e60 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -404,17 +404,25 @@ class Sphinx: raise VersionRequirementError(version) # event interface - def connect(self, event: str, callback: Callable) -> int: + def connect(self, event: str, callback: Callable, priority: int = 500) -> int: """Register *callback* to be called when *event* is emitted. For details on available core events and the arguments of callback functions, please see :ref:`events`. + Registered callbacks will be invoked on event in the order of *priority* and + registration. The priority is ascending order. + The method returns a "listener ID" that can be used as an argument to :meth:`disconnect`. + + .. versionchanged:: 3.0 + + Support *priority* """ - listener_id = self.events.connect(event, callback) - logger.debug('[app] connecting event %r: %r [id=%s]', event, callback, listener_id) + listener_id = self.events.connect(event, callback, priority) + logger.debug('[app] connecting event %r (%d): %r [id=%s]', + event, priority, callback, listener_id) return listener_id def disconnect(self, listener_id: int) -> None: diff --git a/sphinx/events.py b/sphinx/events.py index e6ea379eb..ff49f290c 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -11,8 +11,9 @@ """ import warnings -from collections import OrderedDict, defaultdict -from typing import Any, Callable, Dict, List +from collections import defaultdict +from operator import attrgetter +from typing import Any, Callable, Dict, List, NamedTuple from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import ExtensionError @@ -26,6 +27,10 @@ if False: logger = logging.getLogger(__name__) +EventListener = NamedTuple('EventListener', [('id', int), + ('handler', Callable), + ('priority', int)]) + # List of all known core events. Maps name to arguments description. core_events = { @@ -57,7 +62,7 @@ class EventManager: RemovedInSphinx40Warning) self.app = app self.events = core_events.copy() - self.listeners = defaultdict(OrderedDict) # type: Dict[str, Dict[int, Callable]] + self.listeners = defaultdict(list) # type: Dict[str, List[EventListener]] self.next_listener_id = 0 def add(self, name: str) -> None: @@ -66,20 +71,22 @@ class EventManager: raise ExtensionError(__('Event %r already present') % name) self.events[name] = '' - def connect(self, name: str, callback: Callable) -> int: + def connect(self, name: str, callback: Callable, priority: int) -> int: """Connect a handler to specific event.""" if name not in self.events: raise ExtensionError(__('Unknown event name: %s') % name) listener_id = self.next_listener_id self.next_listener_id += 1 - self.listeners[name][listener_id] = callback + self.listeners[name].append(EventListener(listener_id, callback, priority)) return listener_id def disconnect(self, listener_id: int) -> None: """Disconnect a handler.""" - for event in self.listeners.values(): - event.pop(listener_id, None) + for listeners in self.listeners.values(): + for listener in listeners[:]: + if listener.id == listener_id: + listeners.remove(listener) def emit(self, name: str, *args: Any) -> List: """Emit a Sphinx event.""" @@ -91,12 +98,13 @@ class EventManager: pass results = [] - for callback in self.listeners[name].values(): + listeners = sorted(self.listeners[name], key=attrgetter("priority")) + for listener in listeners: if self.app is None: # for compatibility; RemovedInSphinx40Warning - results.append(callback(*args)) + results.append(listener.handler(*args)) else: - results.append(callback(self.app, *args)) + results.append(listener.handler(self.app, *args)) return results def emit_firstresult(self, name: str, *args: Any) -> Any: diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 000000000..4881588a4 --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,24 @@ +""" + test_events + ~~~~~~~~~~~ + + Test the EventManager class. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from sphinx.events import EventManager + + +def test_event_priority(): + result = [] + events = EventManager(object()) # pass an dummy object as an app + events.connect('builder-inited', lambda app: result.append(1), priority = 500) + events.connect('builder-inited', lambda app: result.append(2), priority = 500) + events.connect('builder-inited', lambda app: result.append(3), priority = 200) # eariler + events.connect('builder-inited', lambda app: result.append(4), priority = 700) # later + events.connect('builder-inited', lambda app: result.append(5), priority = 500) + + events.emit('builder-inited') + assert result == [3, 1, 2, 5, 4]