diff --git a/sphinx/application.py b/sphinx/application.py index 630bff36f..5f6b92f50 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -124,6 +124,7 @@ class Sphinx(object): self.config = Config(confdir, CONFIG_FILENAME, confoverrides or {}, self.tags) self.config.check_unicode(self.warn) + # defer checking types until i18n has been initialized # set confdir to srcdir if -C given (!= no confdir); a few pieces # of code expect a confdir to be set @@ -172,6 +173,8 @@ class Sphinx(object): # set up translation infrastructure self._init_i18n() + # check all configuration values for permissible types + self.config.check_types(self.warn) # set up the build environment self._init_env(freshenv) # set up the builder diff --git a/sphinx/config.py b/sphinx/config.py index cf6bed087..be30d1ea4 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -14,7 +14,7 @@ from os import path from six import PY3, iteritems, string_types, binary_type, integer_types -from sphinx.errors import ConfigError +from sphinx.errors import ConfigError, ConfigWarning from sphinx.locale import l_ from sphinx.util.osutil import make_filename, cd from sphinx.util.pycompat import execfile_ @@ -249,6 +249,30 @@ class Config(object): self.setup = config.get('setup', None) self.extensions = config.get('extensions', []) + def check_types(self, warn): + # 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 l_() we have to wait with calling + # this method until i18n is initialized + for name in self._raw_config: + if name not in Config.config_values: + continue # we don't know a default value + default, dummy_rebuild = Config.config_values[name] + if hasattr(default, '__call__'): + default = default(self) # could invoke l_() + if default is None: + continue + current = self[name] + if type(current) is type(default): + continue + common_bases = ( + set(type(current).__bases__) & 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__)) + def check_unicode(self, warn): # check all string values for non-ASCII characters in bytestrings, # since that can result in UnicodeErrors all over the place @@ -296,7 +320,6 @@ class Config(object): for name in config: if name in self.values: self.__dict__[name] = config[name] - del self._raw_config def __getattr__(self, name): if name.startswith('_'): diff --git a/sphinx/errors.py b/sphinx/errors.py index 3d7a5eb47..4e9828dc2 100644 --- a/sphinx/errors.py +++ b/sphinx/errors.py @@ -3,8 +3,8 @@ sphinx.errors ~~~~~~~~~~~~~ - Contains SphinxError and a few subclasses (in an extra module to avoid - circular import problems). + Contains SphinxError, a few subclasses (in an extra module to avoid + circular import problems), and related classes. :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. @@ -75,3 +75,9 @@ class SphinxParallelError(Exception): def __str__(self): return traceback.format_exception_only( self.orig_exc.__class__, self.orig_exc)[0].strip() + +class ConfigWarning(UserWarning): + """ + Base category for warnings about dubious configuration values. + """ + pass diff --git a/tests/test_config.py b/tests/test_config.py index 0dcf3fa3e..c11c07219 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -133,3 +133,11 @@ def test_config_eol(tmpdir): cfg = Config(tmpdir, 'conf.py', {}, None) cfg.init_values(lambda warning: 1/0) assert cfg.project == u'spam' + + +@with_app(confoverrides={'master_doc': 123}) +def test_check_types(app, status, warning): + # WARNING: the config value 'master_doc' has type `int', defaults to `str.' + assert any(buf.startswith('WARNING:') + and 'master_doc' in buf and 'int' in buf and 'str' in buf + for buf in warning.buflist)