diff options
-rw-r--r-- | sphinx/application.py | 81 | ||||
-rw-r--r-- | sphinx/builders/__init__.py | 8 | ||||
-rw-r--r-- | sphinx/environment/__init__.py | 29 | ||||
-rw-r--r-- | sphinx/environment/adapters/indexentries.py | 4 | ||||
-rw-r--r-- | sphinx/extension.py | 120 | ||||
-rw-r--r-- | sphinx/util/__init__.py | 11 |
6 files changed, 155 insertions, 98 deletions
diff --git a/sphinx/application.py b/sphinx/application.py index eeaa954eb..fc8e0aa86 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -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 diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index f7593020a..adb3a3b8a 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -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 diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 014850f7f..45dfae8f5 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -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: diff --git a/sphinx/environment/adapters/indexentries.py b/sphinx/environment/adapters/indexentries.py index 31ec58301..4eb1f8a32 100644 --- a/sphinx/environment/adapters/indexentries.py +++ b/sphinx/environment/adapters/indexentries.py @@ -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 diff --git a/sphinx/extension.py b/sphinx/extension.py new file mode 100644 index 000000000..12e80d1a2 --- /dev/null +++ b/sphinx/extension.py @@ -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)) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index a3823546d..adf526e06 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -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 |