Enable automatic formatting for `sphinx/config.py`

This commit is contained in:
Adam Turner 2024-12-12 00:19:12 +00:00
parent 6e4c4f11eb
commit 9cb93aeb01
2 changed files with 188 additions and 89 deletions

View File

@ -394,7 +394,6 @@ preview = true
quote-style = "single" quote-style = "single"
exclude = [ exclude = [
"sphinx/builders/latex/constants.py", "sphinx/builders/latex/constants.py",
"sphinx/config.py",
"sphinx/domains/__init__.py", "sphinx/domains/__init__.py",
"sphinx/domains/c/_parser.py", "sphinx/domains/c/_parser.py",
"sphinx/domains/c/_ids.py", "sphinx/domains/c/_ids.py",

View File

@ -29,7 +29,11 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ConfigRebuild: TypeAlias = Literal[ _ConfigRebuild: TypeAlias = Literal[
'', 'env', 'epub', 'gettext', 'html', '',
'env',
'epub',
'gettext',
'html',
# sphinxcontrib-applehelp # sphinxcontrib-applehelp
'applehelp', 'applehelp',
# sphinxcontrib-devhelp # sphinxcontrib-devhelp
@ -130,15 +134,35 @@ class _Opt:
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
if isinstance(other, _Opt): if isinstance(other, _Opt):
self_tpl = (self.default, self.rebuild, self.valid_types, self.description) self_tpl = (
other_tpl = (other.default, other.rebuild, other.valid_types, self.description) 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 self_tpl == other_tpl
return NotImplemented return NotImplemented
def __lt__(self, other: _Opt) -> bool: def __lt__(self, other: _Opt) -> bool:
if self.__class__ is other.__class__: if self.__class__ is other.__class__:
self_tpl = (self.default, self.rebuild, self.valid_types, self.description) self_tpl = (
other_tpl = (other.default, other.rebuild, other.valid_types, self.description) 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 self_tpl > other_tpl
return NotImplemented return NotImplemented
@ -161,7 +185,8 @@ class _Opt:
return self.default, self.rebuild, self.valid_types, self.description return self.default, self.rebuild, self.valid_types, self.description
def __setstate__( def __setstate__(
self, state: tuple[Any, _ConfigRebuild, _OptValidTypes, str]) -> None: self, state: tuple[Any, _ConfigRebuild, _OptValidTypes, str]
) -> None:
default, rebuild, valid_types, description = state default, rebuild, valid_types, description = state
super().__setattr__('default', default) super().__setattr__('default', default)
super().__setattr__('rebuild', rebuild) super().__setattr__('rebuild', rebuild)
@ -172,7 +197,9 @@ class _Opt:
warnings.warn( warnings.warn(
f'The {self.__class__.__name__!r} object tuple interface is deprecated, ' f'The {self.__class__.__name__!r} object tuple interface is deprecated, '
"use attribute access instead for 'default', 'rebuild', and 'valid_types'.", "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] return (self.default, self.rebuild, self.valid_types)[item]
@ -201,35 +228,39 @@ class Config:
'author': _Opt('Author name not set', 'env', ()), 'author': _Opt('Author name not set', 'env', ()),
'project_copyright': _Opt('', 'html', frozenset((str, tuple, list))), 'project_copyright': _Opt('', 'html', frozenset((str, tuple, list))),
'copyright': _Opt( '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', ()), 'version': _Opt('', 'env', ()),
'release': _Opt('', 'env', ()), 'release': _Opt('', 'env', ()),
'today': _Opt('', 'env', ()), 'today': _Opt('', 'env', ()),
# the real default is locale-dependent # the real default is locale-dependent
'today_fmt': _Opt(None, 'env', frozenset((str,))), 'today_fmt': _Opt(None, 'env', frozenset((str,))),
'language': _Opt('en', 'env', frozenset((str,))), 'language': _Opt('en', 'env', frozenset((str,))),
'locale_dirs': _Opt(['locales'], 'env', ()), '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', ()), 'gettext_allow_fuzzy_translations': _Opt(False, 'gettext', ()),
'translation_progress_classes': _Opt( 'translation_progress_classes': _Opt(
False, 'env', ENUM(True, False, 'translated', 'untranslated')), False, 'env', ENUM(True, False, 'translated', 'untranslated')
),
'master_doc': _Opt('index', 'env', ()), 'master_doc': _Opt('index', 'env', ()),
'root_doc': _Opt(lambda config: config.master_doc, 'env', ()), 'root_doc': _Opt(lambda config: config.master_doc, 'env', ()),
# ``source_suffix`` type is actually ``dict[str, str | None]``: # ``source_suffix`` type is actually ``dict[str, str | None]``:
# see ``convert_source_suffix()`` below. # see ``convert_source_suffix()`` below.
'source_suffix': _Opt( 'source_suffix': _Opt({'.rst': 'restructuredtext'}, 'env', Any), # type: ignore[arg-type]
{'.rst': 'restructuredtext'}, 'env', Any), # type: ignore[arg-type]
'source_encoding': _Opt('utf-8-sig', 'env', ()), 'source_encoding': _Opt('utf-8-sig', 'env', ()),
'exclude_patterns': _Opt([], 'env', frozenset((str,))), '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,))), 'default_role': _Opt(None, 'env', frozenset((str,))),
'add_function_parentheses': _Opt(True, 'env', ()), 'add_function_parentheses': _Opt(True, 'env', ()),
'add_module_names': _Opt(True, 'env', ()), 'add_module_names': _Opt(True, 'env', ()),
'toc_object_entries': _Opt(True, 'env', frozenset((bool,))), 'toc_object_entries': _Opt(True, 'env', frozenset((bool,))),
'toc_object_entries_show_parents': _Opt( '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', ()), 'trim_footnote_reference_space': _Opt(False, 'env', ()),
'show_authors': _Opt(False, 'env', ()), 'show_authors': _Opt(False, 'env', ()),
'pygments_style': _Opt(None, 'html', frozenset((str,))), 'pygments_style': _Opt(None, 'html', frozenset((str,))),
@ -253,9 +284,11 @@ class Config:
'nitpick_ignore_regex': _Opt([], '', frozenset((set, list, tuple))), 'nitpick_ignore_regex': _Opt([], '', frozenset((set, list, tuple))),
'numfig': _Opt(False, 'env', ()), 'numfig': _Opt(False, 'env', ()),
'numfig_secnum_depth': _Opt(1, '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( 'maximum_signature_line_length': _Opt(
None, 'env', frozenset((int, types.NoneType))), None, 'env', frozenset((int, types.NoneType))
),
'math_number_all': _Opt(False, 'env', ()), 'math_number_all': _Opt(False, 'env', ()),
'math_eqref_format': _Opt(None, 'env', frozenset((str,))), 'math_eqref_format': _Opt(None, 'env', frozenset((str,))),
'math_numfig': _Opt(True, 'env', ()), 'math_numfig': _Opt(True, 'env', ()),
@ -266,12 +299,18 @@ class Config:
'smartquotes': _Opt(True, 'env', ()), 'smartquotes': _Opt(True, 'env', ()),
'smartquotes_action': _Opt('qDe', 'env', ()), 'smartquotes_action': _Opt('qDe', 'env', ()),
'smartquotes_excludes': _Opt( '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', ()), 'option_emphasise_placeholders': _Opt(False, 'env', ()),
} }
def __init__(self, config: dict[str, Any] | None = None, def __init__(
overrides: dict[str, Any] | None = None) -> None: self,
config: dict[str, Any] | None = None,
overrides: dict[str, Any] | None = None,
) -> None:
raw_config: dict[str, Any] = config or {} raw_config: dict[str, Any] = config or {}
self._overrides = dict(overrides) if overrides is not None else {} self._overrides = dict(overrides) if overrides is not None else {}
self._options = Config.config_values.copy() self._options = Config.config_values.copy()
@ -301,24 +340,33 @@ class Config:
return self._overrides return self._overrides
@classmethod @classmethod
def read(cls: type[Config], confdir: str | os.PathLike[str], overrides: dict | None = None, def read(
tags: Tags | None = None) -> Config: cls: type[Config],
confdir: str | os.PathLike[str],
overrides: dict | None = None,
tags: Tags | None = None,
) -> Config:
"""Create a Config object from configuration file.""" """Create a Config object from configuration file."""
filename = Path(confdir, CONFIG_FILENAME) filename = Path(confdir, CONFIG_FILENAME)
if not filename.is_file(): if not filename.is_file():
raise ConfigError(__("config directory doesn't contain a conf.py file (%s)") % raise ConfigError(
confdir) __("config directory doesn't contain a conf.py file (%s)") % confdir
)
namespace = eval_config_file(filename, tags) namespace = eval_config_file(filename, tags)
# Note: Old sphinx projects have been configured as "language = None" because # Note: Old sphinx projects have been configured as "language = None" because
# sphinx-quickstart previously generated this by default. # sphinx-quickstart previously generated this by default.
# To keep compatibility, they should be fallback to 'en' for a while # To keep compatibility, they should be fallback to 'en' for a while
# (This conversion should not be removed before 2025-01-01). # (This conversion should not be removed before 2025-01-01).
if namespace.get("language", ...) is None: if namespace.get('language', ...) is None:
logger.warning(__("Invalid configuration value found: 'language = None'. " logger.warning(
"Update your configuration to a valid language code. " __(
"Falling back to 'en' (English).")) "Invalid configuration value found: 'language = None'. "
namespace["language"] = "en" 'Update your configuration to a valid language code. '
"Falling back to 'en' (English)."
)
)
namespace['language'] = 'en'
return cls(namespace, overrides) return cls(namespace, overrides)
@ -328,9 +376,11 @@ class Config:
valid_types = opt.valid_types valid_types = opt.valid_types
if valid_types == Any: if valid_types == Any:
return value return value
if (type(default) is bool if type(default) is bool or (
or (not isinstance(valid_types, ENUM) not isinstance(valid_types, ENUM)
and len(valid_types) == 1 and bool in valid_types)): and len(valid_types) == 1
and bool in valid_types
):
if isinstance(valid_types, ENUM) or len(valid_types) > 1: if isinstance(valid_types, ENUM) or len(valid_types) > 1:
# if valid_types are given, and non-bool valid types exist, # if valid_types are given, and non-bool valid types exist,
# return the value without coercing to a Boolean. # return the value without coercing to a Boolean.
@ -338,23 +388,31 @@ class Config:
# given falsy string from a command line option # given falsy string from a command line option
return value not in {'0', ''} return value not in {'0', ''}
if isinstance(default, dict): if isinstance(default, dict):
raise ValueError(__('cannot override dictionary config setting %r, ' raise ValueError(
'ignoring (use %r to set individual elements)') % __(
(name, f'{name}.key=value')) 'cannot override dictionary config setting %r, '
'ignoring (use %r to set individual elements)'
)
% (name, f'{name}.key=value')
)
if isinstance(default, list): if isinstance(default, list):
return value.split(',') return value.split(',')
if isinstance(default, int): if isinstance(default, int):
try: try:
return int(value) return int(value)
except ValueError as exc: except ValueError as exc:
raise ValueError(__('invalid number %r for config value %r, ignoring') % raise ValueError(
(value, name)) from exc __('invalid number %r for config value %r, ignoring')
% (value, name)
) from exc
if callable(default): if callable(default):
return value return value
if isinstance(default, str) or default is None: if isinstance(default, str) or default is None:
return value return value
raise ValueError(__('cannot override config setting %r with unsupported ' raise ValueError(
'type, ignoring') % name) __('cannot override config setting %r with unsupported type, ignoring')
% name
)
@staticmethod @staticmethod
def pre_init_values() -> None: def pre_init_values() -> None:
@ -374,7 +432,9 @@ class Config:
def _report_override_warnings(self) -> None: def _report_override_warnings(self) -> None:
for name in self._overrides: for name in self._overrides:
if name not in self._options: 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: def __repr__(self) -> str:
values = [] values = []
@ -383,7 +443,7 @@ class Config:
opt_value = getattr(self, opt_name) opt_value = getattr(self, opt_name)
except Exception: except Exception:
opt_value = '<error!>' opt_value = '<error!>'
values.append(f"{opt_name}={opt_value!r}") values.append(f'{opt_name}={opt_value!r}')
return self.__class__.__qualname__ + '(' + ', '.join(values) + ')' return self.__class__.__qualname__ + '(' + ', '.join(values) + ')'
def __setattr__(self, key: str, value: object) -> None: def __setattr__(self, key: str, value: object) -> None:
@ -409,7 +469,7 @@ class Config:
try: try:
value = self.convert_overrides(name, value) value = self.convert_overrides(name, value)
except ValueError as exc: except ValueError as exc:
logger.warning("%s", exc) logger.warning('%s', exc)
else: else:
self.__setattr__(name, value) self.__setattr__(name, value)
return value return value
@ -446,9 +506,14 @@ class Config:
for name, opt in self._options.items(): for name, opt in self._options.items():
yield ConfigValue(name, getattr(self, name), opt.rebuild) yield ConfigValue(name, getattr(self, name), opt.rebuild)
def add(self, name: str, default: Any, rebuild: _ConfigRebuild, def add(
types: type | Collection[type] | ENUM, self,
description: str = '') -> None: name: str,
default: Any,
rebuild: _ConfigRebuild,
types: type | Collection[type] | ENUM,
description: str = '',
) -> None:
if name in self._options: if name in self._options:
raise ExtensionError(__('Config value %r already present') % name) raise ExtensionError(__('Config value %r already present') % name)
@ -486,8 +551,10 @@ class Config:
# will always mark the config value as changed, # will always mark the config value as changed,
# and thus always invalidate the cache and perform a rebuild. # and thus always invalidate the cache and perform a rebuild.
logger.warning( 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, name,
type='config', type='config',
subtype='cache', subtype='cache',
@ -510,7 +577,9 @@ class Config:
self.__dict__.update(state) 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.""" """Evaluate a config file."""
filename = Path(filename) 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') code = compile(filename.read_bytes(), filename, 'exec')
exec(code, namespace) # NoQA: S102 exec(code, namespace) # NoQA: S102
except SyntaxError as err: 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 raise ConfigError(msg % err) from err
except SystemExit as exc: except SystemExit as exc:
msg = __("The configuration file (or one of the modules it imports) " msg = __(
"called sys.exit()") 'The configuration file (or one of the modules it imports) '
'called sys.exit()'
)
raise ConfigError(msg) from exc raise ConfigError(msg) from exc
except ConfigError: except ConfigError:
# pass through ConfigError from conf.py as is. It will be shown in console. # pass through ConfigError from conf.py as is. It will be shown in console.
raise raise
except Exception as exc: 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 raise ConfigError(msg % traceback.format_exc()) from exc
return namespace return namespace
def _validate_valid_types( def _validate_valid_types(
valid_types: type | Collection[type] | ENUM, /, valid_types: type | Collection[type] | ENUM, /
) -> tuple[()] | tuple[type, ...] | frozenset[type] | ENUM: ) -> tuple[()] | tuple[type, ...] | frozenset[type] | ENUM:
if not valid_types: if not valid_types:
return () return ()
@ -578,16 +649,24 @@ def convert_source_suffix(app: Sphinx, config: Config) -> None:
# The default filetype is determined on later step. # The default filetype is determined on later step.
# By default, it is considered as restructuredtext. # By default, it is considered as restructuredtext.
config.source_suffix = {source_suffix: 'restructuredtext'} config.source_suffix = {source_suffix: 'restructuredtext'}
logger.info(__("Converting `source_suffix = %r` to `source_suffix = %r`."), logger.info(
source_suffix, config.source_suffix) __('Converting `source_suffix = %r` to `source_suffix = %r`.'),
source_suffix,
config.source_suffix,
)
elif isinstance(source_suffix, list | tuple): elif isinstance(source_suffix, list | tuple):
# if list, considers as all of them are default filetype # if list, considers as all of them are default filetype
config.source_suffix = dict.fromkeys(source_suffix, 'restructuredtext') config.source_suffix = dict.fromkeys(source_suffix, 'restructuredtext')
logger.info(__("Converting `source_suffix = %r` to `source_suffix = %r`."), logger.info(
source_suffix, config.source_suffix) __('Converting `source_suffix = %r` to `source_suffix = %r`.'),
source_suffix,
config.source_suffix,
)
elif not isinstance(source_suffix, dict): elif not isinstance(source_suffix, dict):
msg = __("The config value `source_suffix' expects a dictionary, " msg = __(
"a string, or a list of strings. Got `%r' instead (type %s).") "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))) 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: def init_numfig_format(app: Sphinx, config: Config) -> None:
"""Initialize :confval:`numfig_format`.""" """Initialize :confval:`numfig_format`."""
numfig_format = {'section': _('Section %s'), numfig_format = {
'figure': _('Fig. %s'), 'section': _('Section %s'),
'table': _('Table %s'), 'figure': _('Fig. %s'),
'code-block': _('Listing %s')} 'table': _('Table %s'),
'code-block': _('Listing %s'),
}
# override default labels by configuration # override default labels by configuration
numfig_format.update(config.numfig_format) 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 isinstance(valid_types, ENUM):
if not valid_types.match(value): if not valid_types.match(value):
msg = __("The config value `{name}` has to be a one of {candidates}, " msg = __(
"but `{current}` is given.") 'The config value `{name}` has to be a one of {candidates}, '
'but `{current}` is given.'
)
logger.warning( logger.warning(
msg.format(name=name, current=value, candidates=valid_types.candidates), msg.format(
name=name, current=value, candidates=valid_types.candidates
),
once=True, once=True,
) )
continue 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 if type_value in valid_types: # check explicitly listed types
continue continue
common_bases = ({*type_value.__bases__, type_value} common_bases = {*type_value.__bases__, type_value} & set(type_default.__bases__)
& set(type_default.__bases__))
common_bases.discard(object) common_bases.discard(object)
if common_bases: if common_bases:
continue # at least we share a non-trivial base class continue # at least we share a non-trivial base class
if valid_types: if valid_types:
msg = __("The config value `{name}' has type `{current.__name__}'; " msg = __(
"expected {permitted}.") "The config value `{name}' has type `{current.__name__}'; "
'expected {permitted}.'
)
wrapped_valid_types = sorted(f"`{c.__name__}'" for c in valid_types) wrapped_valid_types = sorted(f"`{c.__name__}'" for c in valid_types)
if len(wrapped_valid_types) > 2: if len(wrapped_valid_types) > 2:
permitted = (", ".join(wrapped_valid_types[:-1]) permitted = (
+ f", or {wrapped_valid_types[-1]}") ', '.join(wrapped_valid_types[:-1])
+ f', or {wrapped_valid_types[-1]}'
)
else: else:
permitted = " or ".join(wrapped_valid_types) permitted = ' or '.join(wrapped_valid_types)
logger.warning( logger.warning(
msg.format(name=name, current=type_value, permitted=permitted), msg.format(name=name, current=type_value, permitted=permitted),
once=True, once=True,
) )
else: else:
msg = __("The config value `{name}' has type `{current.__name__}', " msg = __(
"defaults to `{default.__name__}'.") "The config value `{name}' has type `{current.__name__}', "
"defaults to `{default.__name__}'."
)
logger.warning( logger.warning(
msg.format(name=name, current=type_value, default=type_default), msg.format(name=name, current=type_value, default=type_default),
once=True, once=True,
@ -767,17 +857,27 @@ def check_primary_domain(app: Sphinx, config: Config) -> None:
config.primary_domain = None config.primary_domain = None
def check_root_doc(app: Sphinx, env: BuildEnvironment, added: Set[str], def check_master_doc(
changed: Set[str], removed: Set[str]) -> Iterable[str]: app: Sphinx,
"""Adjust root_doc to 'contents' to support an old project which does not have env: BuildEnvironment,
any root_doc setting. added: Set[str],
""" changed: Set[str],
if (app.config.root_doc == 'index' and removed: Set[str],
'index' not in app.project.docnames and ) -> Iterable[str]:
'contents' in app.project.docnames): """Sphinx 2.0 changed the default from 'contents' to 'index'."""
logger.warning(__('Since v2.0, Sphinx uses "index" as root_doc by default. ' docnames = app.project.docnames
'Please add "root_doc = \'contents\'" to your conf.py.')) if (
app.config.root_doc = "contents" 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 return changed
@ -790,7 +890,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
app.connect('config-inited', correct_copyright_year, priority=800) app.connect('config-inited', correct_copyright_year, priority=800)
app.connect('config-inited', check_confval_types, priority=800) app.connect('config-inited', check_confval_types, priority=800)
app.connect('config-inited', check_primary_domain, 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 { return {
'version': 'builtin', 'version': 'builtin',