diff options
Diffstat (limited to 'sphinx/config.py')
-rw-r--r-- | sphinx/config.py | 430 |
1 files changed, 290 insertions, 140 deletions
diff --git a/sphinx/config.py b/sphinx/config.py index 1f80ab366..1184f1891 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -11,13 +11,19 @@ import re import traceback +import types +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 six import ( + PY2, PY3, iteritems, string_types, binary_type, text_type, integer_types, class_types +) -from sphinx.errors import ConfigError -from sphinx.locale import l_, __ +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 from sphinx.util.osutil import cd @@ -25,27 +31,16 @@ 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' +UNSERIALIZEABLE_TYPES = class_types + (types.ModuleType, types.FunctionType) 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... @@ -54,6 +49,10 @@ ConfigValue = NamedTuple('ConfigValue', [('name', str), ('rebuild', Union[bool, unicode])]) +#: represents the config value accepts any type of value. +Any = object() + + class ENUM(object): """represents the config value should be a one of candidates. @@ -78,8 +77,15 @@ if PY2: class Config(object): - """ - Configuration file abstraction. + """Configuration file abstraction. + + The config object makes the values of all config values available as + attributes. + + It is exposed via the :py:attr:`sphinx.application.Application.config` and + :py:attr:`sphinx.environment.Environment.config` attributes. For example, + to get the value of :confval:`language`, use either ``app.config.language`` + or ``env.config.language``. """ # the values are: (default, what needs to be rebuilt if changed) @@ -89,86 +95,83 @@ class Config(object): config_values = dict( # general options - project = ('Python', 'env'), - copyright = ('', 'html'), - version = ('', 'env'), - release = ('', 'env'), - today = ('', 'env'), + project = ('Python', 'env', []), + author = ('unknown', 'env', []), + copyright = ('', 'html', []), + version = ('', 'env', []), + release = ('', 'env', []), + today = ('', 'env', []), # the real default is locale-dependent today_fmt = (None, 'env', string_classes), language = (None, 'env', string_classes), - locale_dirs = (['locales'], 'env'), + locale_dirs = (['locales'], 'env', []), figure_language_filename = (u'{root}.{language}{ext}', 'env', [str]), - master_doc = ('contents', 'env'), - source_suffix = (['.rst'], 'env'), - source_encoding = ('utf-8-sig', 'env'), - source_parsers = ({}, 'env'), - exclude_patterns = ([], 'env'), + master_doc = ('contents', 'env', []), + source_suffix = ({'.rst': 'restructuredtext'}, 'env', Any), + source_encoding = ('utf-8-sig', 'env', []), + source_parsers = ({}, 'env', []), + exclude_patterns = ([], 'env', []), default_role = (None, 'env', string_classes), - add_function_parentheses = (True, 'env'), - add_module_names = (True, 'env'), - trim_footnote_reference_space = (False, 'env'), - show_authors = (False, 'env'), + add_function_parentheses = (True, 'env', []), + add_module_names = (True, 'env', []), + trim_footnote_reference_space = (False, 'env', []), + show_authors = (False, 'env', []), pygments_style = (None, 'html', string_classes), - highlight_language = ('default', 'env'), - highlight_options = ({}, 'env'), - templates_path = ([], 'html'), + highlight_language = ('default', 'env', []), + highlight_options = ({}, 'env', []), + templates_path = ([], 'html', []), template_bridge = (None, 'html', string_classes), - keep_warnings = (False, 'env'), - suppress_warnings = ([], 'env'), - modindex_common_prefix = ([], 'html'), + keep_warnings = (False, 'env', []), + suppress_warnings = ([], 'env', []), + modindex_common_prefix = ([], 'html', []), rst_epilog = (None, 'env', string_classes), rst_prolog = (None, 'env', string_classes), - trim_doctest_flags = (True, 'env'), + trim_doctest_flags = (True, 'env', []), primary_domain = ('py', 'env', [NoneType]), needs_sphinx = (None, None, string_classes), - needs_extensions = ({}, None), - manpages_url = (None, 'env'), - nitpicky = (False, None), - nitpick_ignore = ([], None), - numfig = (False, 'env'), - numfig_secnum_depth = (1, 'env'), - numfig_format = ({'section': l_('Section %s'), - 'figure': l_('Fig. %s'), - 'table': l_('Table %s'), - 'code-block': l_('Listing %s')}, - 'env'), - - tls_verify = (True, 'env'), - tls_cacerts = (None, 'env'), - smartquotes = (True, 'env'), - smartquotes_action = ('qDe', 'env'), + needs_extensions = ({}, None, []), + manpages_url = (None, 'env', []), + nitpicky = (False, None, []), + nitpick_ignore = ([], None, []), + numfig = (False, 'env', []), + numfig_secnum_depth = (1, 'env', []), + numfig_format = ({}, 'env', []), # will be initialized in init_numfig_format() + + tls_verify = (True, 'env', []), + tls_cacerts = (None, 'env', []), + smartquotes = (True, 'env', []), + smartquotes_action = ('qDe', 'env', []), smartquotes_excludes = ({'languages': ['ja'], 'builders': ['man', 'text']}, - 'env'), + '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: @@ -178,66 +181,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 l_() 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 l_() - if default is None and not permitted: - continue # neither inferrable nor expliclitly permitted types - current = self[name] - if 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 @@ -245,7 +207,9 @@ class Config(object): return value else: defvalue = self.values[name][0] - if isinstance(defvalue, dict): + if self.values[name][2] == Any: + return value + elif isinstance(defvalue, dict): raise ValueError(__('cannot override dictionary config setting %r, ' 'ignoring (use %r to set individual elements)') % (name, name + '.key=value')) @@ -302,8 +266,6 @@ class Config(object): for name in config: if name in self.values: self.__dict__[name] = config[name] # type: ignore - if isinstance(self.source_suffix, string_types): # type: ignore - self.source_suffix = [self.source_suffix] # type: ignore def __getattr__(self, name): # type: (unicode) -> Any @@ -333,16 +295,204 @@ 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 __getstate__(self): + # type: () -> Dict + """Obtains serializable data for pickling.""" + # remove potentially pickling-problematic values from config + __dict__ = {} + for key, value in iteritems(self.__dict__): + if key.startswith('_') or isinstance(value, UNSERIALIZEABLE_TYPES): + pass + else: + __dict__[key] = value + + # create a picklable copy of values list + __dict__['values'] = {} + for key, value in iteritems(self.values): # type: ignore + real_value = getattr(self, key) + if isinstance(real_value, UNSERIALIZEABLE_TYPES): + # omit unserializable value + real_value = None + + # types column is also omitted + __dict__['values'][key] = (real_value, value[1], None) + + return __dict__ + + def __setstate__(self, state): + # type: (Dict) -> None + self.__dict__.update(state) + + +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): + # type: (Sphinx, Config) -> None + """This converts old styled source_suffix to new styled one. + + * old style: str or list + * new style: a dict which maps from fileext to filetype + """ + source_suffix = config.source_suffix + if isinstance(source_suffix, string_types): + # if str, considers as default filetype (None) + # + # The default filetype is determined on later step. + # By default, it is considered as restructuredtext. + config.source_suffix = OrderedDict({source_suffix: None}) # type: ignore + elif isinstance(source_suffix, (list, tuple)): + # if list, considers as all of them are default filetype + config.source_suffix = OrderedDict([(s, None) for s in source_suffix]) # type: ignore # NOQA + elif isinstance(source_suffix, dict): + # if dict, convert it to OrderedDict + config.source_suffix = OrderedDict(config.source_suffix) # type: ignore + else: + logger.warning(__("The config value `source_suffix' expected to " + "a string, list of strings or dictionary. " + "But `%r' is given." % source_suffix)) + + +def init_numfig_format(app, config): + # type: (Sphinx, Config) -> None + """Initialize :confval:`numfig_format`.""" + numfig_format = {'section': _('Section %s'), + 'figure': _('Fig. %s'), + 'table': _('Table %s'), + 'code-block': _('Listing %s')} + + # override default labels by configuration + numfig_format.update(config.numfig_format) + 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: + default, rebuild, annotations = config.values[confval.name] + + 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', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } |