Merge pull request #4773 from tk0miya/refactor_config

Refactor config
This commit is contained in:
Takeshi KOMIYA
2018-04-29 21:02:35 +09:00
committed by GitHub
14 changed files with 311 additions and 242 deletions

View File

@@ -43,6 +43,11 @@ Deprecated
* ``app.override_domain()`` is deprecated * ``app.override_domain()`` is deprecated
* ``app.add_stylesheet()`` is deprecated * ``app.add_stylesheet()`` is deprecated
* ``sphinx.versioning.prepare()`` is deprecated * ``sphinx.versioning.prepare()`` is deprecated
* ``Config.__init__()`` has changed; the *dirname*, *filename* and *tags*
argument has been deprecated
* ``Config.check_types()`` is deprecated
* ``Config.check_unicode()`` is deprecated
* ``sphinx.application.CONFIG_FILENAME`` is deprecated
For more details, see `deprecation APIs list For more details, see `deprecation APIs list
<http://www.sphinx-doc.org/en/master/extdev/index.html#deprecated-apis>`_ <http://www.sphinx-doc.org/en/master/extdev/index.html#deprecated-apis>`_
@@ -74,6 +79,8 @@ Features added
* Improve warning messages during including (refs: #4818) * Improve warning messages during including (refs: #4818)
* LaTeX: separate customizability of :rst:role:`guilabel` and * LaTeX: separate customizability of :rst:role:`guilabel` and
:rst:role:`menuselection` (refs: #4830) :rst:role:`menuselection` (refs: #4830)
* Add ``Config.read()`` classmethod to create a new config object from
configuration file
Bugs fixed Bugs fixed
---------- ----------

View File

@@ -121,6 +121,27 @@ The following is a list of deprecated interface.
- 4.0 - 4.0
- :meth:`~sphinx.application.Sphinx.add_css_file()` - :meth:`~sphinx.application.Sphinx.add_css_file()`
* - ``sphinx.application.CONFIG_FILENAME``
- 1.8
- 3.0
- ``sphinx.config.CONFIG_FILENAME``
* - ``Config.check_unicode()``
- 1.8
- 3.0
- ``sphinx.config.check_unicode()``
* - ``Config.check_types()``
- 1.8
- 3.0
- ``sphinx.config.check_confval_types()``
* - ``dirname``, ``filename`` and ``tags`` arguments of
``Config.__init__()``
- 1.8
- 3.0
- ``Config.read()``
* - The value of :confval:`html_search_options` * - The value of :confval:`html_search_options`
- 1.8 - 1.8
- 3.0 - 3.0

View File

@@ -26,14 +26,13 @@ from six.moves import cStringIO
import sphinx import sphinx
from sphinx import package_dir, locale from sphinx import package_dir, locale
from sphinx.config import Config from sphinx.config import Config, check_unicode
from sphinx.config import CONFIG_FILENAME # NOQA # for compatibility (RemovedInSphinx30)
from sphinx.deprecation import ( from sphinx.deprecation import (
RemovedInSphinx20Warning, RemovedInSphinx30Warning, RemovedInSphinx40Warning RemovedInSphinx20Warning, RemovedInSphinx30Warning, RemovedInSphinx40Warning
) )
from sphinx.environment import BuildEnvironment from sphinx.environment import BuildEnvironment
from sphinx.errors import ( from sphinx.errors import ApplicationError, ConfigError, VersionRequirementError
ApplicationError, ConfigError, ExtensionError, VersionRequirementError
)
from sphinx.events import EventManager from sphinx.events import EventManager
from sphinx.locale import __ from sphinx.locale import __
from sphinx.registry import SphinxComponentRegistry from sphinx.registry import SphinxComponentRegistry
@@ -110,7 +109,6 @@ builtin_extensions = (
'alabaster', 'alabaster',
) # type: Tuple[unicode, ...] ) # type: Tuple[unicode, ...]
CONFIG_FILENAME = 'conf.py'
ENV_PICKLE_FILENAME = 'environment.pickle' ENV_PICKLE_FILENAME = 'environment.pickle'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -189,10 +187,11 @@ class Sphinx(object):
# read config # read config
self.tags = Tags(tags) self.tags = Tags(tags)
self.config = Config(self.confdir, CONFIG_FILENAME, if self.confdir is None:
confoverrides or {}, self.tags) self.config = Config({}, confoverrides or {})
self.config.check_unicode() else:
# defer checking types until i18n has been initialized self.config = Config.read(self.confdir, confoverrides or {}, self.tags)
check_unicode(self.config)
# initialize some limited config variables before initialize i18n and loading # initialize some limited config variables before initialize i18n and loading
# extensions # extensions
@@ -250,8 +249,6 @@ class Sphinx(object):
# create the builder # create the builder
self.builder = self.create_builder(buildername) self.builder = self.create_builder(buildername)
# check all configuration values for permissible types
self.config.check_types()
# set up the build environment # set up the build environment
self._init_env(freshenv) self._init_env(freshenv)
# set up the builder # set up the builder
@@ -561,8 +558,6 @@ class Sphinx(object):
""" """
logger.debug('[app] adding config value: %r', logger.debug('[app] adding config value: %r',
(name, default, rebuild) + ((types,) if types else ())) # type: ignore (name, default, rebuild) + ((types,) if types else ())) # type: ignore
if name in self.config:
raise ExtensionError(__('Config value %r already present') % name)
if rebuild in (False, True): if rebuild in (False, True):
rebuild = rebuild and 'env' or '' rebuild = rebuild and 'env' or ''
self.config.add(name, default, rebuild, types) self.config.add(name, default, rebuild, types)

View File

@@ -323,7 +323,7 @@ def setup(app):
app.add_config_value('latex_appendices', [], None) app.add_config_value('latex_appendices', [], None)
app.add_config_value('latex_use_latex_multicolumn', False, None) app.add_config_value('latex_use_latex_multicolumn', False, None)
app.add_config_value('latex_toplevel_sectioning', None, None, app.add_config_value('latex_toplevel_sectioning', None, None,
ENUM('part', 'chapter', 'section')) ENUM(None, 'part', 'chapter', 'section'))
app.add_config_value('latex_domain_indices', True, None, [list]) app.add_config_value('latex_domain_indices', True, None, [list])
app.add_config_value('latex_show_urls', 'no', None) app.add_config_value('latex_show_urls', 'no', None)
app.add_config_value('latex_show_pagerefs', False, None) app.add_config_value('latex_show_pagerefs', False, None)

View File

@@ -11,13 +11,15 @@
import re import re
import traceback import traceback
import warnings
from collections import OrderedDict from collections import OrderedDict
from os import path, getenv from os import path, getenv
from typing import Any, NamedTuple, Union from typing import Any, NamedTuple, Union
from six import PY2, PY3, iteritems, string_types, binary_type, text_type, integer_types from six import PY2, PY3, iteritems, string_types, binary_type, text_type, integer_types
from sphinx.errors import ConfigError from sphinx.deprecation import RemovedInSphinx30Warning
from sphinx.errors import ConfigError, ExtensionError
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.i18n import format_date from sphinx.util.i18n import format_date
@@ -26,28 +28,15 @@ from sphinx.util.pycompat import execfile_, NoneType
if False: if False:
# For type annotation # For type annotation
from typing import Any, Callable, Dict, Iterable, Iterator, List, Tuple, Union # NOQA from typing import Any, Callable, Dict, Generator, Iterator, List, Tuple, Union # NOQA
from sphinx.application import Sphinx # NOQA from sphinx.application import Sphinx # NOQA
from sphinx.util.tags import Tags # NOQA from sphinx.util.tags import Tags # NOQA
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
nonascii_re = re.compile(br'[\x80-\xff]') CONFIG_FILENAME = 'conf.py'
copyright_year_re = re.compile(r'^((\d{4}-)?)(\d{4})(?=[ ,])') copyright_year_re = re.compile(r'^((\d{4}-)?)(\d{4})(?=[ ,])')
CONFIG_SYNTAX_ERROR = __("There is a syntax error in your configuration file: %s")
if PY3:
CONFIG_SYNTAX_ERROR += __("\nDid you change the syntax from 2.x to 3.x?")
CONFIG_ERROR = __("There is a programable error in your configuration file:\n\n%s")
CONFIG_EXIT_ERROR = __("The configuration file (or one of the modules it imports) "
"called sys.exit()")
CONFIG_ENUM_WARNING = __("The config value `{name}` has to be a one of {candidates}, "
"but `{current}` is given.")
CONFIG_PERMITTED_TYPE_WARNING = __("The config value `{name}' has type `{current.__name__}', "
"expected to {permitted}.")
CONFIG_TYPE_WARNING = __("The config value `{name}' has type `{current.__name__}', "
"defaults to `{default.__name__}'.")
if PY3: if PY3:
unicode = str # special alias for static typing... unicode = str # special alias for static typing...
@@ -155,30 +144,30 @@ class Config(object):
'env'), 'env'),
) # type: Dict[unicode, Tuple] ) # type: Dict[unicode, Tuple]
def __init__(self, dirname, filename, overrides, tags): def __init__(self, *args):
# type: (unicode, unicode, Dict, Tags) -> None # type: (Any) -> None
if len(args) == 4:
# old style arguments: (dirname, filename, overrides, tags)
warnings.warn('The argument of Config() class has been changed. '
'Use Config.read() to read configuration from conf.py.',
RemovedInSphinx30Warning)
dirname, filename, overrides, tags = args
if dirname is None:
config = {} # type: Dict[unicode, Any]
else:
config = eval_config_file(path.join(dirname, filename), tags)
else:
# new style arguments: (config={}, overrides={})
if len(args) == 0:
config, overrides = {}, {}
elif len(args) == 1:
config, overrides = args[0], {}
else:
config, overrides = args[:2]
self.overrides = overrides self.overrides = overrides
self.values = Config.config_values.copy() self.values = Config.config_values.copy()
config = {} # type: Dict[unicode, Any]
if dirname is not None:
config_file = path.join(dirname, filename)
config['__file__'] = config_file
config['tags'] = tags
with cd(dirname):
# we promise to have the config dir as current dir while the
# config file is executed
try:
execfile_(filename, config)
except SyntaxError as err:
raise ConfigError(CONFIG_SYNTAX_ERROR % err)
except SystemExit:
raise ConfigError(CONFIG_EXIT_ERROR)
except Exception:
raise ConfigError(CONFIG_ERROR % traceback.format_exc())
self._raw_config = config self._raw_config = config
# these two must be preinitialized because extensions can add their
# own config values
self.setup = config.get('setup', None) # type: Callable self.setup = config.get('setup', None) # type: Callable
if 'extensions' in overrides: if 'extensions' in overrides:
@@ -188,69 +177,25 @@ class Config(object):
config['extensions'] = overrides.pop('extensions') config['extensions'] = overrides.pop('extensions')
self.extensions = config.get('extensions', []) # type: List[unicode] self.extensions = config.get('extensions', []) # type: List[unicode]
# correct values of copyright year that are not coherent with @classmethod
# the SOURCE_DATE_EPOCH environment variable (if set) def read(cls, confdir, overrides=None, tags=None):
# See https://reproducible-builds.org/specs/source-date-epoch/ # type: (unicode, Dict, Tags) -> Config
if getenv('SOURCE_DATE_EPOCH') is not None: """Create a Config object from configuration file."""
for k in ('copyright', 'epub_copyright'): filename = path.join(confdir, CONFIG_FILENAME)
if k in config: namespace = eval_config_file(filename, tags)
config[k] = copyright_year_re.sub(r'\g<1>%s' % format_date('%Y'), return cls(namespace, overrides or {})
config[k])
def check_types(self): def check_types(self):
# type: () -> None # type: () -> None
# check all values for deviation from the default value's type, since warnings.warn('Config.check_types() is deprecated. Use check_confval_types() instead.',
# that can result in TypeErrors all over the place RemovedInSphinx30Warning)
# NB. since config values might use _() we have to wait with calling check_confval_types(None, self)
# this method until i18n is initialized
for name in self._raw_config:
if name not in self.values:
continue # we don't know a default value
settings = self.values[name]
default, dummy_rebuild = settings[:2]
permitted = settings[2] if len(settings) == 3 else ()
if hasattr(default, '__call__'):
default = default(self) # could invoke _()
if default is None and not permitted:
continue # neither inferrable nor expliclitly permitted types
current = self[name]
if permitted is Any:
# any type of value is accepted
pass
elif isinstance(permitted, ENUM):
if not permitted.match(current):
logger.warning(CONFIG_ENUM_WARNING.format(
name=name, current=current, candidates=permitted.candidates))
else:
if type(current) is type(default):
continue
if type(current) in permitted:
continue
common_bases = (set(type(current).__bases__ + (type(current),)) &
set(type(default).__bases__))
common_bases.discard(object)
if common_bases:
continue # at least we share a non-trivial base class
if permitted:
logger.warning(CONFIG_PERMITTED_TYPE_WARNING.format(
name=name, current=type(current),
permitted=str([cls.__name__ for cls in permitted])))
else:
logger.warning(CONFIG_TYPE_WARNING.format(
name=name, current=type(current), default=type(default)))
def check_unicode(self): def check_unicode(self):
# type: () -> None # type: () -> None
# check all string values for non-ASCII characters in bytestrings, warnings.warn('Config.check_unicode() is deprecated. Use check_unicode() instead.',
# since that can result in UnicodeErrors all over the place RemovedInSphinx30Warning)
for name, value in iteritems(self._raw_config): check_unicode(self)
if isinstance(value, binary_type) and nonascii_re.search(value):
logger.warning(__('the config value %r is set to a string with non-ASCII '
'characters; this can lead to Unicode errors occurring. '
'Please use Unicode strings, e.g. %r.'), name, u'Content')
def convert_overrides(self, name, value): def convert_overrides(self, name, value):
# type: (unicode, Any) -> Any # type: (unicode, Any) -> Any
@@ -346,19 +291,49 @@ class Config(object):
return name in self.values return name in self.values
def __iter__(self): def __iter__(self):
# type: () -> Iterable[ConfigValue] # type: () -> Generator[ConfigValue, None, None]
for name, value in iteritems(self.values): for name, value in iteritems(self.values):
yield ConfigValue(name, getattr(self, name), value[1]) # type: ignore yield ConfigValue(name, getattr(self, name), value[1]) # type: ignore
def add(self, name, default, rebuild, types): def add(self, name, default, rebuild, types):
# type: (unicode, Any, Union[bool, unicode], Any) -> None # type: (unicode, Any, Union[bool, unicode], Any) -> None
self.values[name] = (default, rebuild, types) if name in self.values:
raise ExtensionError(__('Config value %r already present') % name)
else:
self.values[name] = (default, rebuild, types)
def filter(self, rebuild): def filter(self, rebuild):
# type: (Union[unicode, List[unicode]]) -> Iterator[ConfigValue] # type: (Union[unicode, List[unicode]]) -> Iterator[ConfigValue]
if isinstance(rebuild, string_types): if isinstance(rebuild, string_types):
rebuild = [rebuild] rebuild = [rebuild]
return (value for value in self if value.rebuild in rebuild) # type: ignore return (value for value in self if value.rebuild in rebuild)
def eval_config_file(filename, tags):
# type: (unicode, Tags) -> Dict[unicode, Any]
"""Evaluate a config file."""
namespace = {} # type: Dict[unicode, Any]
namespace['__file__'] = filename
namespace['tags'] = tags
with cd(path.dirname(filename)):
# during executing config file, current dir is changed to ``confdir``.
try:
execfile_(filename, namespace)
except SyntaxError as err:
msg = __("There is a syntax error in your configuration file: %s")
if PY3:
msg += __("\nDid you change the syntax from 2.x to 3.x?")
raise ConfigError(msg % err)
except SystemExit:
msg = __("The configuration file (or one of the modules it imports) "
"called sys.exit()")
raise ConfigError(msg)
except Exception:
msg = __("There is a programable error in your configuration file:\n\n%s")
raise ConfigError(msg % traceback.format_exc())
return namespace
def convert_source_suffix(app, config): def convert_source_suffix(app, config):
@@ -400,10 +375,91 @@ def init_numfig_format(app, config):
config.numfig_format = numfig_format # type: ignore config.numfig_format = numfig_format # type: ignore
def correct_copyright_year(app, config):
# type: (Sphinx, Config) -> None
"""correct values of copyright year that are not coherent with
the SOURCE_DATE_EPOCH environment variable (if set)
See https://reproducible-builds.org/specs/source-date-epoch/
"""
if getenv('SOURCE_DATE_EPOCH') is not None:
for k in ('copyright', 'epub_copyright'):
if k in config:
replace = r'\g<1>%s' % format_date('%Y')
config[k] = copyright_year_re.sub(replace, config[k]) # type: ignore
def check_confval_types(app, config):
# type: (Sphinx, Config) -> None
"""check all values for deviation from the default value's type, since
that can result in TypeErrors all over the place NB.
"""
for confval in config:
settings = config.values[confval.name]
default = settings[0]
annotations = settings[2] if len(settings) == 3 else ()
if hasattr(default, '__call__'):
default = default(config) # evaluate default value
if default is None and not annotations:
continue # neither inferrable nor expliclitly annotated types
if annotations is Any:
# any type of value is accepted
pass
elif isinstance(annotations, ENUM):
if not annotations.match(confval.value):
msg = __("The config value `{name}` has to be a one of {candidates}, "
"but `{current}` is given.")
logger.warning(msg.format(name=confval.name,
current=confval.value,
candidates=annotations.candidates))
else:
if type(confval.value) is type(default):
continue
if type(confval.value) in annotations:
continue
common_bases = (set(type(confval.value).__bases__ + (type(confval.value),)) &
set(type(default).__bases__))
common_bases.discard(object)
if common_bases:
continue # at least we share a non-trivial base class
if annotations:
msg = __("The config value `{name}' has type `{current.__name__}', "
"expected to {permitted}.")
logger.warning(msg.format(name=confval.name,
current=type(confval.value),
permitted=str([c.__name__ for c in annotations])))
else:
msg = __("The config value `{name}' has type `{current.__name__}', "
"defaults to `{default.__name__}'.")
logger.warning(msg.format(name=confval.name,
current=type(confval.value),
default=type(default)))
def check_unicode(config):
# type: (Config) -> None
"""check all string values for non-ASCII characters in bytestrings,
since that can result in UnicodeErrors all over the place
"""
nonascii_re = re.compile(br'[\x80-\xff]')
for name, value in iteritems(config._raw_config):
if isinstance(value, binary_type) and nonascii_re.search(value):
logger.warning(__('the config value %r is set to a string with non-ASCII '
'characters; this can lead to Unicode errors occurring. '
'Please use Unicode strings, e.g. %r.'), name, u'Content')
def setup(app): def setup(app):
# type: (Sphinx) -> Dict[unicode, Any] # type: (Sphinx) -> Dict[unicode, Any]
app.connect('config-inited', convert_source_suffix) app.connect('config-inited', convert_source_suffix)
app.connect('config-inited', init_numfig_format) app.connect('config-inited', init_numfig_format)
app.connect('config-inited', correct_copyright_year)
app.connect('config-inited', check_confval_types)
return { return {
'version': 'builtin', 'version': 'builtin',

View File

@@ -20,8 +20,8 @@ from docutils.statemachine import ViewList
from six import iteritems, itervalues, text_type, class_types, string_types from six import iteritems, itervalues, text_type, class_types, string_types
import sphinx import sphinx
from sphinx.application import ExtensionError
from sphinx.deprecation import RemovedInSphinx20Warning from sphinx.deprecation import RemovedInSphinx20Warning
from sphinx.errors import ExtensionError
from sphinx.ext.autodoc.importer import mock, import_object, get_object_members from sphinx.ext.autodoc.importer import mock, import_object, get_object_members
from sphinx.ext.autodoc.importer import _MockImporter # to keep compatibility # NOQA from sphinx.ext.autodoc.importer import _MockImporter # to keep compatibility # NOQA
from sphinx.ext.autodoc.inspector import format_annotation, formatargspec # to keep compatibility # NOQA from sphinx.ext.autodoc.inspector import format_annotation, formatargspec # to keep compatibility # NOQA

View File

@@ -57,7 +57,7 @@ class IfConfig(Directive):
def process_ifconfig_nodes(app, doctree, docname): def process_ifconfig_nodes(app, doctree, docname):
# type: (Sphinx, nodes.Node, unicode) -> None # type: (Sphinx, nodes.Node, unicode) -> None
ns = dict((confval.name, confval.value) for confval in app.config) # type: ignore ns = dict((confval.name, confval.value) for confval in app.config)
ns.update(app.config.__dict__.copy()) ns.update(app.config.__dict__.copy())
ns['builder'] = app.builder.name ns['builder'] = app.builder.name
for node in doctree.traverse(ifconfig): for node in doctree.traverse(ifconfig):

View File

@@ -13,7 +13,7 @@
from docutils import nodes from docutils import nodes
import sphinx import sphinx
from sphinx.application import ExtensionError from sphinx.errors import ExtensionError
from sphinx.ext.mathbase import get_node_equation_number from sphinx.ext.mathbase import get_node_equation_number
from sphinx.ext.mathbase import setup_math as mathbase_setup from sphinx.ext.mathbase import setup_math as mathbase_setup
from sphinx.locale import _ from sphinx.locale import _

View File

@@ -1,54 +1,3 @@
from sphinx.config import string_classes, ENUM project = 'Sphinx <Tests>'
release = '0.6alpha1'
value1 = 123 # wrong type templates_path = ['_templates']
value2 = 123 # lambda with wrong type
value3 = [] # lambda with correct type
value4 = True # child type
value5 = 3 # parent type
value6 = () # other sequence type, also raises
value7 = ['foo'] # explicitly permitted
class A(object):
pass
class B(A):
pass
class C(A):
pass
value8 = C() # sibling type
# both have no default or permissible types
value9 = 'foo'
value10 = 123
value11 = u'bar'
value12 = u'bar'
value13 = 'bar'
value14 = u'bar'
value15 = 'bar'
value16 = u'bar'
def setup(app):
app.add_config_value('value1', 'string', False)
app.add_config_value('value2', lambda conf: [], False)
app.add_config_value('value3', [], False)
app.add_config_value('value4', 100, False)
app.add_config_value('value5', False, False)
app.add_config_value('value6', [], False)
app.add_config_value('value7', 'string', False, [list])
app.add_config_value('value8', B(), False)
app.add_config_value('value9', None, False)
app.add_config_value('value10', None, False)
app.add_config_value('value11', None, False, [str])
app.add_config_value('value12', 'string', False)
app.add_config_value('value13', None, False, string_classes)
app.add_config_value('value14', None, False, string_classes)
app.add_config_value('value15', u'unicode', False)
app.add_config_value('value16', u'unicode', False)
app.add_config_value('value17', 'default', False, ENUM('default', 'one', 'two'))

View File

@@ -12,7 +12,7 @@ from sphinx import addnodes
sys.path.append(os.path.abspath('.')) sys.path.append(os.path.abspath('.'))
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.jsmath', 'sphinx.ext.todo', extensions = ['sphinx.ext.autodoc', 'sphinx.ext.jsmath', 'sphinx.ext.todo',
'sphinx.ext.coverage', 'sphinx.ext.extlinks', 'ext'] 'sphinx.ext.coverage', 'sphinx.ext.extlinks']
jsmath_path = 'dummy.js' jsmath_path = 'dummy.js'
@@ -65,8 +65,6 @@ man_pages = [
'Georg Brandl and someone else', 1), 'Georg Brandl and someone else', 1),
] ]
value_from_conf_py = 84
coverage_c_path = ['special/*.h'] coverage_c_path = ['special/*.h']
coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'} coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'}
@@ -104,7 +102,6 @@ class ClassDirective(Directive):
def setup(app): def setup(app):
import parsermod import parsermod
app.add_config_value('value_from_conf_py', 42, False)
app.add_directive('clsdir', ClassDirective) app.add_directive('clsdir', ClassDirective)
app.add_object_type('userdesc', 'userdescrole', '%s (userdesc)', app.add_object_type('userdesc', 'userdescrole', '%s (userdesc)',
userdesc_parse, objname='user desc') userdesc_parse, objname='user desc')

