Narrow the type for configuration option 'valid_types' values

This commit is contained in:
Adam Turner 2024-01-03 21:51:39 +00:00
parent fd23cf0256
commit 259118d182
15 changed files with 133 additions and 101 deletions

View File

@ -443,6 +443,9 @@ The Config object
.. autoclass:: Config
.. py:class:: ENUM
:no-typesetting:
.. _template-bridge:

View File

@ -10,7 +10,7 @@ import os
import pickle
import sys
from collections import deque
from collections.abc import Sequence # NoQA: TCH003
from collections.abc import Collection, Sequence # NoQA: TCH003
from io import StringIO
from os import path
from typing import IO, TYPE_CHECKING, Any, Callable
@ -22,7 +22,7 @@ from pygments.lexer import Lexer # NoQA: TCH002
import sphinx
from sphinx import locale, package_dir
from sphinx.config import Config, _ConfigRebuild
from sphinx.config import ENUM, Config, _ConfigRebuild
from sphinx.environment import BuildEnvironment
from sphinx.errors import ApplicationError, ConfigError, VersionRequirementError
from sphinx.events import EventManager
@ -508,7 +508,7 @@ class Sphinx:
# TODO(stephenfin): Describe 'types' parameter
def add_config_value(self, name: str, default: Any, rebuild: _ConfigRebuild,
types: Any = ()) -> None:
types: type | Collection[type] | ENUM = ()) -> None:
"""Register a configuration value.
This is necessary for Sphinx to recognize new values and set default
@ -542,8 +542,6 @@ class Sphinx:
converted internally.
"""
logger.debug('[app] adding config value: %r', (name, default, rebuild, types))
if rebuild in (False, True):
rebuild = 'env' if rebuild else ''
self.config.add(name, default, rebuild, types)
def add_event(self, name: str) -> None:

View File

@ -1310,21 +1310,20 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('html_theme', 'alabaster', 'html')
app.add_config_value('html_theme_path', [], 'html')
app.add_config_value('html_theme_options', {}, 'html')
app.add_config_value('html_title',
lambda self: _('%s %s documentation') % (self.project, self.release),
'html', [str])
app.add_config_value(
'html_title', lambda c: _('%s %s documentation') % (c.project, c.release), 'html', str)
app.add_config_value('html_short_title', lambda self: self.html_title, 'html')
app.add_config_value('html_style', None, 'html', [list, str])
app.add_config_value('html_logo', None, 'html', [str])
app.add_config_value('html_favicon', None, 'html', [str])
app.add_config_value('html_style', None, 'html', {list, str})
app.add_config_value('html_logo', None, 'html', str)
app.add_config_value('html_favicon', None, 'html', str)
app.add_config_value('html_css_files', [], 'html')
app.add_config_value('html_js_files', [], 'html')
app.add_config_value('html_static_path', [], 'html')
app.add_config_value('html_extra_path', [], 'html')
app.add_config_value('html_last_updated_fmt', None, 'html', [str])
app.add_config_value('html_last_updated_fmt', None, 'html', str)
app.add_config_value('html_sidebars', {}, 'html')
app.add_config_value('html_additional_pages', {}, 'html')
app.add_config_value('html_domain_indices', True, 'html', [list])
app.add_config_value('html_domain_indices', True, 'html', list)
app.add_config_value('html_permalinks', True, 'html')
app.add_config_value('html_permalinks_icon', '', 'html')
app.add_config_value('html_use_index', True, 'html')
@ -1333,8 +1332,8 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('html_show_sourcelink', True, 'html')
app.add_config_value('html_sourcelink_suffix', '.txt', 'html')
app.add_config_value('html_use_opensearch', '', 'html')
app.add_config_value('html_file_suffix', None, 'html', [str])
app.add_config_value('html_link_suffix', None, 'html', [str])
app.add_config_value('html_file_suffix', None, 'html', str)
app.add_config_value('html_link_suffix', None, 'html', str)
app.add_config_value('html_show_copyright', True, 'html')
app.add_config_value('html_show_search_summary', True, 'html')
app.add_config_value('html_show_sphinx', True, 'html')
@ -1342,7 +1341,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('html_output_encoding', 'utf-8', 'html')
app.add_config_value('html_compact_lists', True, 'html')
app.add_config_value('html_secnumber_suffix', '. ', 'html')
app.add_config_value('html_search_language', None, 'html', [str])
app.add_config_value('html_search_language', None, 'html', str)
app.add_config_value('html_search_options', {}, 'html')
app.add_config_value('html_search_scorer', '', '')
app.add_config_value('html_scaled_image_link', True, 'html')

