mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #3578 from tk0miya/refactor_extensions
Refactor application class around extensions
This commit is contained in:
commit
d53204aa46
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
120
sphinx/extension.py
Normal 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))
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user