mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #6260 from tk0miya/refactor_events
Make EventManager portable
This commit is contained in:
commit
15bc5a32bb
@ -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:
|
||||
|
||||
|
@ -38,3 +38,8 @@ Builder API
|
||||
.. automethod:: write_doc
|
||||
.. automethod:: finish
|
||||
|
||||
**Attributes**
|
||||
|
||||
.. attribute:: events
|
||||
|
||||
An :class:`.EventManager` object.
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'] = []
|
||||
|
Loading…
Reference in New Issue
Block a user