Merge pull request #3578 from tk0miya/refactor_extensions

Refactor application class around extensions
This commit is contained in:
Takeshi KOMIYA 2017-03-26 11:33:42 +09:00 committed by GitHub
commit d53204aa46
6 changed files with 155 additions and 98 deletions

View File

@ -17,7 +17,6 @@ import sys
import types
import warnings
import posixpath
import traceback
from os import path
from collections import deque
@ -38,6 +37,7 @@ from sphinx.domains.std import GenericObject, Target, StandardDomain
from sphinx.deprecation import RemovedInSphinx17Warning, RemovedInSphinx20Warning
from sphinx.environment import BuildEnvironment
from sphinx.events import EventManager
from sphinx.extension import load_extension, verify_required_extensions
from sphinx.io import SphinxStandaloneReader
from sphinx.locale import _
from sphinx.roles import XRefRole
@ -59,6 +59,7 @@ if False:
from sphinx.builders import Builder # NOQA
from sphinx.domains import Domain, Index # NOQA
from sphinx.environment.collectors import EnvironmentCollector # NOQA
from sphinx.extension import Extension # NOQA
builtin_extensions = (
'sphinx.builders.applehelp',
@ -97,15 +98,14 @@ builtin_extensions = (
'sphinx.environment.collectors.title',
'sphinx.environment.collectors.toctree',
'sphinx.environment.collectors.indexentries',
# Strictly, alabaster theme is not a builtin extension,
# but it is loaded automatically to use it as default theme.
'alabaster',
) # type: Tuple[unicode, ...]
CONFIG_FILENAME = 'conf.py'
ENV_PICKLE_FILENAME = 'environment.pickle'
# list of deprecated extensions. Keys are extension name.
# Values are Sphinx version that merge the extension.
EXTENSION_BLACKLIST = {"sphinxjp.themecore": "1.2"} # type: Dict[unicode, unicode]
logger = logging.getLogger(__name__)
@ -117,8 +117,7 @@ class Sphinx(object):
parallel=0):
# type: (unicode, unicode, unicode, unicode, unicode, Dict, IO, IO, bool, bool, List[unicode], int, int) -> None # NOQA
self.verbosity = verbosity
self._extensions = {} # type: Dict[unicode, Any]
self._extension_metadata = {} # type: Dict[unicode, Dict[unicode, Any]]
self.extensions = {} # type: Dict[unicode, Extension]
self._additional_source_parsers = {} # type: Dict[unicode, Parser]
self._setting_up_extension = ['?'] # type: List[unicode]
self.domains = {} # type: Dict[unicode, Type[Domain]]
@ -195,12 +194,6 @@ class Sphinx(object):
for extension in builtin_extensions:
self.setup_extension(extension)
# extension loading support for alabaster theme
# self.config.html_theme is not set from conf.py at here
# for now, sphinx always load a 'alabaster' extension.
if 'alabaster' not in self.config.extensions:
self.config.extensions.append('alabaster')
# load all user-given extension modules
for extension in self.config.extensions:
self.setup_extension(extension)
@ -221,19 +214,7 @@ class Sphinx(object):
self.config.init_values()
# check extension versions if requested
if self.config.needs_extensions:
for extname, needs_ver in self.config.needs_extensions.items():
if extname not in self._extensions:
logger.warning(_('needs_extensions config value specifies a '
'version requirement for extension %s, but it is '
'not loaded'), extname)
continue
has_ver = self._extension_metadata[extname]['version']
if has_ver == 'unknown version' or needs_ver > has_ver:
raise VersionRequirementError(
_('This project needs the extension %s at least in '
'version %s and therefore cannot be built with the '
'loaded version (%s).') % (extname, needs_ver, has_ver))
verify_required_extensions(self, self.config.needs_extensions)
# check primary_domain if requested
if self.config.primary_domain and self.config.primary_domain not in self.domains:
@ -466,53 +447,11 @@ class Sphinx(object):
# ---- general extensibility interface -------------------------------------
def setup_extension(self, extension):
def setup_extension(self, extname):
# type: (unicode) -> None
"""Import and setup a Sphinx extension module. No-op if called twice."""
logger.debug('[app] setting up extension: %r', extension)
if extension in self._extensions:
return
if extension in EXTENSION_BLACKLIST:
logger.warning(_('the extension %r was already merged with Sphinx since '
'version %s; this extension is ignored.'),
extension, EXTENSION_BLACKLIST[extension])
return
self._setting_up_extension.append(extension)
try:
mod = __import__(extension, None, None, ['setup'])
except ImportError as err:
logger.verbose(_('Original exception:\n') + traceback.format_exc())
raise ExtensionError(_('Could not import extension %s') % extension,
err)
if not hasattr(mod, 'setup'):
logger.warning(_('extension %r has no setup() function; is it really '
'a Sphinx extension module?'), extension)
ext_meta = None
else:
try:
ext_meta = mod.setup(self)
except VersionRequirementError as err:
# add the extension name to the version required
raise VersionRequirementError(
_('The %s extension used by this project needs at least '
'Sphinx v%s; it therefore cannot be built with this '
'version.') % (extension, err))
if ext_meta is None:
ext_meta = {}
# special-case for compatibility
if extension == 'rst2pdf.pdfbuilder':
ext_meta = {'parallel_read_safe': True}
try:
if not ext_meta.get('version'):
ext_meta['version'] = 'unknown version'
except Exception:
logger.warning(_('extension %r returned an unsupported object from '
'its setup() function; it should return None or a '
'metadata dictionary'), extension)
ext_meta = {'version': 'unknown version'}
self._extensions[extension] = mod
self._extension_metadata[extension] = ext_meta
self._setting_up_extension.pop()
logger.debug('[app] setting up extension: %r', extname)
load_extension(self, extname)
def require_sphinx(self, version):
# type: (unicode) -> None