View File

@ -529,21 +529,21 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('latex_engine', default_latex_engine, '',
ENUM('pdflatex', 'xelatex', 'lualatex', 'platex', 'uplatex'))
app.add_config_value('latex_documents', default_latex_documents, '')
app.add_config_value('latex_logo', None, '', [str])
app.add_config_value('latex_logo', None, '', str)
app.add_config_value('latex_appendices', [], '')
app.add_config_value('latex_use_latex_multicolumn', False, '')
app.add_config_value('latex_use_xindy', default_latex_use_xindy, '', [bool])
app.add_config_value('latex_use_xindy', default_latex_use_xindy, '', bool)
app.add_config_value('latex_toplevel_sectioning', None, '',
ENUM(None, 'part', 'chapter', 'section'))
app.add_config_value('latex_domain_indices', True, '', [list])
app.add_config_value('latex_domain_indices', True, '', list)
app.add_config_value('latex_show_urls', 'no', '')
app.add_config_value('latex_show_pagerefs', False, '')
app.add_config_value('latex_elements', {}, '')
app.add_config_value('latex_additional_files', [], '')
app.add_config_value('latex_table_style', ['booktabs', 'colorrows'], '', [list])
app.add_config_value('latex_theme', 'manual', '', [str])
app.add_config_value('latex_table_style', ['booktabs', 'colorrows'], '', list)
app.add_config_value('latex_theme', 'manual', '', str)
app.add_config_value('latex_theme_options', {}, '')
app.add_config_value('latex_theme_path', [], '', [list])
app.add_config_value('latex_theme_path', [], '', list)
app.add_config_value('latex_docclass', default_latex_docclass, '')

View File

@ -217,7 +217,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('texinfo_documents', default_texinfo_documents, '')
app.add_config_value('texinfo_appendices', [], '')
app.add_config_value('texinfo_elements', {}, '')
app.add_config_value('texinfo_domain_indices', True, '', [list])
app.add_config_value('texinfo_domain_indices', True, '', list)
app.add_config_value('texinfo_show_urls', 'footnote', '')
app.add_config_value('texinfo_no_detailmenu', False, '')
app.add_config_value('texinfo_cross_references', True, '')

View File

