Código-fonte para sphinx.config

"""Build configuration file handling."""

from __future__ import annotations

import time
import traceback
import types
import warnings
from contextlib import chdir
from os import getenv
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, NamedTuple

from sphinx.deprecation import RemovedInSphinx90Warning
from sphinx.errors import ConfigError, ExtensionError
from sphinx.locale import _, __
from sphinx.util import logging

if TYPE_CHECKING:
    import os
    from collections.abc import Collection, Iterable, Iterator, Sequence, Set
    from typing import TypeAlias

    from sphinx.application import Sphinx
    from sphinx.environment import BuildEnvironment
    from sphinx.util.tags import Tags
    from sphinx.util.typing import ExtensionMetadata, _ExtensionSetupFunc

logger = logging.getLogger(__name__)

_ConfigRebuild: TypeAlias = Literal[
    '',
    'env',
    'epub',
    'gettext',
    'html',
    # sphinxcontrib-applehelp
    'applehelp',
    # sphinxcontrib-devhelp
    'devhelp',
]

CONFIG_FILENAME = 'conf.py'
UNSERIALIZABLE_TYPES = (type, types.ModuleType, types.FunctionType)


class ConfigValue(NamedTuple):
    name: str
    value: Any
    rebuild: _ConfigRebuild


def is_serializable(obj: object, *, _seen: frozenset[int] = frozenset()) -> bool:
    """Check if an object is serializable or not."""
    if isinstance(obj, UNSERIALIZABLE_TYPES):
        return False

    # use id() to handle un-hashable objects
    if id(obj) in _seen:
        return True

    if isinstance(obj, dict):
        seen = _seen | {id(obj)}
        return all(
            is_serializable(key, _seen=seen) and is_serializable(value, _seen=seen)
            for key, value in obj.items()
        )
    elif isinstance(obj, list | tuple | set | frozenset):
        seen = _seen | {id(obj)}
        return all(is_serializable(item, _seen=seen) for item in obj)

    # if an issue occurs for a non-serializable type, pickle will complain
    # since the object is likely coming from a third-party extension
    # (we natively expect 'simple' types and not weird ones)
    return True


class ENUM:
    """Represents the candidates which a config value should be one of.

    Example:
        app.add_config_value('latex_show_urls', 'no', None, ENUM('no', 'footnote', 'inline'))
    """

    def __init__(self, *candidates: str | bool | None) -> None:
        self._candidates = frozenset(candidates)

    def __repr__(self) -> str:
        return f'ENUM({", ".join(sorted(map(repr, self._candidates)))})'

    def match(self, value: str | bool | None | Sequence[str | bool | None]) -> bool:  # NoQA: RUF036
        if isinstance(value, str | bool | None):
            return value in self._candidates
        return all(item in self._candidates for item in value)


_OptValidTypes: TypeAlias = frozenset[type] | ENUM


