Merge pull request #6260 from tk0miya/refactor_events

Make EventManager portable
This commit is contained in:
Takeshi KOMIYA 2019-04-16 14:04:54 +09:00 committed by GitHub
commit 15bc5a32bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 78 additions and 33 deletions

View File

@ -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:

View File

@ -38,3 +38,8 @@ Builder API
.. automethod:: write_doc
.. automethod:: finish
**Attributes**
.. attribute:: events
An :class:`.EventManager` object.

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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():
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

View File

@ -403,8 +403,8 @@ class Documenter:
retann = self.retann
result = self.env.app.emit_firstresult(
'autodoc-process-signature', self.objtype, self.fullname,
result = self.env.events.emit_firstresult('autodoc-process-signature',
self.objtype, self.fullname,
self.object, self.options, args, retann)
if result:
args, retann = result

View File

@ -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'] = []