diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index 4cb8501be..18eea34e7 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -145,7 +145,7 @@ Sphinx core events ------------------ These events are known to the core. The arguments shown are given to the -registered event handlers. Use :meth:`.connect` in an extension's ``setup`` +registered event handlers. Use :meth:`.Sphinx.connect` in an extension's ``setup`` function (note that ``conf.py`` can also have a ``setup`` function) to connect handlers to the events. Example: diff --git a/doc/extdev/builderapi.rst b/doc/extdev/builderapi.rst index 2c2cf12e3..0ab7a30f4 100644 --- a/doc/extdev/builderapi.rst +++ b/doc/extdev/builderapi.rst @@ -38,3 +38,8 @@ Builder API .. automethod:: write_doc .. automethod:: finish + **Attributes** + + .. attribute:: events + + An :class:`.EventManager` object. diff --git a/doc/extdev/envapi.rst b/doc/extdev/envapi.rst index 1dee6a576..d7ec23925 100644 --- a/doc/extdev/envapi.rst +++ b/doc/extdev/envapi.rst @@ -27,6 +27,10 @@ Build environment API Directory for storing pickled doctrees. + .. attribute:: events + + An :class:`.EventManager` object. + .. attribute:: found_docs A set of all existing docnames. diff --git a/doc/extdev/utils.rst b/doc/extdev/utils.rst index 2a94a34bb..e842f3032 100644 --- a/doc/extdev/utils.rst +++ b/doc/extdev/utils.rst @@ -29,3 +29,9 @@ components (e.g. :class:`.Config`, :class:`.BuildEnvironment` and so on) easily. .. autoclass:: sphinx.transforms.post_transforms.images.ImageConverter :members: + +Utility components +------------------ + +.. autoclass:: sphinx.events.EventManager + :members: diff --git a/sphinx/application.py b/sphinx/application.py index afcdc02ed..e9b950c83 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -182,7 +182,7 @@ class Sphinx: self.warningiserror = warningiserror logging.setup(self, self._status, self._warning) - self.events = EventManager() + self.events = EventManager(self) # keep last few messages for traceback # This will be filled by sphinx.util.logging.LastMessagesWriter @@ -249,7 +249,7 @@ class Sphinx: # now that we know all config values, collect them from conf.py self.config.init_values() - self.emit('config-inited', self.config) + self.events.emit('config-inited', self.config) # create the project self.project = Project(self.srcdir, self.config.source_suffix) @@ -319,7 +319,7 @@ class Sphinx: # type: () -> None self.builder.set_environment(self.env) self.builder.init() - self.emit('builder-inited') + self.events.emit('builder-inited') # ---- main "build" method ------------------------------------------------- @@ -360,10 +360,10 @@ class Sphinx: envfile = path.join(self.doctreedir, ENV_PICKLE_FILENAME) if path.isfile(envfile): os.unlink(envfile) - self.emit('build-finished', err) + self.events.emit('build-finished', err) raise else: - self.emit('build-finished', None) + self.events.emit('build-finished', None) self.builder.cleanup() # ---- general extensibility interface ------------------------------------- @@ -420,13 +420,7 @@ class Sphinx: Return the return values of all callbacks as a list. Do not emit core Sphinx events in extensions! """ - try: - logger.debug('[app] emitting event: %r%s', event, repr(args)[:100]) - except Exception: - # not every object likes to be repr()'d (think - # random stuff coming via autodoc) - pass - return self.events.emit(event, self, *args) + return self.events.emit(event, *args) def emit_firstresult(self, event, *args): # type: (str, Any) -> Any @@ -436,7 +430,7 @@ class Sphinx: .. versionadded:: 0.5 """ - return self.events.emit_firstresult(event, self, *args) + return self.events.emit_firstresult(event, *args) # registering addon parts diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 1b29fa983..8eaa0e215 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -43,6 +43,7 @@ if False: from sphinx.application import Sphinx # NOQA from sphinx.config import Config # NOQA from sphinx.environment import BuildEnvironment # NOQA + from sphinx.events import EventManager # NOQA from sphinx.util.i18n import CatalogInfo # NOQA from sphinx.util.tags import Tags # NOQA @@ -93,6 +94,7 @@ class Builder: self.app = app # type: Sphinx self.env = None # type: BuildEnvironment + self.events = app.events # type: EventManager self.config = app.config # type: Config self.tags = app.tags # type: Tags self.tags.add(self.format) @@ -399,7 +401,7 @@ class Builder: added, changed, removed = self.env.get_outdated_files(updated) # allow user intervention as well - for docs in self.app.emit('env-get-outdated', self, added, changed, removed): + for docs in self.events.emit('env-get-outdated', self, added, changed, removed): changed.update(set(docs) & self.env.found_docs) # if files were added or removed, all documents with globbed toctrees @@ -416,13 +418,13 @@ class Builder: # clear all files no longer present for docname in removed: - self.app.emit('env-purge-doc', self.env, docname) + self.events.emit('env-purge-doc', self.env, docname) self.env.clear_doc(docname) # read all new and changed files docnames = sorted(added | changed) # allow changing and reordering the list of docs to read - self.app.emit('env-before-read-docs', self.env, docnames) + self.events.emit('env-before-read-docs', self.env, docnames) # check if we should do parallel or serial read if parallel_available and len(docnames) > 5 and self.app.parallel > 1: @@ -439,7 +441,7 @@ class Builder: raise SphinxError('master file %s not found' % self.env.doc2path(self.config.master_doc)) - for retval in self.app.emit('env-updated', self.env): + for retval in self.events.emit('env-updated', self.env): if retval is not None: docnames.extend(retval) @@ -453,7 +455,7 @@ class Builder: for docname in status_iterator(docnames, __('reading sources... '), "purple", len(docnames), self.app.verbosity): # remove all inventory entries for that file - self.app.emit('env-purge-doc', self.env, docname) + self.events.emit('env-purge-doc', self.env, docname) self.env.clear_doc(docname) self.read_doc(docname) @@ -461,7 +463,7 @@ class Builder: # type: (List[str], int) -> None # clear all outdated docs at once for docname in docnames: - self.app.emit('env-purge-doc', self.env, docname) + self.events.emit('env-purge-doc', self.env, docname) self.env.clear_doc(docname) def read_process(docs): diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 97774c668..81c64d445 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -653,7 +653,7 @@ class StandaloneHTMLBuilder(Builder): def gen_additional_pages(self): # type: () -> None # pages from extensions - for pagelist in self.app.emit('html-collect-pages'): + for pagelist in self.events.emit('html-collect-pages'): for pagename, context, template in pagelist: self.handle_page(pagename, context, template) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index f931b3b13..a5adcbb74 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -34,6 +34,7 @@ if False: from sphinx.application import Sphinx # NOQA from sphinx.builders import Builder # NOQA from sphinx.config import Config # NOQA + from sphinx.event import EventManager # NOQA from sphinx.domains import Domain # NOQA from sphinx.project import Project # NOQA @@ -95,6 +96,7 @@ class BuildEnvironment: self.srcdir = None # type: str self.config = None # type: Config self.config_status = None # type: int + self.events = None # type: EventManager self.project = None # type: Project self.version = None # type: Dict[str, str] @@ -190,7 +192,7 @@ class BuildEnvironment: # type: () -> Dict """Obtains serializable data for pickling.""" __dict__ = self.__dict__.copy() - __dict__.update(app=None, domains={}) # clear unpickable attributes + __dict__.update(app=None, domains={}, events=None) # clear unpickable attributes return __dict__ def __setstate__(self, state): @@ -210,6 +212,7 @@ class BuildEnvironment: self.app = app self.doctreedir = app.doctreedir + self.events = app.events self.srcdir = app.srcdir self.project = app.project self.version = app.registry.get_envversion(app) @@ -307,7 +310,7 @@ class BuildEnvironment: for domainname, domain in self.domains.items(): domain.merge_domaindata(docnames, other.domaindata[domainname]) - app.emit('env-merge-info', self, docnames, other) + self.events.emit('env-merge-info', self, docnames, other) def path2doc(self, filename): # type: (str) -> Optional[str] @@ -449,7 +452,7 @@ class BuildEnvironment: def check_dependents(self, app, already): # type: (Sphinx, Set[str]) -> Iterator[str] to_rewrite = [] # type: List[str] - for docnames in app.emit('env-get-updated', self): + for docnames in self.events.emit('env-get-updated', self): to_rewrite.extend(docnames) for docname in set(to_rewrite): if docname not in already: @@ -597,7 +600,7 @@ class BuildEnvironment: self.temp_data = backup # allow custom references to be resolved - self.app.emit('doctree-resolved', doctree, docname) + self.events.emit('doctree-resolved', doctree, docname) def collect_relations(self): # type: () -> Dict[str, List[str]] @@ -653,4 +656,4 @@ class BuildEnvironment: # call check-consistency for all extensions for domain in self.domains.values(): domain.check_consistency() - self.app.emit('env-check-consistency', self) + self.events.emit('env-check-consistency', self) diff --git a/sphinx/events.py b/sphinx/events.py index 25a378d7c..df72f8f21 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -10,14 +10,20 @@ :license: BSD, see LICENSE for details. """ +import warnings from collections import OrderedDict, defaultdict +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import ExtensionError from sphinx.locale import __ +from sphinx.util import logging if False: # For type annotation from typing import Any, Callable, Dict, List # NOQA + from sphinx.application import Sphinx # NOQA + +logger = logging.getLogger(__name__) # List of all known core events. Maps name to arguments description. @@ -42,20 +48,28 @@ core_events = { class EventManager: - def __init__(self): - # type: () -> None + """Event manager for Sphinx.""" + + def __init__(self, app=None): + # type: (Sphinx) -> None + if app is None: + warnings.warn('app argument is required for EventManager.', + RemovedInSphinx40Warning) + self.app = app self.events = core_events.copy() self.listeners = defaultdict(OrderedDict) # type: Dict[str, Dict[int, Callable]] self.next_listener_id = 0 def add(self, name): # type: (str) -> None + """Register a custom Sphinx event.""" if name in self.events: raise ExtensionError(__('Event %r already present') % name) self.events[name] = '' def connect(self, name, callback): # type: (str, Callable) -> int + """Connect a handler to specific event.""" if name not in self.events: raise ExtensionError(__('Unknown event name: %s') % name) @@ -66,18 +80,35 @@ class EventManager: def disconnect(self, listener_id): # type: (int) -> None + """Disconnect a handler.""" for event in self.listeners.values(): event.pop(listener_id, None) def emit(self, name, *args): # type: (str, Any) -> List + """Emit a Sphinx event.""" + try: + logger.debug('[app] emitting event: %r%s', name, repr(args)[:100]) + except Exception: + # not every object likes to be repr()'d (think + # random stuff coming via autodoc) + pass + results = [] for callback in self.listeners[name].values(): - results.append(callback(*args)) + if self.app is None: + # for compatibility; RemovedInSphinx40Warning + results.append(callback(*args)) + else: + results.append(callback(self.app, *args)) return results def emit_firstresult(self, name, *args): # type: (str, Any) -> 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): if result is not None: return result diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index b3fb52538..aa2f2276f 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -403,9 +403,9 @@ class Documenter: retann = self.retann - result = self.env.app.emit_firstresult( - 'autodoc-process-signature', self.objtype, self.fullname, - self.object, self.options, args, retann) + result = self.env.events.emit_firstresult('autodoc-process-signature', + self.objtype, self.fullname, + self.object, self.options, args, retann) if result: args, retann = result diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index 1922bb49c..f43520036 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -86,7 +86,7 @@ def process_todos(app, doctree): if not hasattr(env, 'todo_all_todos'): env.todo_all_todos = [] # type: ignore for node in doctree.traverse(todo_node): - app.emit('todo-defined', node) + app.events.emit('todo-defined', node) newnode = node.deepcopy() newnode['ids'] = []