summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--sphinx/application.py81
-rw-r--r--sphinx/builders/__init__.py8
-rw-r--r--sphinx/environment/__init__.py29
-rw-r--r--sphinx/environment/adapters/indexentries.py4
-rw-r--r--sphinx/extension.py120
-rw-r--r--sphinx/util/__init__.py11
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