@ -24,7 +24,7 @@ else:
if TYPE_CHECKING:
import os
from collections.abc import Iterator, Sequence
from collections.abc import Collection, Iterator, Sequence
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
@ -80,13 +80,13 @@ class _Opt:
default: Any
rebuild: _ConfigRebuild
valid_types: Sequence[type] | ENUM | Any
valid_types: tuple[()] | tuple[type, ...] | frozenset[type] | ENUM
def __init__(
self,
default: Any,
rebuild: _ConfigRebuild,
valid_types: Sequence[type] | ENUM | Any,
valid_types: tuple[()] | tuple[type, ...] | frozenset[type] | ENUM,
) -> None:
"""Configuration option type for Sphinx.
@ -175,70 +175,75 @@ class Config:
config_values: dict[str, _Opt] = {
# general options
'project': _Opt('Python', 'env', []),
'author': _Opt('unknown', 'env', []),
'project_copyright': _Opt('', 'html', [str, tuple, list]),
'copyright': _Opt(lambda c: c.project_copyright, 'html', [str, tuple, list]),
'version': _Opt('', 'env', []),
'release': _Opt('', 'env', []),
'today': _Opt('', 'env', []),
'project': _Opt('Python', 'env', ()),
'author': _Opt('unknown', 'env', ()),
'project_copyright': _Opt('', 'html', frozenset((str, tuple, list))),
'copyright': _Opt(
lambda c: c.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', [str]),
'today_fmt': _Opt(None, 'env', frozenset((str,))),
'language': _Opt('en', 'env', [str]),
'locale_dirs': _Opt(['locales'], 'env', []),
'figure_language_filename': _Opt('{root}.{language}{ext}', 'env', [str]),
'gettext_allow_fuzzy_translations': _Opt(False, 'gettext', []),
'language': _Opt('en', 'env', frozenset((str,))),
'locale_dirs': _Opt(['locales'], 'env', ()),
'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')),
'master_doc': _Opt('index', 'env', []),
'root_doc': _Opt(lambda config: config.master_doc, 'env', []),
'source_suffix': _Opt({'.rst': 'restructuredtext'}, 'env', Any),
'source_encoding': _Opt('utf-8-sig', 'env', []),
'exclude_patterns': _Opt([], 'env', [str]),
'include_patterns': _Opt(["**"], 'env', [str]),
'default_role': _Opt(None, 'env', [str]),
'add_function_parentheses': _Opt(True, 'env', []),
'add_module_names': _Opt(True, 'env', []),
'toc_object_entries': _Opt(True, 'env', [bool]),
'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_encoding': _Opt('utf-8-sig', 'env', ()),
'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', ()),
'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')),
'trim_footnote_reference_space': _Opt(False, 'env', []),
'show_authors': _Opt(False, 'env', []),
'pygments_style': _Opt(None, 'html', [str]),
'highlight_language': _Opt('default', 'env', []),
'highlight_options': _Opt({}, 'env', []),
'templates_path': _Opt([], 'html', []),
'template_bridge': _Opt(None, 'html', [str]),
'keep_warnings': _Opt(False, 'env', []),
'suppress_warnings': _Opt([], 'env', []),
'modindex_common_prefix': _Opt([], 'html', []),
'rst_epilog': _Opt(None, 'env', [str]),
'rst_prolog': _Opt(None, 'env', [str]),
'trim_doctest_flags': _Opt(True, 'env', []),
'primary_domain': _Opt('py', 'env', [NoneType]),
'needs_sphinx': _Opt(None, '', [str]),
'needs_extensions': _Opt({}, '', []),
'manpages_url': _Opt(None, 'env', []),
'nitpicky': _Opt(False, '', []),
'nitpick_ignore': _Opt([], '', [set, list, tuple]),
'nitpick_ignore_regex': _Opt([], '', [set, list, tuple]),
'numfig': _Opt(False, 'env', []),
'numfig_secnum_depth': _Opt(1, 'env', []),
'numfig_format': _Opt({}, 'env', []), # will be initialized in init_numfig_format()
'maximum_signature_line_length': _Opt(None, 'env', {int, None}),
'math_number_all': _Opt(False, 'env', []),
'math_eqref_format': _Opt(None, 'env', [str]),
'math_numfig': _Opt(True, 'env', []),
'tls_verify': _Opt(True, 'env', []),
'tls_cacerts': _Opt(None, 'env', []),
'user_agent': _Opt(None, 'env', [str]),
'smartquotes': _Opt(True, 'env', []),
'smartquotes_action': _Opt('qDe', 'env', []),
'trim_footnote_reference_space': _Opt(False, 'env', ()),
'show_authors': _Opt(False, 'env', ()),
'pygments_style': _Opt(None, 'html', frozenset((str,))),
'highlight_language': _Opt('default', 'env', ()),
'highlight_options': _Opt({}, 'env', ()),
'templates_path': _Opt([], 'html', ()),
'template_bridge': _Opt(None, 'html', frozenset((str,))),
'keep_warnings': _Opt(False, 'env', ()),
'suppress_warnings': _Opt([], 'env', ()),
'modindex_common_prefix': _Opt([], 'html', ()),
'rst_epilog': _Opt(None, 'env', frozenset((str,))),
'rst_prolog': _Opt(None, 'env', frozenset((str,))),
'trim_doctest_flags': _Opt(True, 'env', ()),
'primary_domain': _Opt('py', 'env', frozenset((NoneType,))), # type: ignore[arg-type]
'needs_sphinx': _Opt(None, '', frozenset((str,))),
'needs_extensions': _Opt({}, '', ()),
'manpages_url': _Opt(None, 'env', ()),
'nitpicky': _Opt(False, '', ()),
'nitpick_ignore': _Opt([], '', frozenset((set, list, tuple))),
'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()
'maximum_signature_line_length': _Opt(
None, 'env', frozenset((int, NoneType))), # type: ignore[arg-type]
'math_number_all': _Opt(False, 'env', ()),
'math_eqref_format': _Opt(None, 'env', frozenset((str,))),
'math_numfig': _Opt(True, 'env', ()),
'tls_verify': _Opt(True, 'env', ()),
'tls_cacerts': _Opt(None, 'env', ()),
'user_agent': _Opt(None, 'env', frozenset((str,))),
'smartquotes': _Opt(True, 'env', ()),
'smartquotes_action': _Opt('qDe', 'env', ()),
'smartquotes_excludes': _Opt(
{'languages': ['ja'], 'builders': ['man', 'text']}, 'env', []),
'option_emphasise_placeholders': _Opt(False, 'env', []),
{'languages': ['ja'], 'builders': ['man', 'text']}, 'env', ()),
'option_emphasise_placeholders': _Opt(False, 'env', ()),
}
def __init__(self, config: dict[str, Any] | None = None,
@ -296,7 +301,9 @@ class Config:
return True
else:
return value
elif type(default) is bool or valid_types == [bool]:
elif (type(default) is bool
or (not isinstance(valid_types, ENUM)
and len(valid_types) == 1 and bool in valid_types)):
if value == '0':
# given falsy string from command line option
return False
@ -406,8 +413,7 @@ class Config:
yield ConfigValue(name, getattr(self, name), opt.rebuild)
def add(self, name: str, default: Any, rebuild: _ConfigRebuild,
types: Sequence[type] | ENUM | Any) -> None:
valid_types = types
types: type | Collection[type] | ENUM) -> None:
if name in self.values:
raise ExtensionError(__('Config value %r already present') % name)
@ -415,6 +421,8 @@ class Config:
if isinstance(rebuild, bool):
rebuild = 'env' if rebuild else ''
# standardise valid_types
valid_types = _validate_valid_types(types)
self.values[name] = _Opt(default, rebuild, valid_types)
def filter(self, rebuild: str | Sequence[str]) -> Iterator[ConfigValue]:
@ -479,6 +487,29 @@ def eval_config_file(filename: str, tags: Tags | None) -> dict[str, Any]:
return namespace
def _validate_valid_types(
valid_types: type | Collection[type] | ENUM, /,
) -> tuple[()] | tuple[type, ...] | frozenset[type] | ENUM:
if not valid_types:
return ()
if isinstance(valid_types, (frozenset, ENUM)):
return valid_types
if isinstance(valid_types, type):
return frozenset((valid_types,))
if isinstance(valid_types, set):
return frozenset(valid_types)
if not isinstance(valid_types, tuple):
try:
valid_types = tuple(valid_types)
except TypeError:
logger.warning(__('Failed to convert %r to a set or tuple'), valid_types)
return valid_types # type: ignore[return-value]
try:
return frozenset(valid_types)
except TypeError:
return valid_types
def convert_source_suffix(app: Sphinx, config: Config) -> None:
"""Convert old styled source_suffix to new styled one.
@ -620,7 +651,7 @@ def check_confval_types(app: Sphinx | None, config: Config) -> None:
if valid_types:
msg = __("The config value `{name}' has type `{current.__name__}'; "
"expected {permitted}.")
wrapped_valid_types = [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:
permitted = (", ".join(wrapped_valid_types[:-1])
+ f", or {wrapped_valid_types[-1]}")

View File

@ -498,7 +498,7 @@ class JavaScriptDomain(Domain):
def setup(app: Sphinx) -> dict[str, Any]:
app.add_domain(JavaScriptDomain)
app.add_config_value(
'javascript_maximum_signature_line_length', None, 'env', types={int, None},
'javascript_maximum_signature_line_length', None, 'env', {int, type(None)},
)
return {
'version': 'builtin',

View File

@ -1755,8 +1755,9 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_domain(PythonDomain)
app.add_config_value('python_use_unqualified_type_names', False, 'env')
app.add_config_value('python_maximum_signature_line_length', None, 'env',
types={int, None})
app.add_config_value(
'python_maximum_signature_line_length', None, 'env', {int, type(None)},
)
app.add_config_value('python_display_short_literal_types', False, 'env')
app.connect('object-description-transform', filter_meta_fields)
app.connect('missing-reference', builtin_resolver, priority=900)

View File

@ -162,7 +162,7 @@ class FakeDirective(DocumenterBridge):
settings = Struct(tab_width=8)
document = Struct(settings=settings)
app = FakeApplication()
app.config.add('autodoc_class_signature', 'mixed', 'env', None)
app.config.add('autodoc_class_signature', 'mixed', 'env', ())
env = BuildEnvironment(app) # type: ignore[arg-type]
state = Struct(document=document)
super().__init__(env, None, Options(), 0, state)
@ -837,11 +837,11 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.connect('builder-inited', process_generate_options)
app.add_config_value('autosummary_context', {}, 'env')
app.add_config_value('autosummary_filename_map', {}, 'html')
app.add_config_value('autosummary_generate', True, 'env', [bool, list])
app.add_config_value('autosummary_generate', True, 'env', {bool, list})
app.add_config_value('autosummary_generate_overwrite', True, '')
app.add_config_value('autosummary_mock_imports',
lambda config: config.autodoc_mock_imports, 'env')
app.add_config_value('autosummary_imported_members', [], '', [bool])
app.add_config_value('autosummary_imported_members', [], '', bool)
app.add_config_value('autosummary_ignore_module_all', True, 'env', bool)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@ -71,8 +71,8 @@ class DummyApplication:
self._warncount = 0
self.warningiserror = False
self.config.add('autosummary_context', {}, 'env', None)
self.config.add('autosummary_filename_map', {}, 'env', None)
self.config.add('autosummary_context', {}, 'env', ())
self.config.add('autosummary_filename_map', {}, 'env', ())
self.config.add('autosummary_ignore_module_all', True, 'env', bool)
self.config.init_values()

View File

@ -397,8 +397,8 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('coverage_c_regexes', {}, '')
app.add_config_value('coverage_ignore_c_items', {}, '')
app.add_config_value('coverage_write_headline', True, '')
app.add_config_value('coverage_statistics_to_report', True, '', (bool,))
app.add_config_value('coverage_statistics_to_stdout', True, '', (bool,))
app.add_config_value('coverage_statistics_to_report', True, '', bool)
app.add_config_value('coverage_statistics_to_stdout', True, '', bool)
app.add_config_value('coverage_skip_undoc_in_source', False, '')
app.add_config_value('coverage_show_missing_items', False, '')
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@ -563,7 +563,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_directive('testoutput', TestoutputDirective)
app.add_builder(DocTestBuilder)
# this config value adds to sys.path
app.add_config_value('doctest_show_successes', True, '', (bool,))
app.add_config_value('doctest_show_successes', True, '', bool)
app.add_config_value('doctest_path', [], '')
app.add_config_value('doctest_test_doctest_blocks', 'default', '')
app.add_config_value('doctest_global_setup', '', '')

View File

@ -402,6 +402,6 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('imgmath_latex_preamble', '', 'html')
app.add_config_value('imgmath_add_tooltips', True, 'html')
app.add_config_value('imgmath_font_size', 12, 'html')
app.add_config_value('imgmath_embed', False, 'html', [bool])
app.add_config_value('imgmath_embed', False, 'html', bool)
app.connect('build-finished', clean_up_files)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

View File

@ -344,7 +344,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('viewcode_import', None, '')
app.add_config_value('viewcode_enable_epub', False, '')
app.add_config_value('viewcode_follow_imported_members', True, '')
app.add_config_value('viewcode_line_numbers', False, 'env', (bool,))
app.add_config_value('viewcode_line_numbers', False, 'env', bool)
app.connect('doctree-read', doctree_read)
app.connect('env-merge-info', env_merge_info)
app.connect('env-purge-doc', env_purge_doc)

View File

@ -281,9 +281,9 @@ TYPECHECK_WARNING_MESSAGES = [
('value1', 'string', [str], ['foo', 'bar'],
"The config value `value1' has type `list'; expected `str'."),
('value1', 'string', [str, int], ['foo', 'bar'],
"The config value `value1' has type `list'; expected `str' or `int'."),
"The config value `value1' has type `list'; expected `int' or `str'."),
('value1', 'string', [str, int, tuple], ['foo', 'bar'],
"The config value `value1' has type `list'; expected `str', `int', or `tuple'."),
"The config value `value1' has type `list'; expected `int', `str', or `tuple'."),
]