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 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 function (note that ``conf.py`` can also have a ``setup`` function) to connect
handlers to the events. Example: handlers to the events. Example:

View File

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

View File

@ -27,6 +27,10 @@ Build environment API
Directory for storing pickled doctrees. Directory for storing pickled doctrees.
.. attribute:: events
An :class:`.EventManager` object.
.. attribute:: found_docs .. attribute:: found_docs
A set of all existing docnames. 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 .. autoclass:: sphinx.transforms.post_transforms.images.ImageConverter
:members: :members:
Utility components
------------------
.. autoclass:: sphinx.events.EventManager
:members:

View File

@ -182,7 +182,7 @@ class Sphinx:
self.warningiserror = warningiserror self.warningiserror = warningiserror
logging.setup(self, self._status, self._warning) logging.setup(self, self._status, self._warning)
self.events = EventManager() self.events = EventManager(self)
# keep last few messages for traceback # keep last few messages for traceback
# This will be filled by sphinx.util.logging.LastMessagesWriter # 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 # now that we know all config values, collect them from conf.py
self.config.init_values() self.config.init_values()
self.emit('config-inited', self.config) self.events.emit('config-inited', self.config)
# create the project # create the project
self.project = Project(self.srcdir, self.config.source_suffix) self.project = Project(self.srcdir, self.config.source_suffix)
@ -319,7 +319,7 @@ class Sphinx:
# type: () -> None # type: () -> None
self.builder.set_environment(self.env) self.builder.set_environment(self.env)
self.builder.init() self.builder.init()
self.emit('builder-inited') self.events.emit('builder-inited')
# ---- main "build" method ------------------------------------------------- # ---- main "build" method -------------------------------------------------
@ -360,10 +360,10 @@ class Sphinx:
envfile = path.join(self.doctreedir, ENV_PICKLE_FILENAME) envfile = path.join(self.doctreedir, ENV_PICKLE_FILENAME)
if path.isfile(envfile): if path.isfile(envfile):
os.unlink(envfile) os.unlink(envfile)
self.emit('build-finished', err) self.events.emit('build-finished', err)
raise raise
else: else:
self.emit('build-finished', None) self.events.emit('build-finished', None)
self.builder.cleanup() self.builder.cleanup()
# ---- general extensibility interface ------------------------------------- # ---- general extensibility interface -------------------------------------
@ -420,13 +420,7 @@ class Sphinx:
Return the return values of all callbacks as a list. Do not emit core Return the return values of all callbacks as a list. Do not emit core
Sphinx events in extensions! Sphinx events in extensions!
""" """
try: return self.events.emit(event, *args)
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)
def emit_firstresult(self, event, *args): def emit_firstresult(self, event, *args):
# type: (str, Any) -> Any # type: (str, Any) -> Any
@ -436,7 +430,7 @@ class Sphinx:
.. versionadded:: 0.5 .. versionadded:: 0.5
""" """
return self.events.emit_firstresult(event, self, *args) return self.events.emit_firstresult(event, *args)
# registering addon parts # registering addon parts

View File