View File

@@ -1,5 +0,0 @@
# Test extension module
def setup(app):
app.add_config_value('value_from_ext', [], False)

View File

@@ -11,7 +11,7 @@
import pytest import pytest
from docutils import nodes from docutils import nodes
from sphinx.application import ExtensionError from sphinx.errors import ExtensionError
from sphinx.domains import Domain from sphinx.domains import Domain
from sphinx.testing.util import strip_escseq from sphinx.testing.util import strip_escseq
from sphinx.util import logging from sphinx.util import logging

View File

@@ -11,15 +11,15 @@
""" """
import mock import mock
import pytest import pytest
from six import PY3, iteritems from six import PY3
import sphinx import sphinx
from sphinx.config import Config from sphinx.config import Config, ENUM, string_classes, check_confval_types
from sphinx.errors import ExtensionError, ConfigError, VersionRequirementError from sphinx.errors import ExtensionError, ConfigError, VersionRequirementError
from sphinx.testing.path import path from sphinx.testing.path import path
@pytest.mark.sphinx(confoverrides={ @pytest.mark.sphinx(testroot='config', confoverrides={
'master_doc': 'master', 'master_doc': 'master',
'nonexisting_value': 'True', 'nonexisting_value': 'True',
'latex_elements.docclass': 'scrartcl', 'latex_elements.docclass': 'scrartcl',
@@ -74,36 +74,64 @@ def test_core_config(app, status, warning):
assert cfg['project'] == cfg.project == 'Sphinx Tests' assert cfg['project'] == cfg.project == 'Sphinx Tests'
def test_extension_values(app, status, warning): def test_extension_values():
cfg = app.config config = Config()
# default value # check standard settings
assert cfg.value_from_ext == [] assert config.master_doc == 'contents'
# non-default value
assert cfg.value_from_conf_py == 84
# no duplicate values allowed # can't override it by add_config_value()
with pytest.raises(ExtensionError) as excinfo: with pytest.raises(ExtensionError) as excinfo:
app.add_config_value('html_title', 'x', True) config.add('master_doc', 'index', 'env', None)
assert 'already present' in str(excinfo.value) assert 'already present' in str(excinfo.value)
# add a new config value
config.add('value_from_ext', [], 'env', None)
assert config.value_from_ext == []
# can't override it by add_config_value()
with pytest.raises(ExtensionError) as excinfo: with pytest.raises(ExtensionError) as excinfo:
app.add_config_value('value_from_ext', 'x', True) config.add('value_from_ext', [], 'env', None)
assert 'already present' in str(excinfo.value) assert 'already present' in str(excinfo.value)
def test_overrides():
config = Config({'value1': '1', 'value2': 2, 'value6': {'default': 6}},
{'value2': 999, 'value3': '999', 'value5.attr1': 999, 'value6.attr1': 999,
'value7': 'abc,def,ghi', 'value8': 'abc,def,ghi'})
config.add('value1', None, 'env', ())
config.add('value2', None, 'env', ())
config.add('value3', 0, 'env', ())
config.add('value4', 0, 'env', ())
config.add('value5', {'default': 0}, 'env', ())
config.add('value6', {'default': 0}, 'env', ())
config.add('value7', None, 'env', ())
config.add('value8', [], 'env', ())
config.init_values()
assert config.value1 == '1'
assert config.value2 == 999
assert config.value3 == 999
assert config.value4 == 0
assert config.value5 == {'attr1': 999}
assert config.value6 == {'default': 6, 'attr1': 999}
assert config.value7 == 'abc,def,ghi'
assert config.value8 == ['abc', 'def', 'ghi']
@mock.patch("sphinx.config.logger") @mock.patch("sphinx.config.logger")
def test_errors_warnings(logger, tempdir): def test_errors_warnings(logger, tempdir):
# test the error for syntax errors in the config file # test the error for syntax errors in the config file
(tempdir / 'conf.py').write_text(u'project = \n', encoding='ascii') (tempdir / 'conf.py').write_text(u'project = \n', encoding='ascii')
with pytest.raises(ConfigError) as excinfo: with pytest.raises(ConfigError) as excinfo:
Config(tempdir, 'conf.py', {}, None) Config.read(tempdir, {}, None)
assert 'conf.py' in str(excinfo.value) assert 'conf.py' in str(excinfo.value)
# test the automatic conversion of 2.x only code in configs # test the automatic conversion of 2.x only code in configs
(tempdir / 'conf.py').write_text( (tempdir / 'conf.py').write_text(
u'# -*- coding: utf-8\n\nproject = u"Jägermeister"\n', u'# -*- coding: utf-8\n\nproject = u"Jägermeister"\n',
encoding='utf-8') encoding='utf-8')
cfg = Config(tempdir, 'conf.py', {}, None) cfg = Config.read(tempdir, {}, None)
cfg.init_values() cfg.init_values()
assert cfg.project == u'Jägermeister' assert cfg.project == u'Jägermeister'
assert logger.called is False assert logger.called is False
@@ -115,7 +143,7 @@ def test_errors_warnings(logger, tempdir):
return return
(tempdir / 'conf.py').write_text( (tempdir / 'conf.py').write_text(
u'# -*- coding: latin-1\nproject = "fooä"\n', encoding='latin-1') u'# -*- coding: latin-1\nproject = "fooä"\n', encoding='latin-1')
cfg = Config(tempdir, 'conf.py', {}, None) cfg = Config.read(tempdir, {}, None)
assert logger.warning.called is False assert logger.warning.called is False
cfg.check_unicode() cfg.check_unicode()
@@ -174,7 +202,7 @@ def test_config_eol(logger, tempdir):
configfile = tempdir / 'conf.py' configfile = tempdir / 'conf.py'
for eol in (b'\n', b'\r\n'): for eol in (b'\n', b'\r\n'):
configfile.write_bytes(b'project = "spam"' + eol) configfile.write_bytes(b'project = "spam"' + eol)
cfg = Config(tempdir, 'conf.py', {}, None) cfg = Config.read(tempdir, {}, None)
cfg.init_values() cfg.init_values()
assert cfg.project == u'spam' assert cfg.project == u'spam'
assert logger.called is False assert logger.called is False
@@ -195,60 +223,81 @@ def test_builtin_conf(app, status, warning):
'warning') 'warning')
# See roots/test-config/conf.py. # example classes for type checking
TYPECHECK_WARNINGS = { class A(object):
'value1': True, pass
'value2': True,
'value3': False,
'value4': True,
'value5': False,
'value6': True,
'value7': False,
'value8': False,
'value9': False,
'value10': False,
'value11': False if PY3 else True,
'value12': False,
'value13': False,
'value14': False,
'value15': False,
'value16': False,
}
@pytest.mark.parametrize("key,should", iteritems(TYPECHECK_WARNINGS)) class B(A):
@pytest.mark.sphinx(testroot='config') pass
def test_check_types(warning, key, should):
warn = warning.getvalue()
if should:
assert key in warn, (
'override on "%s" should raise a type warning' % key
)
else:
assert key not in warn, (
'override on "%s" should NOT raise a type warning' % key
)
@pytest.mark.sphinx(testroot='config') class C(A):
def test_check_enum(app, status, warning): pass
assert "The config value `value17` has to be a one of ('default', 'one', 'two'), " \
not in warning.getvalue()
@pytest.mark.sphinx(testroot='config', confoverrides={'value17': 'invalid'}) # name, default, annotation, actual, warned
def test_check_enum_failed(app, status, warning): TYPECHECK_WARNINGS = [
assert "The config value `value17` has to be a one of ('default', 'one', 'two'), " \ ('value1', 'string', None, 123, True), # wrong type
"but `invalid` is given." in warning.getvalue() ('value2', lambda _: [], None, 123, True), # lambda with wrong type
('value3', lambda _: [], None, [], False), # lambda with correct type
('value4', 100, None, True, True), # child type
('value5', False, None, True, False), # parent type
('value6', [], None, (), True), # other sequence type
('value7', 'string', [list], ['foo'], False), # explicit type annotation
('value8', B(), None, C(), False), # sibling type
('value9', None, None, 'foo', False), # no default or no annotations
('value10', None, None, 123, False), # no default or no annotations
('value11', None, [str], u'bar', False if PY3 else True), # str vs unicode
('value12', 'string', None, u'bar', False), # str vs unicode
('value13', None, string_classes, 'bar', False), # string_classes
('value14', None, string_classes, u'bar', False), # string_classes
('value15', u'unicode', None, 'bar', False), # str vs unicode
('value16', u'unicode', None, u'bar', False), # str vs unicode
]
@pytest.mark.sphinx(testroot='config', confoverrides={'value17': ['one', 'two']}) @mock.patch("sphinx.config.logger")
def test_check_enum_for_list(app, status, warning): @pytest.mark.parametrize("name,default,annotation,actual,warned", TYPECHECK_WARNINGS)
assert "The config value `value17` has to be a one of ('default', 'one', 'two'), " \ def test_check_types(logger, name, default, annotation, actual, warned):
not in warning.getvalue() config = Config({name: actual})
config.add(name, default, 'env', annotation or ())
config.init_values()
check_confval_types(None, config)
assert logger.warning.called == warned
@pytest.mark.sphinx(testroot='config', confoverrides={'value17': ['one', 'two', 'invalid']}) @mock.patch("sphinx.config.logger")
def test_check_enum_for_list_failed(app, status, warning): def test_check_enum(logger):
assert "The config value `value17` has to be a one of ('default', 'one', 'two'), " \ config = Config()
"but `['one', 'two', 'invalid']` is given." in warning.getvalue() config.add('value', 'default', False, ENUM('default', 'one', 'two'))
config.init_values()
check_confval_types(None, config)
logger.warning.assert_not_called() # not warned
@mock.patch("sphinx.config.logger")
def test_check_enum_failed(logger):
config = Config({'value': 'invalid'})
config.add('value', 'default', False, ENUM('default', 'one', 'two'))
config.init_values()
check_confval_types(None, config)
logger.warning.assert_called()
@mock.patch("sphinx.config.logger")
def test_check_enum_for_list(logger):
config = Config({'value': ['one', 'two']})
config.add('value', 'default', False, ENUM('default', 'one', 'two'))
config.init_values()
check_confval_types(None, config)
logger.warning.assert_not_called() # not warned
@mock.patch("sphinx.config.logger")
def test_check_enum_for_list_failed(logger):
config = Config({'value': ['one', 'two', 'invalid']})
config.add('value', 'default', False, ENUM('default', 'one', 'two'))
config.init_values()
check_confval_types(None, config)
logger.warning.assert_called()

View File

@@ -17,7 +17,7 @@ from sphinx.config import Config
from sphinx.directives.code import LiteralIncludeReader from sphinx.directives.code import LiteralIncludeReader
from sphinx.testing.util import etree_parse from sphinx.testing.util import etree_parse
DUMMY_CONFIG = Config(None, None, {}, '') DUMMY_CONFIG = Config({}, {})
@pytest.fixture(scope='module') @pytest.fixture(scope='module')