diff --git a/CHANGES b/CHANGES index 22638f6aa..0dd0a4f21 100644 --- a/CHANGES +++ b/CHANGES @@ -73,6 +73,8 @@ Features added :rst:dir:`py:exception:` and :rst:dir:`py:method:` directives * #7596: py domain: Change a type annotation for variables to a hyperlink * #7582: napoleon: a type for attribute are represented like type annotation +* #7683: Add ``allowed_exceptions`` parameter to ``Sphinx.emit()`` to allow + handlers to raise specified exceptions Bugs fixed ---------- diff --git a/sphinx/application.py b/sphinx/application.py index 8f06bf9cf..b02fb4d60 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -436,22 +436,32 @@ class Sphinx: logger.debug('[app] disconnecting event: [id=%s]', listener_id) self.events.disconnect(listener_id) - def emit(self, event: str, *args: Any) -> List: + def emit(self, event: str, *args: Any, + allowed_exceptions: Tuple["Type[Exception]", ...] = ()) -> List: """Emit *event* and pass *arguments* to the callback functions. Return the return values of all callbacks as a list. Do not emit core Sphinx events in extensions! - """ - return self.events.emit(event, *args) - def emit_firstresult(self, event: str, *args: Any) -> Any: + .. versionchanged:: 3.1 + + Added *allowed_exceptions* to specify path-through exceptions + """ + return self.events.emit(event, *args, allowed_exceptions=allowed_exceptions) + + def emit_firstresult(self, event: str, *args: Any, + allowed_exceptions: Tuple["Type[Exception]", ...] = ()) -> Any: """Emit *event* and pass *arguments* to the callback functions. Return the result of the first callback that doesn't return ``None``. .. versionadded:: 0.5 + .. versionchanged:: 3.1 + + Added *allowed_exceptions* to specify path-through exceptions """ - return self.events.emit_firstresult(event, *args) + return self.events.emit_firstresult(event, *args, + allowed_exceptions=allowed_exceptions) # registering addon parts diff --git a/sphinx/events.py b/sphinx/events.py index 0f0f47b65..0911dfaaa 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -13,7 +13,7 @@ import warnings from collections import defaultdict from operator import attrgetter -from typing import Any, Callable, Dict, List, NamedTuple +from typing import Any, Callable, Dict, List, NamedTuple, Tuple from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import ExtensionError, SphinxError @@ -22,6 +22,7 @@ from sphinx.util import logging if False: # For type annotation + from typing import Type # for python3.5.1 from sphinx.application import Sphinx @@ -88,7 +89,8 @@ class EventManager: if listener.id == listener_id: listeners.remove(listener) - def emit(self, name: str, *args: Any) -> List: + def emit(self, name: str, *args: Any, + allowed_exceptions: Tuple["Type[Exception]", ...] = ()) -> List: """Emit a Sphinx event.""" try: logger.debug('[app] emitting event: %r%s', name, repr(args)[:100]) @@ -106,6 +108,9 @@ class EventManager: results.append(listener.handler(*args)) else: results.append(listener.handler(self.app, *args)) + except allowed_exceptions: + # pass through the errors specified as *allowed_exceptions* + raise except SphinxError: raise except Exception as exc: @@ -113,12 +118,13 @@ class EventManager: (listener.handler, name)) from exc return results - def emit_firstresult(self, name: str, *args: Any) -> Any: + def emit_firstresult(self, name: str, *args: Any, + allowed_exceptions: Tuple["Type[Exception]", ...] = ()) -> Any: """Emit a Sphinx event and returns first result. This returns the result of the first handler that doesn't return ``None``. """ - for result in self.emit(name, *args): + for result in self.emit(name, *args, allowed_exceptions=allowed_exceptions): if result is not None: return result return None diff --git a/tests/test_events.py b/tests/test_events.py index 4881588a4..4fbe03a17 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -8,6 +8,9 @@ :license: BSD, see LICENSE for details. """ +import pytest + +from sphinx.errors import ExtensionError from sphinx.events import EventManager @@ -22,3 +25,19 @@ def test_event_priority(): events.emit('builder-inited') assert result == [3, 1, 2, 5, 4] + + +def test_event_allowed_exceptions(): + def raise_error(app): + raise RuntimeError + + events = EventManager(object()) # pass an dummy object as an app + events.connect('builder-inited', raise_error, priority=500) + + # all errors are conveted to ExtensionError + with pytest.raises(ExtensionError): + events.emit('builder-inited') + + # Allow RuntimeError (pass-through) + with pytest.raises(RuntimeError): + events.emit('builder-inited', allowed_exceptions=(RuntimeError,))