View File

@ -17,6 +17,7 @@ try:
except ImportError:
multiprocessing = None
from six import itervalues
from docutils import nodes
from sphinx.util import i18n, path_stabilize, logging, status_iterator
@ -337,11 +338,10 @@ class Builder(object):
self.parallel_ok = False
if parallel_available and self.app.parallel > 1 and self.allow_parallel:
self.parallel_ok = True
for extname, md in self.app._extension_metadata.items():
par_ok = md.get('parallel_write_safe', True)
if not par_ok:
for extension in itervalues(self.app.extensions):
if not extension.parallel_write_safe:
logger.warning('the %s extension is not safe for parallel '
'writing, doing serial write', extname)
'writing, doing serial write', extension.name)
self.parallel_ok = False
break

View File

@ -43,6 +43,7 @@ from sphinx.util.matching import compile_matchers
from sphinx.util.parallel import ParallelTasks, parallel_available, make_chunks
from sphinx.util.websupport import is_commentable
from sphinx.errors import SphinxError, ExtensionError
from sphinx.locale import _
from sphinx.transforms import SphinxTransformer
from sphinx.versioning import add_uids, merge_doctrees
from sphinx.deprecation import RemovedInSphinx17Warning, RemovedInSphinx20Warning
@ -559,22 +560,20 @@ class BuildEnvironment(object):
# check if we should do parallel or serial read
par_ok = False
if parallel_available and len(docnames) > 5 and self.app.parallel > 1:
par_ok = True
for extname, md in self.app._extension_metadata.items():
ext_ok = md.get('parallel_read_safe')
if ext_ok:
continue
if ext_ok is None:
logger.warning('the %s extension does not declare if it '
'is safe for parallel reading, assuming it '
'isn\'t - please ask the extension author to '
'check and make it explicit', extname)
for ext in itervalues(self.app.extensions):
if ext.parallel_read_safe is None:
logger.warning(_('the %s extension does not declare if it is safe '
'for parallel reading, assuming it isn\'t - please '
'ask the extension author to check and make it '
'explicit'), ext.name)
logger.warning('doing serial read')
else:
logger.warning('the %s extension is not safe for parallel '
'reading, doing serial read', extname)
par_ok = False
break
break
elif ext.parallel_read_safe is False:
break
else:
# all extensions support parallel-read
par_ok = True
if par_ok:
self._read_parallel(docnames, self.app, nproc=self.app.parallel)
else:

View File

@ -13,10 +13,10 @@ import bisect
import unicodedata
from itertools import groupby
from six import text_type
from six import text_type, iteritems
from sphinx.locale import _
from sphinx.util import iteritems, split_into, logging
from sphinx.util import split_into, logging
if False:
# For type annotation

120
sphinx/extension.py Normal file
View File

