Merge pull request #2033 from lehmannro/config-types-for-real

Allow hinting permissible types for configuration values
This commit is contained in:
Takayuki SHIMIZUKAWA
2015-11-15 16:39:07 +09:00
7 changed files with 127 additions and 86 deletions

View File

@@ -535,13 +535,14 @@ class Sphinx(object):
builder.name, self.builderclasses[builder.name].__module__))
self.builderclasses[builder.name] = builder
def add_config_value(self, name, default, rebuild):
self.debug('[app] adding config value: %r', (name, default, rebuild))
def add_config_value(self, name, default, rebuild, types=()):
self.debug('[app] adding config value: %r',
(name, default, rebuild) + ((types,) if types else ()))
if name in self.config.values:
raise ExtensionError('Config value %r already present' % name)
if rebuild in (False, True):
rebuild = rebuild and 'env' or ''
self.config.values[name] = (default, rebuild)
self.config.values[name] = (default, rebuild, types)
def add_event(self, name):
self.debug('[app] adding event: %r', name)

View File

@@ -18,7 +18,7 @@ from six import PY3, iteritems, string_types, binary_type, integer_types
from sphinx.errors import ConfigError
from sphinx.locale import l_
from sphinx.util.osutil import make_filename, cd
from sphinx.util.pycompat import execfile_
from sphinx.util.pycompat import execfile_, NoneType
nonascii_re = re.compile(br'[\x80-\xff]')
@@ -27,10 +27,8 @@ if PY3:
CONFIG_SYNTAX_ERROR += "\nDid you change the syntax from 2.x to 3.x?"
CONFIG_EXIT_ERROR = "The configuration file (or one of the modules it imports) " \
"called sys.exit()"
IGNORE_CONFIG_TYPE_CHECKS = (
'html_domain_indices', 'latex_domain_indices', 'texinfo_domain_indices'
)
CONFIG_TYPE_WARNING = "The config value `{name}' has type `{current.__name__}', " \
"defaults to `{default.__name__}.'"
class Config(object):
@@ -50,9 +48,10 @@ class Config(object):
version = ('', 'env'),
release = ('', 'env'),
today = ('', 'env'),
today_fmt = (None, 'env'), # the real default is locale-dependent
# the real default is locale-dependent
today_fmt = (None, 'env', [str]),
language = (None, 'env'),
language = (None, 'env', [str]),
locale_dirs = ([], 'env'),
master_doc = ('contents', 'env'),
@@ -60,23 +59,23 @@ class Config(object):
source_encoding = ('utf-8-sig', 'env'),
source_parsers = ({}, 'env'),
exclude_patterns = ([], 'env'),
default_role = (None, 'env'),
default_role = (None, 'env', [str]),
add_function_parentheses = (True, 'env'),
add_module_names = (True, 'env'),
trim_footnote_reference_space = (False, 'env'),
show_authors = (False, 'env'),
pygments_style = (None, 'html'),
pygments_style = (None, 'html', [str]),
highlight_language = ('python', 'env'),
highlight_options = ({}, 'env'),
templates_path = ([], 'html'),
template_bridge = (None, 'html'),
template_bridge = (None, 'html', [str]),
keep_warnings = (False, 'env'),
modindex_common_prefix = ([], 'html'),
rst_epilog = (None, 'env'),
rst_prolog = (None, 'env'),
rst_epilog = (None, 'env', [str]),
rst_prolog = (None, 'env', [str]),
trim_doctest_flags = (True, 'env'),
primary_domain = ('py', 'env'),
needs_sphinx = (None, None),
primary_domain = ('py', 'env', [NoneType]),
needs_sphinx = (None, None, [str]),
needs_extensions = ({}, None),
nitpicky = (False, 'env'),
nitpick_ignore = ([], 'html'),
@@ -95,34 +94,34 @@ class Config(object):
(self.project, self.release),
'html'),
html_short_title = (lambda self: self.html_title, 'html'),
html_style = (None, 'html'),
html_logo = (None, 'html'),
html_favicon = (None, 'html'),
html_style = (None, 'html', [str]),
html_logo = (None, 'html', [str]),
html_favicon = (None, 'html', [str]),
html_static_path = ([], 'html'),
html_extra_path = ([], 'html'),
# the real default is locale-dependent
html_last_updated_fmt = (None, 'html'),
html_last_updated_fmt = (None, 'html', [str]),
html_use_smartypants = (True, 'html'),
html_translator_class = (None, 'html'),
html_translator_class = (None, 'html', [str]),
html_sidebars = ({}, 'html'),
html_additional_pages = ({}, 'html'),
html_use_modindex = (True, 'html'), # deprecated
html_domain_indices = (True, 'html'),
html_domain_indices = (True, 'html', [list]),
html_add_permalinks = (u'\u00B6', 'html'),
html_use_index = (True, 'html'),
html_split_index = (False, 'html'),
html_copy_source = (True, 'html'),
html_show_sourcelink = (True, 'html'),
html_use_opensearch = ('', 'html'),
html_file_suffix = (None, 'html'),
html_link_suffix = (None, 'html'),
html_file_suffix = (None, 'html', [str]),
html_link_suffix = (None, 'html', [str]),
html_show_copyright = (True, 'html'),
html_show_sphinx = (True, 'html'),
html_context = ({}, 'html'),
html_output_encoding = ('utf-8', 'html'),
html_compact_lists = (True, 'html'),
html_secnumber_suffix = ('. ', 'html'),
html_search_language = (None, 'html'),
html_search_language = (None, 'html', [str]),
html_search_options = ({}, 'html'),
html_search_scorer = ('', None),
html_scaled_image_link = (True, 'html'),
@@ -139,17 +138,17 @@ class Config(object):
# Apple help options
applehelp_bundle_name = (lambda self: make_filename(self.project),
'applehelp'),
applehelp_bundle_id = (None, 'applehelp'),
applehelp_bundle_id = (None, 'applehelp', [str]),
applehelp_dev_region = ('en-us', 'applehelp'),
applehelp_bundle_version = ('1', 'applehelp'),
applehelp_icon = (None, 'applehelp'),
applehelp_icon = (None, 'applehelp', [str]),
applehelp_kb_product = (lambda self: '%s-%s' %
(make_filename(self.project), self.release),
'applehelp'),
applehelp_kb_url = (None, 'applehelp'),
applehelp_remote_url = (None, 'applehelp'),
applehelp_index_anchors = (False, 'applehelp'),
applehelp_min_term_length = (None, 'applehelp'),
applehelp_kb_url = (None, 'applehelp', [str]),
applehelp_remote_url = (None, 'applehelp', [str]),
applehelp_index_anchors = (False, 'applehelp', [str]),
applehelp_min_term_length = (None, 'applehelp', [str]),
applehelp_stopwords = (lambda self: self.language or 'en', 'applehelp'),
applehelp_locale = (lambda self: self.language or 'en', 'applehelp'),
applehelp_title = (lambda self: self.project + ' Help', 'applehelp'),
@@ -196,11 +195,11 @@ class Config(object):
self.project,
'', 'manual')],
None),
latex_logo = (None, None),
latex_logo = (None, None, [str]),
latex_appendices = ([], None),
latex_use_parts = (False, None),
latex_use_modindex = (True, None), # deprecated
latex_domain_indices = (True, None),
latex_domain_indices = (True, None, [list]),
latex_show_urls = ('no', None),
latex_show_pagerefs = (False, None),
# paper_size and font_size are still separate values
@@ -236,13 +235,13 @@ class Config(object):
None),
texinfo_appendices = ([], None),
texinfo_elements = ({}, None),
texinfo_domain_indices = (True, None),
texinfo_domain_indices = (True, None, [list]),
texinfo_show_urls = ('footnote', None),
texinfo_no_detailmenu = (False, None),
# linkcheck options
linkcheck_ignore = ([], None),
linkcheck_timeout = (None, None),
linkcheck_timeout = (None, None, [int]),
linkcheck_workers = (5, None),
linkcheck_anchors = (True, None),
@@ -292,25 +291,30 @@ class Config(object):
# NB. since config values might use l_() we have to wait with calling
# this method until i18n is initialized
for name in self._raw_config:
if name in IGNORE_CONFIG_TYPE_CHECKS:
continue # for a while, ignore multiple types config value. see #1781
if name not in Config.config_values:
if name not in self.values:
continue # we don't know a default value
default, dummy_rebuild = Config.config_values[name]
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 l_()
if default is None:
continue
if default is None and not permitted:
continue # neither inferrable nor expliclitly permitted types
current = self[name]
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
warn("the config value %r has type `%s', defaults to `%s.'" %
(name, type(current).__name__, type(default).__name__))
warn(CONFIG_TYPE_WARNING.format(
name=name, current=type(current), default=type(default)))
def check_unicode(self, warn):
# check all string values for non-ASCII characters in bytestrings,

