mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #2033 from lehmannro/config-types-for-real
Allow hinting permissible types for configuration values
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -20,6 +20,8 @@ from itertools import product
|
||||
|
||||
from six import PY3, text_type, exec_
|
||||
|
||||
NoneType = type(None)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Python 2/3 compatibility
|
||||
|
||||
|
||||
32
tests/roots/test-config/conf.py
Normal file
32
tests/roots/test-config/conf.py
Normal 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)
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user