@ -43,6 +43,7 @@ if False:
from sphinx.application import Sphinx # NOQA from sphinx.application import Sphinx # NOQA
from sphinx.config import Config # NOQA from sphinx.config import Config # NOQA
from sphinx.environment import BuildEnvironment # NOQA from sphinx.environment import BuildEnvironment # NOQA
from sphinx.events import EventManager # NOQA
from sphinx.util.i18n import CatalogInfo # NOQA from sphinx.util.i18n import CatalogInfo # NOQA
from sphinx.util.tags import Tags # NOQA from sphinx.util.tags import Tags # NOQA
@ -93,6 +94,7 @@ class Builder:
self.app = app # type: Sphinx self.app = app # type: Sphinx
self.env = None # type: BuildEnvironment self.env = None # type: BuildEnvironment
self.events = app.events # type: EventManager
self.config = app.config # type: Config self.config = app.config # type: Config
self.tags = app.tags # type: Tags self.tags = app.tags # type: Tags
self.tags.add(self.format) self.tags.add(self.format)
@ -399,7 +401,7 @@ class Builder:
added, changed, removed = self.env.get_outdated_files(updated) added, changed, removed = self.env.get_outdated_files(updated)
# allow user intervention as well # 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) changed.update(set(docs) & self.env.found_docs)
# if files were added or removed, all documents with globbed toctrees # if files were added or removed, all documents with globbed toctrees
@ -416,13 +418,13 @@ class Builder:
# clear all files no longer present # clear all files no longer present
for docname in removed: 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) self.env.clear_doc(docname)
# read all new and changed files # read all new and changed files
docnames = sorted(added | changed) docnames = sorted(added | changed)
# allow changing and reordering the list of docs to read # 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 # check if we should do parallel or serial read
if parallel_available and len(docnames) > 5 and self.app.parallel > 1: 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' % raise SphinxError('master file %s not found' %
self.env.doc2path(self.config.master_doc)) 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: if retval is not None:
docnames.extend(retval) docnames.extend(retval)
@ -453,7 +455,7 @@ class Builder:
for docname in status_iterator(docnames, __('reading sources... '), "purple", for docname in status_iterator(docnames, __('reading sources... '), "purple",
len(docnames), self.app.verbosity): len(docnames), self.app.verbosity):
# remove all inventory entries for that file # 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.env.clear_doc(docname)
self.read_doc(docname) self.read_doc(docname)
@ -461,7 +463,7 @@ class Builder:
# type: (List[str], int) -> None # type: (List[str], int) -> None
# clear all outdated docs at once # clear all outdated docs at once
for docname in docnames: 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) self.env.clear_doc(docname)
def read_process(docs): def read_process(docs):

View File

@ -653,7 +653,7 @@ class StandaloneHTMLBuilder(Builder):
def gen_additional_pages(self): def gen_additional_pages(self):
# type: () -> None # type: () -> None
# pages from extensions # 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: for pagename, context, template in pagelist:
self.handle_page(pagename, context, template) self.handle_page(pagename, context, template)

View File