class _Opt:
    __slots__ = 'default', 'rebuild', 'valid_types', 'description'

    default: Any
    rebuild: _ConfigRebuild
    valid_types: _OptValidTypes
    description: str

    def __init__(
        self,
        default: Any,
        rebuild: _ConfigRebuild,
        valid_types: _OptValidTypes,
        description: str = '',
    ) -> None:
        """Configuration option type for Sphinx.

        The type is intended to be immutable; changing the field values
        is an unsupported action.
        No validation is performed on the values, though consumers will
        likely expect them to be of the types advertised.
        The old tuple-based interface will be removed in Sphinx 9.
        """
        super().__setattr__('default', default)
        super().__setattr__('rebuild', rebuild)
        super().__setattr__('valid_types', valid_types)
        super().__setattr__('description', description)

    def __repr__(self) -> str:
        return (
            f'{self.__class__.__qualname__}('
            f'default={self.default!r}, '
            f'rebuild={self.rebuild!r}, '
            f'valid_types={self.rebuild!r}, '
            f'description={self.description!r})'
        )

    def __eq__(self, other: object) -> bool:
        if isinstance(other, _Opt):
            self_tpl = (
                self.default,
                self.rebuild,
                self.valid_types,
                self.description,
            )
            other_tpl = (
                other.default,
                other.rebuild,
                other.valid_types,
                other.description,
            )
            return self_tpl == other_tpl
        return NotImplemented

    def __lt__(self, other: _Opt) -> bool:
        if self.__class__ is other.__class__:
            self_tpl = (
                self.default,
                self.rebuild,
                self.valid_types,
                self.description,
            )
            other_tpl = (
                other.default,
                other.rebuild,
                other.valid_types,
                other.description,
            )
            return self_tpl > other_tpl
        return NotImplemented

    def __hash__(self) -> int:
        return hash((self.default, self.rebuild, self.valid_types, self.description))

    def __setattr__(self, key: str, value: Any) -> None:
        if key in {'default', 'rebuild', 'valid_types', 'description'}:
            msg = f'{self.__class__.__name__!r} object does not support assignment to {key!r}'
            raise TypeError(msg)
        super().__setattr__(key, value)

    def __delattr__(self, key: str) -> None:
        if key in {'default', 'rebuild', 'valid_types', 'description'}:
            msg = f'{self.__class__.__name__!r} object does not support deletion of {key!r}'
            raise TypeError(msg)
        super().__delattr__(key)

    def __getstate__(self) -> tuple[Any, _ConfigRebuild, _OptValidTypes, str]:
        return self.default, self.rebuild, self.valid_types, self.description

    def __setstate__(
        self, state: tuple[Any, _ConfigRebuild, _OptValidTypes, str]
    ) -> None:
        default, rebuild, valid_types, description = state
        super().__setattr__('default', default)
        super().__setattr__('rebuild', rebuild)
        super().__setattr__('valid_types', valid_types)
        super().__setattr__('description', description)

    def __getitem__(self, item: int | slice) -> Any:
        warnings.warn(
            f'The {self.__class__.__name__!r} object tuple interface is deprecated, '
            "use attribute access instead for 'default', 'rebuild', and 'valid_types'.",
            RemovedInSphinx90Warning,
            stacklevel=2,
        )
        return (self.default, self.rebuild, self.valid_types)[item]


