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
commit a1d78b330b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 311 additions and 242 deletions

View File

@ -43,6 +43,11 @@ Deprecated
* ``app.override_domain()`` is deprecated
* ``app.add_stylesheet()`` 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
<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)
* LaTeX: separate customizability of :rst:role:`guilabel` and
:rst:role:`menuselection` (refs: #4830)
* Add ``Config.read()`` classmethod to create a new config object from
configuration file
Bugs fixed
----------

View File

@ -121,6 +121,27 @@ The following is a list of deprecated interface.
- 4.0
- :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`
- 1.8
- 3.0

View File

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

View File

@ -11,13 +11,15 @@
import re
import traceback
import warnings
from collections import OrderedDict
from os import path, getenv
from typing import Any, NamedTuple, Union
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.util import logging
from sphinx.util.i18n import format_date
@ -26,28 +28,15 @@ from sphinx.util.pycompat import execfile_, NoneType
if False:
# 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.util.tags import Tags # NOQA
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})(?=[ ,])')
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:
unicode = str # special alias for static typing...
@ -155,30 +144,30 @@ class Config(object):
'env'),
) # type: Dict[unicode, Tuple]
def __init__(self, dirname, filename, overrides, tags):
# type: (unicode, unicode, Dict, Tags) -> None
def __init__(self, *args):
# 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.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
# these two must be preinitialized because extensions can add their
# own config values
self.setup = config.get('setup', None) # type: Callable
if 'extensions' in overrides:
@ -188,69 +177,25 @@ class Config(object):
config['extensions'] = overrides.pop('extensions')
self.extensions = config.get('extensions', []) # type: List[unicode]
# 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:
config[k] = copyright_year_re.sub(r'\g<1>%s' % format_date('%Y'),
config[k])
@classmethod
def read(cls, confdir, overrides=None, tags=None):
# type: (unicode, Dict, Tags) -> Config
"""Create a Config object from configuration file."""
filename = path.join(confdir, CONFIG_FILENAME)
namespace = eval_config_file(filename, tags)
return cls(namespace, overrides or {})
def check_types(self):
# type: () -> None
# check all values for deviation from the default value's type, since
# that can result in TypeErrors all over the place
# NB. since config values might use _() we have to wait with calling
# 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)))
warnings.warn('Config.check_types() is deprecated. Use check_confval_types() instead.',
RemovedInSphinx30Warning)
check_confval_types(None, self)
def check_unicode(self):
# type: () -> None
# check all string values for non-ASCII characters in bytestrings,
# since that can result in UnicodeErrors all over the place
for name, value in iteritems(self._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')
warnings.warn('Config.check_unicode() is deprecated. Use check_unicode() instead.',
RemovedInSphinx30Warning)
check_unicode(self)
def convert_overrides(self, name, value):
# type: (unicode, Any) -> Any
@ -346,19 +291,49 @@ class Config(object):
return name in self.values
def __iter__(self):
# type: () -> Iterable[ConfigValue]
# type: () -> Generator[ConfigValue, None, None]
for name, value in iteritems(self.values):
yield ConfigValue(name, getattr(self, name), value[1]) # type: ignore
def add(self, name, default, rebuild, types):
# 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):
# type: (Union[unicode, List[unicode]]) -> Iterator[ConfigValue]
if isinstance(rebuild, string_types):
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):
@ -400,10 +375,91 @@ def init_numfig_format(app, config):
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):
# type: (Sphinx) -> Dict[unicode, Any]
app.connect('config-inited', convert_source_suffix)
app.connect('config-inited', init_numfig_format)
app.connect('config-inited', correct_copyright_year)
app.connect('config-inited', check_confval_types)
return {
'version': 'builtin',

View File

@ -20,8 +20,8 @@ from docutils.statemachine import ViewList
from six import iteritems, itervalues, text_type, class_types, string_types
import sphinx
from sphinx.application import ExtensionError
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 _MockImporter # 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):
# 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['builder'] = app.builder.name
for node in doctree.traverse(ifconfig):

View File

@ -13,7 +13,7 @@
from docutils import nodes
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 setup_math as mathbase_setup
from sphinx.locale import _

View File

@ -1,54 +1,3 @@
from sphinx.config import string_classes, ENUM
value1 = 123 # wrong type
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'))
project = 'Sphinx <Tests>'
release = '0.6alpha1'
templates_path = ['_templates']

View File

@ -12,7 +12,7 @@ from sphinx import addnodes
sys.path.append(os.path.abspath('.'))
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'
@ -65,8 +65,6 @@ man_pages = [
'Georg Brandl and someone else', 1),
]
value_from_conf_py = 84
coverage_c_path = ['special/*.h']
coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'}
@ -104,7 +102,6 @@ class ClassDirective(Directive):
def setup(app):
import parsermod
app.add_config_value('value_from_conf_py', 42, False)
app.add_directive('clsdir', ClassDirective)
app.add_object_type('userdesc', 'userdescrole', '%s (userdesc)',
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
from docutils import nodes
from sphinx.application import ExtensionError
from sphinx.errors import ExtensionError
from sphinx.domains import Domain
from sphinx.testing.util import strip_escseq
from sphinx.util import logging

View File

@ -11,15 +11,15 @@
"""
import mock
import pytest
from six import PY3, iteritems
from six import PY3
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.testing.path import path
@pytest.mark.sphinx(confoverrides={
@pytest.mark.sphinx(testroot='config', confoverrides={
'master_doc': 'master',
'nonexisting_value': 'True',
'latex_elements.docclass': 'scrartcl',
@ -74,36 +74,64 @@ def test_core_config(app, status, warning):
assert cfg['project'] == cfg.project == 'Sphinx Tests'
def test_extension_values(app, status, warning):
cfg = app.config
def test_extension_values():
config = Config()
# default value
assert cfg.value_from_ext == []
# non-default value
assert cfg.value_from_conf_py == 84
# check standard settings
assert config.master_doc == 'contents'
# no duplicate values allowed
# can't override it by add_config_value()
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)
# 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:
app.add_config_value('value_from_ext', 'x', True)
config.add('value_from_ext', [], 'env', None)
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")
def test_errors_warnings(logger, tempdir):
# test the error for syntax errors in the config file
(tempdir / 'conf.py').write_text(u'project = \n', encoding='ascii')
with pytest.raises(ConfigError) as excinfo:
Config(tempdir, 'conf.py', {}, None)
Config.read(tempdir, {}, None)
assert 'conf.py' in str(excinfo.value)
# test the automatic conversion of 2.x only code in configs
(tempdir / 'conf.py').write_text(
u'# -*- coding: utf-8\n\nproject = u"Jägermeister"\n',
encoding='utf-8')
cfg = Config(tempdir, 'conf.py', {}, None)
cfg = Config.read(tempdir, {}, None)
cfg.init_values()
assert cfg.project == u'Jägermeister'
assert logger.called is False
@ -115,7 +143,7 @@ def test_errors_warnings(logger, tempdir):
return
(tempdir / 'conf.py').write_text(
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
cfg.check_unicode()
@ -174,7 +202,7 @@ def test_config_eol(logger, tempdir):
configfile = tempdir / 'conf.py'
for eol in (b'\n', b'\r\n'):
configfile.write_bytes(b'project = "spam"' + eol)
cfg = Config(tempdir, 'conf.py', {}, None)
cfg = Config.read(tempdir, {}, None)
cfg.init_values()
assert cfg.project == u'spam'
assert logger.called is False
@ -195,60 +223,81 @@ def test_builtin_conf(app, status, warning):
'warning')
# See roots/test-config/conf.py.
TYPECHECK_WARNINGS = {
'value1': True,
'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,
}
# example classes for type checking
class A(object):
pass
@pytest.mark.parametrize("key,should", iteritems(TYPECHECK_WARNINGS))
@pytest.mark.sphinx(testroot='config')
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
)
class B(A):
pass
@pytest.mark.sphinx(testroot='config')
def test_check_enum(app, status, warning):
assert "The config value `value17` has to be a one of ('default', 'one', 'two'), " \
not in warning.getvalue()
class C(A):
pass
@pytest.mark.sphinx(testroot='config', confoverrides={'value17': 'invalid'})
def test_check_enum_failed(app, status, warning):
assert "The config value `value17` has to be a one of ('default', 'one', 'two'), " \
"but `invalid` is given." in warning.getvalue()
# name, default, annotation, actual, warned
TYPECHECK_WARNINGS = [
('value1', 'string', None, 123, True), # wrong type
('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']})
def test_check_enum_for_list(app, status, warning):
assert "The config value `value17` has to be a one of ('default', 'one', 'two'), " \
not in warning.getvalue()
@mock.patch("sphinx.config.logger")
@pytest.mark.parametrize("name,default,annotation,actual,warned", TYPECHECK_WARNINGS)
def test_check_types(logger, name, default, annotation, actual, warned):
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']})
def test_check_enum_for_list_failed(app, status, warning):
assert "The config value `value17` has to be a one of ('default', 'one', 'two'), " \
"but `['one', 'two', 'invalid']` is given." in warning.getvalue()
@mock.patch("sphinx.config.logger")
def test_check_enum(logger):
config = Config()
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.testing.util import etree_parse
DUMMY_CONFIG = Config(None, None, {}, '')
DUMMY_CONFIG = Config({}, {})
@pytest.fixture(scope='module')