diff --git a/.ruff.toml b/.ruff.toml index 5185c7150..2ecfb4d93 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -394,7 +394,6 @@ preview = true quote-style = "single" exclude = [ "sphinx/builders/latex/constants.py", - "sphinx/config.py", "sphinx/domains/__init__.py", "sphinx/domains/c/_parser.py", "sphinx/domains/c/_ids.py", diff --git a/sphinx/config.py b/sphinx/config.py index 24b0ba2cd..0fa0f5b30 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -29,7 +29,11 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) _ConfigRebuild: TypeAlias = Literal[ - '', 'env', 'epub', 'gettext', 'html', + '', + 'env', + 'epub', + 'gettext', + 'html', # sphinxcontrib-applehelp 'applehelp', # sphinxcontrib-devhelp @@ -130,15 +134,35 @@ class _Opt: 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, self.description) + 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, self.description) + 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 @@ -161,7 +185,8 @@ class _Opt: return self.default, self.rebuild, self.valid_types, self.description def __setstate__( - self, state: tuple[Any, _ConfigRebuild, _OptValidTypes, str]) -> None: + self, state: tuple[Any, _ConfigRebuild, _OptValidTypes, str] + ) -> None: default, rebuild, valid_types, description = state super().__setattr__('default', default) super().__setattr__('rebuild', rebuild) @@ -172,7 +197,9 @@ class _Opt: 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) + RemovedInSphinx90Warning, + stacklevel=2, + ) return (self.default, self.rebuild, self.valid_types)[item] @@ -201,35 +228,39 @@ class Config: 'author': _Opt('Author name not set', 'env', ()), 'project_copyright': _Opt('', 'html', frozenset((str, tuple, list))), 'copyright': _Opt( - lambda config: config.project_copyright, 'html', frozenset((str, tuple, list))), + lambda config: config.project_copyright, + 'html', + frozenset((str, tuple, list)), + ), 'version': _Opt('', 'env', ()), 'release': _Opt('', 'env', ()), 'today': _Opt('', 'env', ()), # the real default is locale-dependent 'today_fmt': _Opt(None, 'env', frozenset((str,))), - 'language': _Opt('en', 'env', frozenset((str,))), 'locale_dirs': _Opt(['locales'], 'env', ()), - 'figure_language_filename': _Opt('{root}.{language}{ext}', 'env', frozenset((str,))), + 'figure_language_filename': _Opt( + '{root}.{language}{ext}', 'env', frozenset((str,)) + ), 'gettext_allow_fuzzy_translations': _Opt(False, 'gettext', ()), 'translation_progress_classes': _Opt( - False, 'env', ENUM(True, False, 'translated', 'untranslated')), - + False, 'env', ENUM(True, False, 'translated', 'untranslated') + ), 'master_doc': _Opt('index', 'env', ()), 'root_doc': _Opt(lambda config: config.master_doc, 'env', ()), # ``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_suffix': _Opt({'.rst': 'restructuredtext'}, 'env', Any), # type: ignore[arg-type] 'source_encoding': _Opt('utf-8-sig', 'env', ()), 'exclude_patterns': _Opt([], 'env', frozenset((str,))), - 'include_patterns': _Opt(["**"], 'env', frozenset((str,))), + 'include_patterns': _Opt(['**'], 'env', frozenset((str,))), 'default_role': _Opt(None, 'env', frozenset((str,))), 'add_function_parentheses': _Opt(True, 'env', ()), 'add_module_names': _Opt(True, 'env', ()), 'toc_object_entries': _Opt(True, 'env', frozenset((bool,))), 'toc_object_entries_show_parents': _Opt( - 'domain', 'env', ENUM('domain', 'all', 'hide')), + 'domain', 'env', ENUM('domain', 'all', 'hide') + ), 'trim_footnote_reference_space': _Opt(False, 'env', ()), 'show_authors': _Opt(False, 'env', ()), 'pygments_style': _Opt(None, 'html', frozenset((str,))), @@ -253,9 +284,11 @@ class Config: 'nitpick_ignore_regex': _Opt([], '', frozenset((set, list, tuple))), 'numfig': _Opt(False, 'env', ()), 'numfig_secnum_depth': _Opt(1, 'env', ()), - 'numfig_format': _Opt({}, 'env', ()), # will be initialized in init_numfig_format() + # numfig_format will be initialized in init_numfig_format() + 'numfig_format': _Opt({}, 'env', ()), 'maximum_signature_line_length': _Opt( - None, 'env', frozenset((int, types.NoneType))), + None, 'env', frozenset((int, types.NoneType)) + ), 'math_number_all': _Opt(False, 'env', ()), 'math_eqref_format': _Opt(None, 'env', frozenset((str,))), 'math_numfig': _Opt(True, 'env', ()), @@ -266,12 +299,18 @@ class Config: 'smartquotes': _Opt(True, 'env', ()), 'smartquotes_action': _Opt('qDe', 'env', ()), 'smartquotes_excludes': _Opt( - {'languages': ['ja', 'zh_CN', 'zh_TW'], 'builders': ['man', 'text']}, 'env', ()), + {'languages': ['ja', 'zh_CN', 'zh_TW'], 'builders': ['man', 'text']}, + 'env', + (), + ), 'option_emphasise_placeholders': _Opt(False, 'env', ()), } - def __init__(self, config: dict[str, Any] | None = None, - overrides: dict[str, Any] | None = None) -> None: + 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() @@ -301,24 +340,33 @@ class Config: return self._overrides @classmethod - def read(cls: type[Config], confdir: str | os.PathLike[str], overrides: dict | None = None, - tags: Tags | None = None) -> Config: + def read( + cls: type[Config], + confdir: str | os.PathLike[str], + overrides: dict | 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) + 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" + 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) @@ -328,9 +376,11 @@ class Config: valid_types = opt.valid_types if valid_types == Any: return value - if (type(default) is bool - or (not isinstance(valid_types, ENUM) - and len(valid_types) == 1 and bool in valid_types)): + if type(default) is bool or ( + not isinstance(valid_types, ENUM) + and len(valid_types) == 1 + and bool in valid_types + ): if isinstance(valid_types, ENUM) or len(valid_types) > 1: # if valid_types are given, and non-bool valid types exist, # return the value without coercing to a Boolean. @@ -338,23 +388,31 @@ class Config: # given falsy string from a command line option return value not in {'0', ''} if isinstance(default, dict): - raise ValueError(__('cannot override dictionary config setting %r, ' - 'ignoring (use %r to set individual elements)') % - (name, f'{name}.key=value')) + raise ValueError( + __( + '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 + 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) + raise ValueError( + __('cannot override config setting %r with unsupported type, ignoring') + % name + ) @staticmethod def pre_init_values() -> None: @@ -374,7 +432,9 @@ class Config: 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) + logger.warning( + __('unknown config value %r in override, ignoring'), name + ) def __repr__(self) -> str: values = [] @@ -383,7 +443,7 @@ class Config: opt_value = getattr(self, opt_name) except Exception: opt_value = '' - values.append(f"{opt_name}={opt_value!r}") + values.append(f'{opt_name}={opt_value!r}') return self.__class__.__qualname__ + '(' + ', '.join(values) + ')' def __setattr__(self, key: str, value: object) -> None: @@ -409,7 +469,7 @@ class Config: try: value = self.convert_overrides(name, value) except ValueError as exc: - logger.warning("%s", exc) + logger.warning('%s', exc) else: self.__setattr__(name, value) return value @@ -446,9 +506,14 @@ class Config: 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: + 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) @@ -486,8 +551,10 @@ class Config: # will always mark the config value as changed, # and thus always invalidate the cache and perform a rebuild. logger.warning( - __('cannot cache unpickable configuration value: %r ' - '(because it contains a function, class, or module object)'), + __( + 'cannot cache unpickable configuration value: %r ' + '(because it contains a function, class, or module object)' + ), name, type='config', subtype='cache', @@ -510,7 +577,9 @@ class Config: self.__dict__.update(state) -def eval_config_file(filename: str | os.PathLike[str], tags: Tags | None) -> dict[str, Any]: +def eval_config_file( + filename: str | os.PathLike[str], tags: Tags | None +) -> dict[str, Any]: """Evaluate a config file.""" filename = Path(filename) @@ -524,24 +593,26 @@ def eval_config_file(filename: str | os.PathLike[str], tags: Tags | None) -> dic 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") + 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()") + 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") + 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, /, + valid_types: type | Collection[type] | ENUM, / ) -> tuple[()] | tuple[type, ...] | frozenset[type] | ENUM: if not valid_types: return () @@ -578,16 +649,24 @@ def convert_source_suffix(app: Sphinx, config: Config) -> 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) + 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) + 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).") + 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))) @@ -605,10 +684,12 @@ def convert_highlight_options(app: Sphinx, config: Config) -> None: 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')} + 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) @@ -715,10 +796,14 @@ def check_confval_types(app: Sphinx | None, config: Config) -> None: 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.") + 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), + msg.format( + name=name, current=value, candidates=valid_types.candidates + ), once=True, ) continue @@ -732,28 +817,33 @@ def check_confval_types(app: Sphinx | None, config: Config) -> None: if type_value in valid_types: # check explicitly listed types continue - common_bases = ({*type_value.__bases__, type_value} - & set(type_default.__bases__)) + 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}.") + 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]}") + permitted = ( + ', '.join(wrapped_valid_types[:-1]) + + f', or {wrapped_valid_types[-1]}' + ) else: - permitted = " or ".join(wrapped_valid_types) + 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__}'.") + 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, @@ -767,17 +857,27 @@ def check_primary_domain(app: Sphinx, config: Config) -> None: config.primary_domain = None -def check_root_doc(app: Sphinx, env: BuildEnvironment, added: Set[str], - changed: Set[str], removed: Set[str]) -> Iterable[str]: - """Adjust root_doc to 'contents' to support an old project which does not have - any root_doc setting. - """ - if (app.config.root_doc == 'index' and - 'index' not in app.project.docnames and - 'contents' in app.project.docnames): - logger.warning(__('Since v2.0, Sphinx uses "index" as root_doc by default. ' - 'Please add "root_doc = \'contents\'" to your conf.py.')) - app.config.root_doc = "contents" +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 @@ -790,7 +890,7 @@ def setup(app: Sphinx) -> ExtensionMetadata: 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_root_doc) + app.connect('env-get-outdated', check_master_doc) return { 'version': 'builtin',