View File

@@ -583,5 +583,5 @@ def setup(app):
app.add_role('autolink', autolink_role)
app.connect('doctree-read', process_autosummary_toc)
app.connect('builder-inited', process_generate_options)
app.add_config_value('autosummary_generate', [], True)
app.add_config_value('autosummary_generate', [], True, [bool])
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@@ -20,6 +20,8 @@ from itertools import product
from six import PY3, text_type, exec_
NoneType = type(None)
# ------------------------------------------------------------------------------
# Python 2/3 compatibility

View File

@@ -0,0 +1,32 @@
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
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)

View File

@@ -9,9 +9,10 @@
:copyright: Copyright 2007-2015 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from six import PY2, PY3, StringIO
from six import PY2, PY3, StringIO, iteritems
from util import TestApp, with_app, with_tempdir, raises, raises_msg
from util import TestApp, with_app, gen_with_app, with_tempdir, \
raises, raises_msg, assert_in, assert_not_in
from sphinx.config import Config
from sphinx.errors import ExtensionError, ConfigError, VersionRequirementError
@@ -135,36 +136,36 @@ def test_config_eol(tmpdir):
assert cfg.project == u'spam'
TYPECHECK_OVERRIDES = [
# configuration key, override value, should warn, default type
('master_doc', 123, True, str),
('man_pages', 123, True, list), # lambda
('man_pages', [], False, list),
('epub_tocdepth', True, True, int), # child type
('nitpicky', 3, False, bool), # parent type
('templates_path', (), True, list), # other sequence, also raises
]
if PY2:
# Run a check for proper sibling detection in Python 2. Under py3k, the
# default types do not have any siblings.
TYPECHECK_OVERRIDES.append(
('html_add_permalinks', 'bar', False, unicode))
@with_app(confoverrides={
'master_doc': 123,
'language': 'foo',
'primary_domain': None})
def test_builtin_conf(app, status, warning):
warnings = warning.getvalue()
assert_in('master_doc', warnings,
'override on builtin "master_doc" should raise a type warning')
assert_not_in('language', warnings, 'explicitly permitted '
'override on builtin "language" should NOT raise a type warning')
assert_not_in('primary_domain', warnings, 'override to None on builtin '
'"primary_domain" should NOT raise a type warning')
def test_gen_check_types():
for key, value, should, deftype in TYPECHECK_OVERRIDES:
warning = StringIO()
app = TestApp(confoverrides={key: value}, warning=warning)
app.cleanup()
real = type(value).__name__
msg = ("WARNING: the config value %r has type `%s',"
" defaults to `%s.'\n" % (key, real, deftype.__name__))
def test():
warning_list = warning.getvalue()
assert (msg in warning_list) == should, \
"Setting %s to %r should%s raise: %s" % \
(key, value, " not" if should else "", msg)
test.description = "test_check_type_%s_on_%s" % \
(real, type(Config.config_values[key][0]).__name__)
yield test
# 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,
}
@gen_with_app(testroot='config')
def test_gen_check_types(app, status, warning):
for key, should in iteritems(TYPECHECK_WARNINGS):
yield assert_in if should else assert_not_in, key, warning.getvalue(), \
'override on "%s" should%s raise a type warning' % \
(key, '' if should else ' NOT')

View File

@@ -94,14 +94,15 @@ def assert_startswith(thing, prefix):
assert False, '%r does not start with %r' % (thing, prefix)
def assert_in(x, thing):
if x not in thing:
assert False, '%r is not in %r' % (x, thing)
def assert_not_in(x, thing):
if x in thing:
assert False, '%r is in %r' % (x, thing)
try:
from nose.tools import assert_in, assert_not_in
except ImportError:
def assert_in(x, thing, msg=''):
if x not in thing:
assert False, msg or '%r is not in %r%r' % (x, thing)
def assert_not_in(x, thing, msg=''):
if x in thing:
assert False, msg or '%r is in %r%r' % (x, thing)
def skip_if(condition, msg=None):