@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
"""
sphinx.extension
~~~~~~~~~~~~~~~~
Utilities for Sphinx extensions.
:copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import traceback
from six import iteritems
from sphinx.errors import ExtensionError, VersionRequirementError
from sphinx.locale import _
from sphinx.util import logging
if False:
# For type annotation
from typing import Any, Dict # NOQA
from sphinx.application import Sphinx # NOQA
logger = logging.getLogger(__name__)
# list of deprecated extensions. Keys are extension name.
# Values are Sphinx version that merge the extension.
EXTENSION_BLACKLIST = {
"sphinxjp.themecore": "1.2"
} # type: Dict[unicode, unicode]
class Extension(object):
def __init__(self, name, module, **kwargs):
self.name = name
self.module = module
self.metadata = kwargs
self.version = kwargs.pop('version', 'unknown version')
# The extension supports parallel read or not. The default value
# is ``None``. It means the extension does not tell the status.
# It will be warned on parallel reading.
self.parallel_read_safe = kwargs.pop('parallel_read_safe', None)
# The extension supports parallel write or not. The default value
# is ``True``. Sphinx writes parallelly documents even if
# the extension does not tell its status.
self.parallel_write_safe = kwargs.pop('parallel_read_safe', True)
def load_extension(app, extname):
# type: (Sphinx, unicode) -> None
"""Load a Sphinx extension."""
if extname in app.extensions: # alread loaded
return
if extname in EXTENSION_BLACKLIST:
logger.warning(_('the extension %r was already merged with Sphinx since '
'version %s; this extension is ignored.'),
extname, EXTENSION_BLACKLIST[extname])
return
# update loading context
app._setting_up_extension.append(extname)
try:
mod = __import__(extname, None, None, ['setup'])
except ImportError as err:
logger.verbose(_('Original exception:\n') + traceback.format_exc())
raise ExtensionError(_('Could not import extension %s') % extname, err)
if not hasattr(mod, 'setup'):
logger.warning(_('extension %r has no setup() function; is it really '
'a Sphinx extension module?'), extname)
metadata = {} # type: Dict[unicode, Any]
else:
try:
metadata = mod.setup(app)
except VersionRequirementError as err:
# add the extension name to the version required
raise VersionRequirementError(
_('The %s extension used by this project needs at least '
'Sphinx v%s; it therefore cannot be built with this '
'version.') % (extname, err)
)
if metadata is None:
metadata = {}
if extname == 'rst2pdf.pdfbuilder':
metadata['parallel_read_safe'] = True
elif not isinstance(metadata, dict):
logger.warning(_('extension %r returned an unsupported object from '
'its setup() function; it should return None or a '
'metadata dictionary'), extname)
app.extensions[extname] = Extension(extname, mod, **metadata)
app._setting_up_extension.pop()
def verify_required_extensions(app, requirements):
# type: (Sphinx, Dict[unicode, unicode]) -> None
"""Verify the required Sphinx extensions are loaded."""
if requirements is None:
return
for extname, reqversion in iteritems(requirements):
extension = app.extensions.get(extname)
if extension is None:
logger.warning(_('needs_extensions config value specifies a '
'version requirement for extension %s, but it is '
'not loaded'), extname)
continue
if extension.version == 'unknown version' or reqversion > extension.version:
raise VersionRequirementError(_('This project needs the extension %s at least in '
'version %s and therefore cannot be built with '
'the loaded version (%s).') %
(extname, reqversion, extension.version))

View File

@ -22,7 +22,7 @@ from os import path
from codecs import BOM_UTF8
from collections import deque
from six import iteritems, text_type, binary_type
from six import text_type, binary_type
from six.moves import range
from six.moves.urllib.parse import urlsplit, urlunsplit, quote_plus, parse_qsl, urlencode
from docutils.utils import relative_path
@ -227,14 +227,13 @@ def save_traceback(app):
jinja2.__version__, # type: ignore
last_msgs)).encode('utf-8'))
if app is not None:
for extname, extmod in iteritems(app._extensions):
modfile = getattr(extmod, '__file__', 'unknown')
for ext in app.extensions:
modfile = getattr(ext.module, '__file__', 'unknown')
if isinstance(modfile, bytes):
modfile = modfile.decode(fs_encoding, 'replace')
version = app._extension_metadata[extname]['version']
if version != 'builtin':
if ext.version != 'builtin':
os.write(fd, ('# %s (%s) from %s\n' %
(extname, version, modfile)).encode('utf-8'))
(ext.name, ext.version, modfile)).encode('utf-8'))
os.write(fd, exc_format.encode('utf-8'))
os.close(fd)
return path