From 1e5f2f99139a09e72d277e544c72dfeaaa3cc13b Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Sun, 30 Nov 2025 00:02:39 +0000 Subject: [PATCH] Improve event manager API documentation (#14122) --- doc/extdev/appapi.rst | 7 +++ doc/extdev/event_callbacks.rst | 4 +- doc/extdev/eventapi.rst | 9 ++++ doc/extdev/index.rst | 23 +++++++--- doc/extdev/utils.rst | 7 --- sphinx/events.py | 84 +++++++++++++++++++++++++++++----- 6 files changed, 107 insertions(+), 27 deletions(-) create mode 100644 doc/extdev/eventapi.rst diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index abf202e19b..c7b17e796a 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -106,6 +106,13 @@ package. Emitting events --------------- +.. attention:: + + Extension developers should prefer using the event manager (``events``) + object directly, via :meth:`.EventManager.emit` + and :meth:`.EventManager.emit_firstresult`, + which have identical behaviour to the methods below. + .. class:: Sphinx :no-index: diff --git a/doc/extdev/event_callbacks.rst b/doc/extdev/event_callbacks.rst index aec9a47e84..7ce7ad4cf2 100644 --- a/doc/extdev/event_callbacks.rst +++ b/doc/extdev/event_callbacks.rst @@ -7,7 +7,7 @@ Connecting callback functions to events is a simple way to extend Sphinx, by hooking into the build process at various points. Use :meth:`.Sphinx.connect` in an extension's ``setup`` function, -or a ``setup`` function in your projects :file:`conf.py`, +or a ``setup`` function in your project's :file:`conf.py`, to connect functions to the events: .. code-block:: python @@ -22,7 +22,7 @@ to connect functions to the events: Extensions can add their own events by using :meth:`.Sphinx.add_event`, and calling them them with - :meth:`.Sphinx.emit` or :meth:`.Sphinx.emit_firstresult`. + :meth:`.EventManager.emit` or :meth:`.EventManager.emit_firstresult`. Core events overview -------------------- diff --git a/doc/extdev/eventapi.rst b/doc/extdev/eventapi.rst new file mode 100644 index 0000000000..7db6ac9fa1 --- /dev/null +++ b/doc/extdev/eventapi.rst @@ -0,0 +1,9 @@ +.. _event-api: + +Event Manager API +----------------- + +.. module:: sphinx.events + +.. autoclass:: EventManager + :members: diff --git a/doc/extdev/index.rst b/doc/extdev/index.rst index 2eff8f0738..149a85edd6 100644 --- a/doc/extdev/index.rst +++ b/doc/extdev/index.rst @@ -49,11 +49,9 @@ extension. These are: **Application** The application object (usually called ``app``) is an instance of - :class:`.Sphinx`. It controls most high-level functionality, such as the - setup of extensions, event dispatching and producing output (logging). - - If you have the environment object, the application is available as - ``env.app``. + :class:`.Sphinx`. + It controls most high-level functionality, such as loading config, + initialising the environment, and the setup of extensions. **Environment** The build environment object (usually called ``env``) is an instance of @@ -67,6 +65,8 @@ extension. These are: If you have the application or builder object, the environment is available as ``app.env`` or ``builder.env``. + In :class:`.SphinxDirective`, :class:`.SphinxRole`, or :class:`.SphinxTransform` + subclasses, the environment is available as ``self.env``. **Builder** The builder object (usually called ``builder``) is an instance of a specific @@ -82,7 +82,17 @@ extension. These are: configuration values set in :file:`conf.py` as attributes. It is an instance of :class:`.Config`. - The config is available as ``app.config`` or ``env.config``. + The config is available as ``env.config``, ``builder.config``, or ``app.config``. + In :class:`.SphinxDirective`, :class:`.SphinxRole`, or :class:`.SphinxTransform` + subclasses, the environment is available as ``self.config``. + +**Events** + The event manager object (usually called ``events``) manages and dispatches + Sphinx's events system. + It is an instance of :class:`.EventManager`. + + The event manager is available as ``env.events``, ``builder.events``, + or ``app.events``. To see an example of use of these objects, refer to :ref:`the tutorials `. @@ -232,6 +242,7 @@ disposal when developing Sphinx extensions. Some are core to Sphinx projectapi envapi builderapi + eventapi collectorapi markupapi domainapi diff --git a/doc/extdev/utils.rst b/doc/extdev/utils.rst index 9e10a0ded8..26974b0069 100644 --- a/doc/extdev/utils.rst +++ b/doc/extdev/utils.rst @@ -32,13 +32,6 @@ components (e.g. :class:`.Config`, :class:`.BuildEnvironment` and so on) easily. :members: -Utility components ------------------- - -.. autoclass:: sphinx.events.EventManager - :members: - - Utility functions ----------------- diff --git a/sphinx/events.py b/sphinx/events.py index cf26ccf913..11c915c7b5 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -80,9 +80,15 @@ class EventManager: self._reraise_errors: bool = app.pdb def add(self, name: str) -> None: - """Register a custom Sphinx event.""" + """Register a custom Sphinx event called *name*. + + This is needed to be able to emit the event. + + :param name: The name of the event. + """ if name in self.events: - raise ExtensionError(__('Event %r already present') % name) + msg = __('Event %r already present') + raise ExtensionError(msg % name) self.events[name] = '' @property @@ -353,9 +359,30 @@ class EventManager: ) -> int: ... def connect(self, name: str, callback: Callable[..., Any], priority: int) -> int: - """Connect a handler to specific event.""" + """Connect a handler to specific event. + + Register *callback* to be called when the *name* event is emitted. + + See :ref:`event callbacks ` for details on available core events + and the arguments of their corresponding callback functions. + + :param name: + The name of the target event. + :param callback: + Callback function for the event. + :param priority: + The priority of the callback. + The callbacks will be invoked in ascending order of *priority*. + :return: + A listener ID, for use with the :meth:`disconnect` method. + + .. versionchanged:: 3.0 + + Support *priority* + """ if name not in self.events: - raise ExtensionError(__('Unknown event name: %s') % name) + msg = __('Unknown event name: %s') + raise ExtensionError(msg % name) listener_id = self.next_listener_id self.next_listener_id += 1 @@ -363,7 +390,11 @@ class EventManager: return listener_id def disconnect(self, listener_id: int) -> None: - """Disconnect a handler.""" + """Disconnect the handler given by *listener_id*. + + :param listener_id: + A listener_id previously returned by :meth:`connect`. + """ for listeners in self.listeners.values(): for listener in listeners.copy(): if listener.id == listener_id: @@ -375,9 +406,25 @@ class EventManager: *args: Any, allowed_exceptions: tuple[type[Exception], ...] = (), ) -> list[Any]: - """Emit a Sphinx event.""" - # not every object likes to be repr()'d (think - # random stuff coming via autodoc) + """Emit a Sphinx event. + + This emits the *name* event and passes *args* to the handler functions. + Return the return values of all handlers as a list. + Do not emit core Sphinx events in extensions! + + :param name: + The name of the event that will be emitted. + :param args: + The arguments for the event, to be passed to the handler functions. + :param allowed_exceptions: + The list of exceptions that are allowed in the handlers. + + .. versionchanged:: 3.1 + + Added *allowed_exceptions* to specify path-through exceptions + """ + # not every object likes to be repr()'d + # (think random stuff coming via autodoc) try: repr_args = repr(args) except Exception: @@ -399,9 +446,9 @@ class EventManager: if self._reraise_errors: raise modname = safe_getattr(listener.handler, '__module__', None) + msg = __('Handler %r for event %r threw an exception') raise ExtensionError( - __('Handler %r for event %r threw an exception') - % (listener.handler, name), + msg % (listener.handler, name), exc, modname=modname, ) from exc @@ -413,9 +460,22 @@ class EventManager: *args: Any, allowed_exceptions: tuple[type[Exception], ...] = (), ) -> Any: - """Emit a Sphinx event and returns first result. + """Emit a Sphinx event and return the first result. - This returns the result of the first handler that doesn't return ``None``. + This emits the *name* event and passes *args* to the handler functions. + The first non-None result is returned. + + :param name: + The name of the event that will be emitted. + :param args: + The arguments for the event, to be passed to the handler functions. + :param allowed_exceptions: + The list of exceptions that are allowed in the handlers. + + .. versionadded:: 0.5 + .. versionchanged:: 3.1 + + Added *allowed_exceptions* to specify path-through exceptions """ for result in self.emit(name, *args, allowed_exceptions=allowed_exceptions): if result is not None: