mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Introduce a configuration option type
* Normalise rebuild values * Ensure that the tuple interface continues to work
This commit is contained in:
252
sphinx/config.py
252
sphinx/config.py
@@ -6,9 +6,11 @@ import sys
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
|
import warnings
|
||||||
from os import getenv, path
|
from os import getenv, path
|
||||||
from typing import TYPE_CHECKING, Any, Literal, NamedTuple
|
from typing import TYPE_CHECKING, Any, Literal, NamedTuple
|
||||||
|
|
||||||
|
from sphinx.deprecation import RemovedInSphinx90Warning
|
||||||
from sphinx.errors import ConfigError, ExtensionError
|
from sphinx.errors import ConfigError, ExtensionError
|
||||||
from sphinx.locale import _, __
|
from sphinx.locale import _, __
|
||||||
from sphinx.util import logging
|
from sphinx.util import logging
|
||||||
@@ -73,6 +75,85 @@ class ENUM:
|
|||||||
return value in self.candidates
|
return value in self.candidates
|
||||||
|
|
||||||
|
|
||||||
|
class _Opt:
|
||||||
|
__slots__ = 'default', 'rebuild', 'valid_types'
|
||||||
|
|
||||||
|
default: Any
|
||||||
|
rebuild: _ConfigRebuild
|
||||||
|
valid_types: Sequence[type] | ENUM | Any
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
default: Any,
|
||||||
|
rebuild: _ConfigRebuild,
|
||||||
|
valid_types: Sequence[type] | ENUM | Any,
|
||||||
|
) -> 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)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
f'{self.__class__.__qualname__}('
|
||||||
|
f'default={self.default!r}, '
|
||||||
|
f'rebuild={self.rebuild!r}, '
|
||||||
|
f'valid_types={self.valid_types!r})'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if self.__class__ is other.__class__:
|
||||||
|
self_tpl = (self.default, self.rebuild, self.valid_types)
|
||||||
|
other_tpl = (other.default, other.rebuild, other.valid_types)
|
||||||
|
return self_tpl == other_tpl
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if self.__class__ is other.__class__:
|
||||||
|
self_tpl = (self.default, self.rebuild, self.valid_types)
|
||||||
|
other_tpl = (other.default, other.rebuild, other.valid_types)
|
||||||
|
return self_tpl > other_tpl
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.default, self.rebuild, self.valid_types))
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
if key in {'default', 'rebuild', 'valid_types'}:
|
||||||
|
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):
|
||||||
|
if key in {'default', 'rebuild', 'valid_types'}:
|
||||||
|
msg = f'{self.__class__.__name__!r} object does not support deletion of {key!r}'
|
||||||
|
raise TypeError(msg)
|
||||||
|
super().__delattr__(key)
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
return self.default, self.rebuild, self.valid_types
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
default, rebuild, valid_types = state
|
||||||
|
super().__setattr__('default', default)
|
||||||
|
super().__setattr__('rebuild', rebuild)
|
||||||
|
super().__setattr__('valid_types', valid_types)
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
r"""Configuration file abstraction.
|
r"""Configuration file abstraction.
|
||||||
|
|
||||||
@@ -92,73 +173,72 @@ class Config:
|
|||||||
|
|
||||||
# If you add a value here, remember to include it in the docs!
|
# If you add a value here, remember to include it in the docs!
|
||||||
|
|
||||||
config_values: dict[str, tuple[Any, _ConfigRebuild, Sequence[type] | ENUM | Any]] = {
|
config_values: dict[str, _Opt] = {
|
||||||
# general options
|
# general options
|
||||||
'project': ('Python', 'env', []),
|
'project': _Opt('Python', 'env', []),
|
||||||
'author': ('unknown', 'env', []),
|
'author': _Opt('unknown', 'env', []),
|
||||||
'project_copyright': ('', 'html', [str, tuple, list]),
|
'project_copyright': _Opt('', 'html', [str, tuple, list]),
|
||||||
'copyright': (lambda c: c.project_copyright, 'html', [str, tuple, list]),
|
'copyright': _Opt(lambda c: c.project_copyright, 'html', [str, tuple, list]),
|
||||||
'version': ('', 'env', []),
|
'version': _Opt('', 'env', []),
|
||||||
'release': ('', 'env', []),
|
'release': _Opt('', 'env', []),
|
||||||
'today': ('', 'env', []),
|
'today': _Opt('', 'env', []),
|
||||||
# the real default is locale-dependent
|
# the real default is locale-dependent
|
||||||
'today_fmt': (None, 'env', [str]),
|
'today_fmt': _Opt(None, 'env', [str]),
|
||||||
|
|
||||||
'language': ('en', 'env', [str]),
|
'language': _Opt('en', 'env', [str]),
|
||||||
'locale_dirs': (['locales'], 'env', []),
|
'locale_dirs': _Opt(['locales'], 'env', []),
|
||||||
'figure_language_filename': ('{root}.{language}{ext}', 'env', [str]),
|
'figure_language_filename': _Opt('{root}.{language}{ext}', 'env', [str]),
|
||||||
'gettext_allow_fuzzy_translations': (False, 'gettext', []),
|
'gettext_allow_fuzzy_translations': _Opt(False, 'gettext', []),
|
||||||
'translation_progress_classes': (False, 'env',
|
'translation_progress_classes': _Opt(
|
||||||
ENUM(True, False, 'translated', 'untranslated')),
|
False, 'env', ENUM(True, False, 'translated', 'untranslated')),
|
||||||
|
|
||||||
'master_doc': ('index', 'env', []),
|
'master_doc': _Opt('index', 'env', []),
|
||||||
'root_doc': (lambda config: config.master_doc, 'env', []),
|
'root_doc': _Opt(lambda config: config.master_doc, 'env', []),
|
||||||
'source_suffix': ({'.rst': 'restructuredtext'}, 'env', Any),
|
'source_suffix': _Opt({'.rst': 'restructuredtext'}, 'env', Any),
|
||||||
'source_encoding': ('utf-8-sig', 'env', []),
|
'source_encoding': _Opt('utf-8-sig', 'env', []),
|
||||||
'exclude_patterns': ([], 'env', [str]),
|
'exclude_patterns': _Opt([], 'env', [str]),
|
||||||
'include_patterns': (["**"], 'env', [str]),
|
'include_patterns': _Opt(["**"], 'env', [str]),
|
||||||
'default_role': (None, 'env', [str]),
|
'default_role': _Opt(None, 'env', [str]),
|
||||||
'add_function_parentheses': (True, 'env', []),
|
'add_function_parentheses': _Opt(True, 'env', []),
|
||||||
'add_module_names': (True, 'env', []),
|
'add_module_names': _Opt(True, 'env', []),
|
||||||
'toc_object_entries': (True, 'env', [bool]),
|
'toc_object_entries': _Opt(True, 'env', [bool]),
|
||||||
'toc_object_entries_show_parents': ('domain', 'env',
|
'toc_object_entries_show_parents': _Opt(
|
||||||
ENUM('domain', 'all', 'hide')),
|
'domain', 'env', ENUM('domain', 'all', 'hide')),
|
||||||
'trim_footnote_reference_space': (False, 'env', []),
|
'trim_footnote_reference_space': _Opt(False, 'env', []),
|
||||||
'show_authors': (False, 'env', []),
|
'show_authors': _Opt(False, 'env', []),
|
||||||
'pygments_style': (None, 'html', [str]),
|
'pygments_style': _Opt(None, 'html', [str]),
|
||||||
'highlight_language': ('default', 'env', []),
|
'highlight_language': _Opt('default', 'env', []),
|
||||||
'highlight_options': ({}, 'env', []),
|
'highlight_options': _Opt({}, 'env', []),
|
||||||
'templates_path': ([], 'html', []),
|
'templates_path': _Opt([], 'html', []),
|
||||||
'template_bridge': (None, 'html', [str]),
|
'template_bridge': _Opt(None, 'html', [str]),
|
||||||
'keep_warnings': (False, 'env', []),
|
'keep_warnings': _Opt(False, 'env', []),
|
||||||
'suppress_warnings': ([], 'env', []),
|
'suppress_warnings': _Opt([], 'env', []),
|
||||||
'modindex_common_prefix': ([], 'html', []),
|
'modindex_common_prefix': _Opt([], 'html', []),
|
||||||
'rst_epilog': (None, 'env', [str]),
|
'rst_epilog': _Opt(None, 'env', [str]),
|
||||||
'rst_prolog': (None, 'env', [str]),
|
'rst_prolog': _Opt(None, 'env', [str]),
|
||||||
'trim_doctest_flags': (True, 'env', []),
|
'trim_doctest_flags': _Opt(True, 'env', []),
|
||||||
'primary_domain': ('py', 'env', [NoneType]),
|
'primary_domain': _Opt('py', 'env', [NoneType]),
|
||||||
'needs_sphinx': (None, '', [str]),
|
'needs_sphinx': _Opt(None, '', [str]),
|
||||||
'needs_extensions': ({}, '', []),
|
'needs_extensions': _Opt({}, '', []),
|
||||||
'manpages_url': (None, 'env', []),
|
'manpages_url': _Opt(None, 'env', []),
|
||||||
'nitpicky': (False, '', []),
|
'nitpicky': _Opt(False, '', []),
|
||||||
'nitpick_ignore': ([], '', [set, list, tuple]),
|
'nitpick_ignore': _Opt([], '', [set, list, tuple]),
|
||||||
'nitpick_ignore_regex': ([], '', [set, list, tuple]),
|
'nitpick_ignore_regex': _Opt([], '', [set, list, tuple]),
|
||||||
'numfig': (False, 'env', []),
|
'numfig': _Opt(False, 'env', []),
|
||||||
'numfig_secnum_depth': (1, 'env', []),
|
'numfig_secnum_depth': _Opt(1, 'env', []),
|
||||||
'numfig_format': ({}, 'env', []), # will be initialized in init_numfig_format()
|
'numfig_format': _Opt({}, 'env', []), # will be initialized in init_numfig_format()
|
||||||
'maximum_signature_line_length': (None, 'env', {int, None}),
|
'maximum_signature_line_length': _Opt(None, 'env', {int, None}),
|
||||||
'math_number_all': (False, 'env', []),
|
'math_number_all': _Opt(False, 'env', []),
|
||||||
'math_eqref_format': (None, 'env', [str]),
|
'math_eqref_format': _Opt(None, 'env', [str]),
|
||||||
'math_numfig': (True, 'env', []),
|
'math_numfig': _Opt(True, 'env', []),
|
||||||
'tls_verify': (True, 'env', []),
|
'tls_verify': _Opt(True, 'env', []),
|
||||||
'tls_cacerts': (None, 'env', []),
|
'tls_cacerts': _Opt(None, 'env', []),
|
||||||
'user_agent': (None, 'env', [str]),
|
'user_agent': _Opt(None, 'env', [str]),
|
||||||
'smartquotes': (True, 'env', []),
|
'smartquotes': _Opt(True, 'env', []),
|
||||||
'smartquotes_action': ('qDe', 'env', []),
|
'smartquotes_action': _Opt('qDe', 'env', []),
|
||||||
'smartquotes_excludes': ({'languages': ['ja'],
|
'smartquotes_excludes': _Opt(
|
||||||
'builders': ['man', 'text']},
|
{'languages': ['ja'], 'builders': ['man', 'text']}, 'env', []),
|
||||||
'env', []),
|
'option_emphasise_placeholders': _Opt(False, 'env', []),
|
||||||
'option_emphasise_placeholders': (False, 'env', []),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, config: dict[str, Any] | None = None,
|
def __init__(self, config: dict[str, Any] | None = None,
|
||||||
@@ -203,7 +283,9 @@ class Config:
|
|||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
default, _rebuild, valid_types = self.values[name]
|
opt = self.values[name]
|
||||||
|
default = opt.default
|
||||||
|
valid_types = opt.valid_types
|
||||||
if valid_types == Any:
|
if valid_types == Any:
|
||||||
return value
|
return value
|
||||||
elif valid_types == {bool, str}:
|
elif valid_types == {bool, str}:
|
||||||
@@ -289,7 +371,7 @@ class Config:
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
values = []
|
values = []
|
||||||
for opt_name in self._options:
|
for opt_name in self.values:
|
||||||
try:
|
try:
|
||||||
opt_value = getattr(self, opt_name)
|
opt_value = getattr(self, opt_name)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -302,7 +384,7 @@ class Config:
|
|||||||
raise AttributeError(name)
|
raise AttributeError(name)
|
||||||
if name not in self.values:
|
if name not in self.values:
|
||||||
raise AttributeError(__('No such config value: %s') % name)
|
raise AttributeError(__('No such config value: %s') % name)
|
||||||
default = self.values[name][0]
|
default = self.values[name].default
|
||||||
if callable(default):
|
if callable(default):
|
||||||
return default(self)
|
return default(self)
|
||||||
return default
|
return default
|
||||||
@@ -320,15 +402,20 @@ class Config:
|
|||||||
return name in self.values
|
return name in self.values
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[ConfigValue]:
|
def __iter__(self) -> Iterator[ConfigValue]:
|
||||||
for name, (_default, rebuild, _valid_types) in self.values.items():
|
for name, opt in self.values.items():
|
||||||
yield ConfigValue(name, getattr(self, name), rebuild)
|
yield ConfigValue(name, getattr(self, name), opt.rebuild)
|
||||||
|
|
||||||
def add(self, name: str, default: Any, rebuild: _ConfigRebuild,
|
def add(self, name: str, default: Any, rebuild: _ConfigRebuild,
|
||||||
types: Sequence[type] | ENUM | Any) -> None:
|
types: Sequence[type] | ENUM | Any) -> None:
|
||||||
valid_types = types
|
valid_types = types
|
||||||
if name in self.values:
|
if name in self.values:
|
||||||
raise ExtensionError(__('Config value %r already present') % name)
|
raise ExtensionError(__('Config value %r already present') % name)
|
||||||
self.values[name] = (default, rebuild, valid_types)
|
|
||||||
|
# standardise rebuild
|
||||||
|
if isinstance(rebuild, bool):
|
||||||
|
rebuild = 'env' if rebuild else ''
|
||||||
|
|
||||||
|
self.values[name] = _Opt(default, rebuild, valid_types)
|
||||||
|
|
||||||
def filter(self, rebuild: str | Sequence[str]) -> Iterator[ConfigValue]:
|
def filter(self, rebuild: str | Sequence[str]) -> Iterator[ConfigValue]:
|
||||||
if isinstance(rebuild, str):
|
if isinstance(rebuild, str):
|
||||||
@@ -338,27 +425,28 @@ class Config:
|
|||||||
def __getstate__(self) -> dict:
|
def __getstate__(self) -> dict:
|
||||||
"""Obtains serializable data for pickling."""
|
"""Obtains serializable data for pickling."""
|
||||||
# remove potentially pickling-problematic values from config
|
# remove potentially pickling-problematic values from config
|
||||||
__dict__ = {}
|
__dict__ = {
|
||||||
for key, value in self.__dict__.items():
|
key: value
|
||||||
if key.startswith('_') or not is_serializable(value):
|
for key, value in self.__dict__.items()
|
||||||
pass
|
if not key.startswith('_') and is_serializable(value)
|
||||||
else:
|
}
|
||||||
__dict__[key] = value
|
|
||||||
|
|
||||||
# create a picklable copy of values list
|
# create a picklable copy of values list
|
||||||
__dict__['values'] = values = {}
|
__dict__['values'] = values = {}
|
||||||
for name, (_default, rebuild, _valid_types) in self.values.items():
|
for name, opt in self.values.items():
|
||||||
real_value = getattr(self, name)
|
real_value = getattr(self, name)
|
||||||
if not is_serializable(real_value):
|
if not is_serializable(real_value):
|
||||||
# omit unserializable value
|
# omit unserializable value
|
||||||
real_value = None
|
real_value = None
|
||||||
|
# valid_types is also omitted
|
||||||
# The valid_types column is also omitted
|
values[name] = real_value, opt.rebuild
|
||||||
values[name] = (real_value, rebuild, None)
|
|
||||||
|
|
||||||
return __dict__
|
return __dict__
|
||||||
|
|
||||||
def __setstate__(self, state: dict) -> None:
|
def __setstate__(self, state: dict) -> None:
|
||||||
|
self.values = {
|
||||||
|
name: _Opt(real_value, rebuild, ())
|
||||||
|
for name, (real_value, rebuild) in state.pop('values').items()
|
||||||
|
}
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
|
|
||||||
|
|
||||||
@@ -491,7 +579,9 @@ def check_confval_types(app: Sphinx | None, config: Config) -> None:
|
|||||||
"""Check all values for deviation from the default value's type, since
|
"""Check all values for deviation from the default value's type, since
|
||||||
that can result in TypeErrors all over the place NB.
|
that can result in TypeErrors all over the place NB.
|
||||||
"""
|
"""
|
||||||
for name, (default, _rebuild, valid_types) in config.values.items():
|
for name, opt in config.values.items():
|
||||||
|
default = opt.default
|
||||||
|
valid_types = opt.valid_types
|
||||||
value = getattr(config, name)
|
value = getattr(config, name)
|
||||||
|
|
||||||
if callable(default):
|
if callable(default):
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ default_settings: dict[str, Any] = {
|
|||||||
|
|
||||||
# This is increased every time an environment attribute is added
|
# This is increased every time an environment attribute is added
|
||||||
# or changed to properly invalidate pickle files.
|
# or changed to properly invalidate pickle files.
|
||||||
ENV_VERSION = 60
|
ENV_VERSION = 61
|
||||||
|
|
||||||
# config status
|
# config status
|
||||||
CONFIG_UNSET = -1
|
CONFIG_UNSET = -1
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Test the sphinx.config.Config class."""
|
"""Test the sphinx.config.Config class."""
|
||||||
|
import pickle
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
@@ -7,10 +7,24 @@ from unittest import mock
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import sphinx
|
import sphinx
|
||||||
from sphinx.config import ENUM, Config, check_confval_types
|
from sphinx.config import ENUM, Config, _Opt, check_confval_types
|
||||||
|
from sphinx.deprecation import RemovedInSphinx90Warning
|
||||||
from sphinx.errors import ConfigError, ExtensionError, VersionRequirementError
|
from sphinx.errors import ConfigError, ExtensionError, VersionRequirementError
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_opt_deprecated(recwarn):
|
||||||
|
opt = _Opt('default', '', ())
|
||||||
|
|
||||||
|
with pytest.warns(RemovedInSphinx90Warning):
|
||||||
|
default, rebuild, valid_types = opt
|
||||||
|
|
||||||
|
with pytest.warns(RemovedInSphinx90Warning):
|
||||||
|
_ = opt[0]
|
||||||
|
|
||||||
|
with pytest.warns(RemovedInSphinx90Warning):
|
||||||
|
_ = list(opt)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.sphinx(testroot='config', confoverrides={
|
@pytest.mark.sphinx(testroot='config', confoverrides={
|
||||||
'root_doc': 'root',
|
'root_doc': 'root',
|
||||||
'nonexisting_value': 'True',
|
'nonexisting_value': 'True',
|
||||||
@@ -71,6 +85,16 @@ def test_config_not_found(tmp_path):
|
|||||||
Config.read(tmp_path)
|
Config.read(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL)))
|
||||||
|
def test_config_pickle_protocol(tmp_path, protocol: int):
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
pickled_config = pickle.loads(pickle.dumps(config, protocol))
|
||||||
|
|
||||||
|
assert list(config.values) == list(pickled_config.values)
|
||||||
|
assert repr(config) == repr(pickled_config)
|
||||||
|
|
||||||
|
|
||||||
def test_extension_values():
|
def test_extension_values():
|
||||||
config = Config()
|
config = Config()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user