[documentos] class Config: r"""Configuration file abstraction. The Config object makes the values of all config options available as attributes. It is exposed via the :py:class:`~sphinx.application.Sphinx`\ ``.config`` and :py:class:`sphinx.environment.BuildEnvironment`\ ``.config`` attributes. For example, to get the value of :confval:`language`, use either ``app.config.language`` or ``env.config.language``. """ # The values are: # 1. Default # 2. What needs to be rebuilt if changed # 3. Valid types # If you add a value here, remember to include it in the docs! config_values: dict[str, _Opt] = { # general options 'project': _Opt('Project name not set', 'env', frozenset((str,))), 'author': _Opt('Author name not set', 'env', frozenset((str,))), 'project_copyright': _Opt('', 'html', frozenset((str, tuple, list))), 'copyright': _Opt( lambda config: config.project_copyright, 'html', frozenset((str, tuple, list)), ), 'version': _Opt('', 'env', frozenset((str,))), 'release': _Opt('', 'env', frozenset((str,))), 'today': _Opt('', 'env', frozenset((str,))), # the real default is locale-dependent 'today_fmt': _Opt(None, 'env', frozenset((str,))), 'language': _Opt('en', 'env', frozenset((str,))), 'locale_dirs': _Opt(['locales'], 'env', frozenset((list, tuple))), 'figure_language_filename': _Opt( '{root}.{language}{ext}', 'env', frozenset((str,)) ), 'gettext_allow_fuzzy_translations': _Opt(False, 'gettext', frozenset((bool,))), 'translation_progress_classes': _Opt( False, 'env', ENUM(True, False, 'translated', 'untranslated') ), 'master_doc': _Opt('index', 'env', frozenset((str,))), 'root_doc': _Opt(lambda config: config.master_doc, 'env', frozenset((str,))), # ``source_suffix`` type is actually ``dict[str, str | None]``: # see ``convert_source_suffix()`` below. 'source_suffix': _Opt({'.rst': 'restructuredtext'}, 'env', Any), # type: ignore[arg-type] 'source_encoding': _Opt('utf-8-sig', 'env', frozenset((str,))), 'exclude_patterns': _Opt([], 'env', frozenset((str,))), 'include_patterns': _Opt(['**'], 'env', frozenset((str,))), 'default_role': _Opt(None, 'env', frozenset((str,))), 'add_function_parentheses': _Opt(True, 'env', frozenset((bool,))), 'add_module_names': _Opt(True, 'env', frozenset((bool,))), 'toc_object_entries': _Opt(True, 'env', frozenset((bool,))), 'toc_object_entries_show_parents': _Opt( 'domain', 'env', ENUM('domain', 'all', 'hide') ), 'trim_footnote_reference_space': _Opt(False, 'env', frozenset((bool,))), 'show_authors': _Opt(False, 'env', frozenset((bool,))), 'pygments_style': _Opt(None, 'html', frozenset((str,))), 'highlight_language': _Opt('default', 'env', frozenset((str,))), 'highlight_options': _Opt({}, 'env', frozenset((dict,))), 'templates_path': _Opt([], 'html', frozenset((list,))), 'template_bridge': _Opt(None, 'html', frozenset((str,))), 'keep_warnings': _Opt(False, 'env', frozenset((bool,))), 'suppress_warnings': _Opt([], 'env', frozenset((list, tuple))), 'show_warning_types': _Opt(True, 'env', frozenset((bool,))), 'modindex_common_prefix': _Opt([], 'html', frozenset((list, tuple))), 'rst_epilog': _Opt(None, 'env', frozenset((str,))), 'rst_prolog': _Opt(None, 'env', frozenset((str,))), 'trim_doctest_flags': _Opt(True, 'env', frozenset((bool,))), 'primary_domain': _Opt('py', 'env', frozenset((types.NoneType,))), 'needs_sphinx': _Opt(None, '', frozenset((str,))), 'needs_extensions': _Opt({}, '', frozenset((dict,))), 'manpages_url': _Opt(None, 'env', frozenset((str, types.NoneType))), 'nitpicky': _Opt(False, '', frozenset((bool,))), 'nitpick_ignore': _Opt([], '', frozenset((set, list, tuple))), 'nitpick_ignore_regex': _Opt([], '', frozenset((set, list, tuple))), 'numfig': _Opt(False, 'env', frozenset((bool,))), 'numfig_secnum_depth': _Opt(1, 'env', frozenset((int, types.NoneType))), # numfig_format will be initialized in init_numfig_format() 'numfig_format': _Opt({}, 'env', frozenset((dict,))), 'maximum_signature_line_length': _Opt( None, 'env', frozenset((int, types.NoneType)) ), 'math_number_all': _Opt(False, 'env', frozenset((bool,))), 'math_eqref_format': _Opt(None, 'env', frozenset((str,))), 'math_numfig': _Opt(True, 'env', frozenset((bool,))), 'math_numsep': _Opt('.', 'env', frozenset((str,))), 'tls_verify': _Opt(True, 'env', frozenset((bool,))), 'tls_cacerts': _Opt(None, 'env', frozenset((str, dict, types.NoneType))), 'user_agent': _Opt(None, 'env', frozenset((str,))), 'smartquotes': _Opt(True, 'env', frozenset((bool,))), 'smartquotes_action': _Opt('qDe', 'env', frozenset((str,))), 'smartquotes_excludes': _Opt( {'languages': ['ja', 'zh_CN', 'zh_TW'], 'builders': ['man', 'text']}, 'env', frozenset((dict,)), ), 'option_emphasise_placeholders': _Opt(False, 'env', frozenset((bool,))), } def __init__( self, config: dict[str, Any] | None = None, overrides: dict[str, Any] | None = None, ) -> None: raw_config: dict[str, Any] = config or {} self._overrides = dict(overrides) if overrides is not None else {} self._options = Config.config_values.copy() self._raw_config = raw_config for name in list(self._overrides.keys()): if '.' in name: real_name, key = name.split('.', 1) raw_config.setdefault(real_name, {})[key] = self._overrides.pop(name) self.setup: _ExtensionSetupFunc | None = raw_config.get('setup') if 'extensions' in self._overrides: extensions = self._overrides.pop('extensions') if isinstance(extensions, str): raw_config['extensions'] = extensions.split(',') else: raw_config['extensions'] = extensions self.extensions: list[str] = raw_config.get('extensions', []) @property def values(self) -> dict[str, _Opt]: return self._options @property def overrides(self) -> dict[str, Any]: return self._overrides @classmethod def read( cls: type[Config], confdir: str | os.PathLike[str], overrides: dict[str, Any] | None = None, tags: Tags | None = None, ) -> Config: """Create a Config object from configuration file.""" filename = Path(confdir, CONFIG_FILENAME) if not filename.is_file(): raise ConfigError( __("config directory doesn't contain a conf.py file (%s)") % confdir ) namespace = eval_config_file(filename, tags) # Note: Old sphinx projects have been configured as "language = None" because # sphinx-quickstart previously generated this by default. # To keep compatibility, they should be fallback to 'en' for a while # (This conversion should not be removed before 2025-01-01). if namespace.get('language', ...) is None: logger.warning( __( "Invalid configuration value found: 'language = None'. " 'Update your configuration to a valid language code. ' "Falling back to 'en' (English)." ) ) namespace['language'] = 'en' return cls(namespace, overrides) def convert_overrides(self, name: str, value: str) -> Any: opt = self._options[name] default = opt.default valid_types = opt.valid_types if valid_types == Any: return value if isinstance(valid_types, ENUM): if False in valid_types._candidates and value == '0': return False if True in valid_types._candidates and value == '1': return True return value elif type(default) is bool or (bool in valid_types): if value == '0': return False if value == '1': return True if len(valid_types) > 1: return value msg = __("'%s' must be '0' or '1', got '%s'") % (name, value) raise ConfigError(msg) if isinstance(default, dict): raise ValueError( # NoQA: TRY004 __( 'cannot override dictionary config setting %r, ' 'ignoring (use %r to set individual elements)' ) % (name, f'{name}.key=value') ) if isinstance(default, list): return value.split(',') if isinstance(default, int): try: return int(value) except ValueError as exc: raise ValueError( __('invalid number %r for config value %r, ignoring') % (value, name) ) from exc if callable(default): return value if isinstance(default, str) or default is None: return value raise ValueError( __('cannot override config setting %r with unsupported type, ignoring') % name ) @staticmethod def pre_init_values() -> None: # method only retained for compatibility pass # warnings.warn( # 'Config.pre_init_values() will be removed in Sphinx 9.0 or later', # RemovedInSphinx90Warning, stacklevel=2) def init_values(self) -> None: # method only retained for compatibility self._report_override_warnings() # warnings.warn( # 'Config.init_values() will be removed in Sphinx 9.0 or later', # RemovedInSphinx90Warning, stacklevel=2) def _report_override_warnings(self) -> None: for name in self._overrides: if name not in self._options: logger.warning( __('unknown config value %r in override, ignoring'), name ) def __repr__(self) -> str: values = [] for opt_name in self._options: try: opt_value = getattr(self, opt_name) except Exception: opt_value = '<error!>' values.append(f'{opt_name}={opt_value!r}') return self.__class__.__qualname__ + '(' + ', '.join(values) + ')' def __setattr__(self, key: str, value: object) -> None: # Ensure aliases update their counterpart. if key == 'master_doc': super().__setattr__('root_doc', value) elif key == 'root_doc': super().__setattr__('master_doc', value) elif key == 'copyright': super().__setattr__('project_copyright', value) elif key == 'project_copyright': super().__setattr__('copyright', value) super().__setattr__(key, value) def __getattr__(self, name: str) -> Any: if name in self._options: # first check command-line overrides if name in self._overrides: value = self._overrides[name] if not isinstance(value, str): self.__dict__[name] = value return value try: value = self.convert_overrides(name, value) except ValueError as exc: logger.warning('%s', exc) else: self.__setattr__(name, value) return value # then check values from 'conf.py' if name in self._raw_config: value = self._raw_config[name] self.__setattr__(name, value) return value # finally, fall back to the default value default = self._options[name].default if callable(default): return default(self) self.__dict__[name] = default return default if name.startswith('_'): msg = f'{self.__class__.__name__!r} object has no attribute {name!r}' raise AttributeError(msg) msg = __('No such config value: %r') % name raise AttributeError(msg) def __getitem__(self, name: str) -> Any: return getattr(self, name) def __setitem__(self, name: str, value: Any) -> None: setattr(self, name, value) def __delitem__(self, name: str) -> None: delattr(self, name) def __contains__(self, name: str) -> bool: return name in self._options def __iter__(self) -> Iterator[ConfigValue]: for name, opt in self._options.items(): yield ConfigValue(name, getattr(self, name), opt.rebuild) def add( self, name: str, default: Any, rebuild: _ConfigRebuild, types: type | Collection[type] | ENUM, description: str = '', ) -> None: if name in self._options: raise ExtensionError(__('Config value %r already present') % name) # standardise rebuild if isinstance(rebuild, bool): rebuild = 'env' if rebuild else '' # standardise valid_types valid_types = _validate_valid_types(types) self._options[name] = _Opt(default, rebuild, valid_types, description) def filter(self, rebuild: Set[_ConfigRebuild]) -> Iterator[ConfigValue]: if isinstance(rebuild, str): return (value for value in self if value.rebuild == rebuild) return (value for value in self if value.rebuild in rebuild) def __getstate__(self) -> dict[str, Any]: """Obtains serializable data for pickling.""" # remove potentially pickling-problematic values from config __dict__ = { key: value for key, value in self.__dict__.items() if not key.startswith('_') and is_serializable(value) } # create a pickleable copy of ``self._options`` __dict__['_options'] = _options = {} for name, opt in self._options.items(): if not isinstance(opt, _Opt) and isinstance(opt, tuple) and len(opt) <= 3: # Fix for Furo's ``_update_default``. self._options[name] = opt = _Opt(*opt) real_value = getattr(self, name) if not is_serializable(real_value): if opt.rebuild: # if the value is not cached, then any build that utilises this cache # will always mark the config value as changed, # and thus always invalidate the cache and perform a rebuild. logger.warning( __( 'cannot cache unpickleable configuration value: %r ' '(because it contains a function, class, or module object)' ), name, type='config', subtype='cache', once=True, ) # omit unserializable value real_value = None # valid_types is also omitted _options[name] = real_value, opt.rebuild return __dict__ def __setstate__(self, state: dict[str, Any]) -> None: self._overrides = {} self._options = { name: _Opt(real_value, rebuild, frozenset()) for name, (real_value, rebuild) in state.pop('_options').items() } self._raw_config = {} self.__dict__.update(state)
def eval_config_file( filename: str | os.PathLike[str], tags: Tags | None ) -> dict[str, Any]: """Evaluate a config file.""" filename = Path(filename) namespace: dict[str, Any] = { '__file__': str(filename), 'tags': tags, } with chdir(filename.parent): # during executing config file, current dir is changed to ``confdir``. try: code = compile(filename.read_bytes(), filename, 'exec') exec(code, namespace) # NoQA: S102 except SyntaxError as err: msg = __('There is a syntax error in your configuration file: %s\n') raise ConfigError(msg % err) from err except SystemExit as exc: msg = __( 'The configuration file (or one of the modules it imports) ' 'called sys.exit()' ) raise ConfigError(msg) from exc except ConfigError: # pass through ConfigError from conf.py as is. It will be shown in console. raise except Exception as exc: msg = __('There is a programmable error in your configuration file:\n\n%s') raise ConfigError(msg % traceback.format_exc()) from exc return namespace def _validate_valid_types( valid_types: type | Collection[type] | ENUM, / ) -> frozenset[type] | ENUM: if not valid_types: return frozenset() if isinstance(valid_types, frozenset | ENUM): return valid_types if isinstance(valid_types, type): return frozenset((valid_types,)) if valid_types is Any: return frozenset({Any}) # type: ignore[arg-type] if isinstance(valid_types, set): return frozenset(valid_types) try: return frozenset(valid_types) except TypeError: logger.warning(__('Failed to convert %r to a frozenset'), valid_types) return frozenset() def convert_source_suffix(app: Sphinx, config: Config) -> None: """Convert 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, str): # 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 = {source_suffix: 'restructuredtext'} logger.info( __('Converting `source_suffix = %r` to `source_suffix = %r`.'), source_suffix, config.source_suffix, ) elif isinstance(source_suffix, list | tuple): # if list, considers as all of them are default filetype config.source_suffix = dict.fromkeys(source_suffix, 'restructuredtext') logger.info( __('Converting `source_suffix = %r` to `source_suffix = %r`.'), source_suffix, config.source_suffix, ) elif not isinstance(source_suffix, dict): msg = __( "The config value `source_suffix' expects a dictionary, " "a string, or a list of strings. Got `%r' instead (type %s)." ) raise ConfigError(msg % (source_suffix, type(source_suffix))) def convert_highlight_options(app: Sphinx, config: Config) -> None: """Convert old styled highlight_options to new styled one. * old style: options * new style: a dict which maps from language name to options """ options = config.highlight_options if options and not all(isinstance(v, dict) for v in options.values()): # old styled option detected because all values are not dictionary. config.highlight_options = {config.highlight_language: options} def init_numfig_format(app: Sphinx, config: 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 def evaluate_copyright_placeholders(_app: Sphinx, config: Config) -> None: """Replace copyright year placeholders (%Y) with the current year.""" replace_yr = str(time.localtime().tm_year) for k in ('copyright', 'epub_copyright'): if k in config: value: str | Sequence[str] = config[k] if isinstance(value, str): if '%Y' in value: config[k] = value.replace('%Y', replace_yr) else: if any('%Y' in line for line in value): items = (line.replace('%Y', replace_yr) for line in value) config[k] = type(value)(items) # type: ignore[call-arg] def correct_copyright_year(_app: Sphinx, config: 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 source_date_epoch := int(getenv('SOURCE_DATE_EPOCH', '0')): source_date_epoch_year = time.gmtime(source_date_epoch).tm_year else: return # If the current year is the replacement year, there's no work to do. # We also skip replacement years that are in the future. current_year = time.localtime().tm_year if current_year <= source_date_epoch_year: return current_yr = str(current_year) replace_yr = str(source_date_epoch_year) for k in ('copyright', 'epub_copyright'): if k in config: value: str | Sequence[str] = config[k] if isinstance(value, str): config[k] = _substitute_copyright_year(value, current_yr, replace_yr) else: items = ( _substitute_copyright_year(x, current_yr, replace_yr) for x in value ) config[k] = type(value)(items) # type: ignore[call-arg] def _substitute_copyright_year( copyright_line: str, current_year: str, replace_year: str ) -> str: """Replace the year in a single copyright line. Legal formats are: * ``YYYY`` * ``YYYY,`` * ``YYYY `` * ``YYYY-YYYY`` * ``YYYY-YYYY,`` * ``YYYY-YYYY `` The final year in the string is replaced with ``replace_year``. """ if len(copyright_line) < 4 or not copyright_line[:4].isdigit(): return copyright_line if copyright_line[:4] == current_year and copyright_line[4:5] in {'', ' ', ','}: return replace_year + copyright_line[4:] if copyright_line[4:5] != '-': return copyright_line if ( copyright_line[5:9].isdigit() and copyright_line[5:9] == current_year and copyright_line[9:10] in {'', ' ', ','} ): return copyright_line[:5] + replace_year + copyright_line[9:] return copyright_line def check_confval_types(app: Sphinx | None, config: 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 name, opt in config._options.items(): default = opt.default valid_types = opt.valid_types value = getattr(config, name) if callable(default): default = default(config) # evaluate default value if default is None and not valid_types: continue # neither inferable nor explicitly annotated types if valid_types == frozenset({Any}): # any type of value is accepted continue if isinstance(valid_types, ENUM): if not valid_types.match(value): msg = __( 'The config value `{name}` has to be a one of {candidates}, ' 'but `{current}` is given.' ) logger.warning( msg.format( name=name, current=value, candidates=valid_types._candidates ), once=True, ) continue type_value = type(value) type_default = type(default) if type_value is type_default: # attempt to infer the type continue if type_value in valid_types: # check explicitly listed types if frozenset in valid_types and type_value in {list, tuple, set}: setattr(config, name, frozenset(value)) elif tuple in valid_types and type_value is list: setattr(config, name, tuple(value)) continue common_bases = {*type_value.__bases__, type_value} & set(type_default.__bases__) common_bases.discard(object) if common_bases: continue # at least we share a non-trivial base class if valid_types: msg = __( "The config value `{name}' has type `{current.__name__}'; " 'expected {permitted}.' ) wrapped_valid_types = sorted(f"`{c.__name__}'" for c in valid_types) if len(wrapped_valid_types) > 2: permitted = ( ', '.join(wrapped_valid_types[:-1]) + f', or {wrapped_valid_types[-1]}' ) else: permitted = ' or '.join(wrapped_valid_types) logger.warning( msg.format(name=name, current=type_value, permitted=permitted), once=True, ) else: msg = __( "The config value `{name}' has type `{current.__name__}', " "defaults to `{default.__name__}'." ) logger.warning( msg.format(name=name, current=type_value, default=type_default), once=True, ) def check_primary_domain(app: Sphinx, config: Config) -> None: primary_domain = config.primary_domain if primary_domain and not app.registry.has_domain(primary_domain): logger.warning(__('primary_domain %r not found, ignored.'), primary_domain) config.primary_domain = None def check_master_doc( app: Sphinx, env: BuildEnvironment, added: Set[str], changed: Set[str], removed: Set[str], ) -> Iterable[str]: """Sphinx 2.0 changed the default from 'contents' to 'index'.""" docnames = app.project.docnames if ( app.config.master_doc == 'index' and 'index' not in docnames and 'contents' in docnames ): logger.warning( __( 'Sphinx now uses "index" as the master document by default. ' 'To keep pre-2.0 behaviour, set "master_doc = \'contents\'".' ) ) app.config.master_doc = 'contents' return changed def setup(app: Sphinx) -> ExtensionMetadata: app.connect('config-inited', convert_source_suffix, priority=800) app.connect('config-inited', convert_highlight_options, priority=800) app.connect('config-inited', init_numfig_format, priority=800) app.connect('config-inited', evaluate_copyright_placeholders, priority=795) app.connect('config-inited', correct_copyright_year, priority=800) app.connect('config-inited', check_confval_types, priority=800) app.connect('config-inited', check_primary_domain, priority=800) app.connect('env-get-outdated', check_master_doc) return { 'version': 'builtin', 'parallel_read_safe': True, 'parallel_write_safe': True, }