@ -34,6 +34,7 @@ if False:
from sphinx.application import Sphinx # NOQA from sphinx.application import Sphinx # NOQA
from sphinx.builders import Builder # NOQA from sphinx.builders import Builder # NOQA
from sphinx.config import Config # NOQA from sphinx.config import Config # NOQA
from sphinx.event import EventManager # NOQA
from sphinx.domains import Domain # NOQA from sphinx.domains import Domain # NOQA
from sphinx.project import Project # NOQA from sphinx.project import Project # NOQA
@ -95,6 +96,7 @@ class BuildEnvironment:
self.srcdir = None # type: str self.srcdir = None # type: str
self.config = None # type: Config self.config = None # type: Config
self.config_status = None # type: int self.config_status = None # type: int
self.events = None # type: EventManager
self.project = None # type: Project self.project = None # type: Project
self.version = None # type: Dict[str, str] self.version = None # type: Dict[str, str]
@ -190,7 +192,7 @@ class BuildEnvironment:
# type: () -> Dict # type: () -> Dict
"""Obtains serializable data for pickling.""" """Obtains serializable data for pickling."""
__dict__ = self.__dict__.copy() __dict__ = self.__dict__.copy()
__dict__.update(app=None, domains={}) # clear unpickable attributes __dict__.update(app=None, domains={}, events=None) # clear unpickable attributes
return __dict__ return __dict__
def __setstate__(self, state): def __setstate__(self, state):
@ -210,6 +212,7 @@ class BuildEnvironment:
self.app = app self.app = app
self.doctreedir = app.doctreedir self.doctreedir = app.doctreedir
self.events = app.events
self.srcdir = app.srcdir self.srcdir = app.srcdir
self.project = app.project self.project = app.project
self.version = app.registry.get_envversion(app) self.version = app.registry.get_envversion(app)
@ -307,7 +310,7 @@ class BuildEnvironment:
for domainname, domain in self.domains.items(): for domainname, domain in self.domains.items():
domain.merge_domaindata(docnames, other.domaindata[domainname]) 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): def path2doc(self, filename):
# type: (str) -> Optional[str] # type: (str) -> Optional[str]
@ -449,7 +452,7 @@ class BuildEnvironment:
def check_dependents(self, app, already): def check_dependents(self, app, already):
# type: (Sphinx, Set[str]) -> Iterator[str] # type: (Sphinx, Set[str]) -> Iterator[str]
to_rewrite = [] # type: List[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) to_rewrite.extend(docnames)
for docname in set(to_rewrite): for docname in set(to_rewrite):
if docname not in already: if docname not in already:
@ -597,7 +600,7 @@ class BuildEnvironment:
self.temp_data = backup self.temp_data = backup
# allow custom references to be resolved # allow custom references to be resolved
self.app.emit('doctree-resolved', doctree, docname) self.events.emit('doctree-resolved', doctree, docname)
def collect_relations(self): def collect_relations(self):
# type: () -> Dict[str, List[str]] # type: () -> Dict[str, List[str]]
@ -653,4 +656,4 @@ class BuildEnvironment:
# call check-consistency for all extensions # call check-consistency for all extensions
for domain in self.domains.values(): for domain in self.domains.values():
domain.check_consistency() 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. :license: BSD, see LICENSE for details.
""" """
import warnings
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.errors import ExtensionError from sphinx.errors import ExtensionError
from sphinx.locale import __ from sphinx.locale import __
from sphinx.util import logging
if False: if False:
# For type annotation # For type annotation
from typing import Any, Callable, Dict, List # NOQA 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. # List of all known core events. Maps name to arguments description.
@ -42,20 +48,28 @@ core_events = {
class EventManager: class EventManager:
def __init__(self): """Event manager for Sphinx."""
# type: () -> None
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.events = core_events.copy()
self.listeners = defaultdict(OrderedDict) # type: Dict[str, Dict[int, Callable]] self.listeners = defaultdict(OrderedDict) # type: Dict[str, Dict[int, Callable]]
self.next_listener_id = 0 self.next_listener_id = 0
def add(self, name): def add(self, name):
# type: (str) -> None # type: (str) -> None
"""Register a custom Sphinx event."""
if name in self.events: if name in self.events:
raise ExtensionError(__('Event %r already present') % name) raise ExtensionError(__('Event %r already present') % name)
self.events[name] = '' self.events[name] = ''
def connect(self, name, callback): def connect(self, name, callback):
# type: (str, Callable) -> int # type: (str, Callable) -> int
"""Connect a handler to specific event."""
if name not in self.events: if name not in self.events:
raise ExtensionError(__('Unknown event name: %s') % name) raise ExtensionError(__('Unknown event name: %s') % name)
@ -66,18 +80,35 @@ class EventManager:
def disconnect(self, listener_id): def disconnect(self, listener_id):
# type: (int) -> None # type: (int) -> None
"""Disconnect a handler."""
for event in self.listeners.values(): for event in self.listeners.values():
event.pop(listener_id, None) event.pop(listener_id, None)
def emit(self, name, *args): def emit(self, name, *args):
# type: (str, Any) -> List # 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 = [] results = []
for callback in self.listeners[name].values(): 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 return results
def emit_firstresult(self, name, *args): def emit_firstresult(self, name, *args):
# type: (str, Any) -> Any # 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): for result in self.emit(name, *args):
if result is not None: if result is not None:
return result return result

View File

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

View File

@ -86,7 +86,7 @@ def process_todos(app, doctree):
if not hasattr(env, 'todo_all_todos'): if not hasattr(env, 'todo_all_todos'):
env.todo_all_todos = [] # type: ignore env.todo_all_todos = [] # type: ignore
for node in doctree.traverse(todo_node): for node in doctree.traverse(todo_node):
app.emit('todo-defined', node) app.events.emit('todo-defined', node)
newnode = node.deepcopy() newnode = node.deepcopy()
newnode['ids'] = [] newnode['ids'] = []