Skip to content

Commit efd24cf

Browse files
authored
Defer computation of configuration values (#11855)
1 parent 4ce2d84 commit efd24cf

File tree

8 files changed

+119
-112
lines changed

8 files changed

+119
-112
lines changed

sphinx/application.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,6 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st
210210
self.confdir = _StrPath(confdir).resolve()
211211
self.config = Config.read(self.confdir, confoverrides or {}, self.tags)
212212

213-
# initialize some limited config variables before initialize i18n and loading
214-
# extensions
215-
self.config.pre_init_values()
216-
217213
# set up translation infrastructure
218214
self._init_i18n()
219215

@@ -252,8 +248,8 @@ def __init__(self, srcdir: str | os.PathLike[str], confdir: str | os.PathLike[st
252248
"This is needed for conf.py to behave as a Sphinx extension."),
253249
)
254250

255-
# now that we know all config values, collect them from conf.py
256-
self.config.init_values()
251+
# Report any warnings for overrides.
252+
self.config._report_override_warnings()
257253
self.events.emit('config-inited', self.config)
258254

259255
# create the project

sphinx/config.py

Lines changed: 83 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,12 @@ def __init__(self, config: dict[str, Any] | None = None,
252252
self._overrides = dict(overrides) if overrides is not None else {}
253253
self._options = Config.config_values.copy()
254254
self._raw_config = raw_config
255+
256+
for name in list(self._overrides.keys()):
257+
if '.' in name:
258+
real_name, key = name.split('.', 1)
259+
raw_config.setdefault(real_name, {})[key] = self._overrides.pop(name)
260+
255261
self.setup: _ExtensionSetupFunc | None = raw_config.get('setup')
256262

257263
if 'extensions' in self._overrides:
@@ -292,90 +298,60 @@ def read(cls, confdir: str | os.PathLike[str], overrides: dict | None = None,
292298

293299
return cls(namespace, overrides)
294300

295-
def convert_overrides(self, name: str, value: Any) -> Any:
296-
if not isinstance(value, str):
301+
def convert_overrides(self, name: str, value: str) -> Any:
302+
opt = self._options[name]
303+
default = opt.default
304+
valid_types = opt.valid_types
305+
if valid_types == Any:
297306
return value
298-
else:
299-
opt = self._options[name]
300-
default = opt.default
301-
valid_types = opt.valid_types
302-
if valid_types == Any:
303-
return value
304-
elif (type(default) is bool
305-
or (not isinstance(valid_types, ENUM)
306-
and len(valid_types) == 1 and bool in valid_types)):
307-
if isinstance(valid_types, ENUM) or len(valid_types) > 1:
308-
# if valid_types are given, and non-bool valid types exist,
309-
# return the value without coercing to a Boolean.
310-
return value
311-
# given falsy string from a command line option
312-
return value not in {'0', ''}
313-
elif isinstance(default, dict):
314-
raise ValueError(__('cannot override dictionary config setting %r, '
315-
'ignoring (use %r to set individual elements)') %
316-
(name, name + '.key=value'))
317-
elif isinstance(default, list):
318-
return value.split(',')
319-
elif isinstance(default, int):
320-
try:
321-
return int(value)
322-
except ValueError as exc:
323-
raise ValueError(__('invalid number %r for config value %r, ignoring') %
324-
(value, name)) from exc
325-
elif callable(default):
326-
return value
327-
elif default is not None and not isinstance(default, str):
328-
raise ValueError(__('cannot override config setting %r with unsupported '
329-
'type, ignoring') % name)
330-
else:
307+
elif (type(default) is bool
308+
or (not isinstance(valid_types, ENUM)
309+
and len(valid_types) == 1 and bool in valid_types)):
310+
if isinstance(valid_types, ENUM) or len(valid_types) > 1:
311+
# if valid_types are given, and non-bool valid types exist,
312+
# return the value without coercing to a Boolean.
331313
return value
332-
333-
def pre_init_values(self) -> None:
334-
"""
335-
Initialize some limited config variables before initializing i18n and loading
336-
extensions.
337-
"""
338-
for name in 'needs_sphinx', 'suppress_warnings', 'language', 'locale_dirs':
314+
# given falsy string from a command line option
315+
return value not in {'0', ''}
316+
elif isinstance(default, dict):
317+
raise ValueError(__('cannot override dictionary config setting %r, '
318+
'ignoring (use %r to set individual elements)') %
319+
(name, f'{name}.key=value'))
320+
elif isinstance(default, list):
321+
return value.split(',')
322+
elif isinstance(default, int):
339323
try:
340-
if name in self._overrides:
341-
self.__dict__[name] = self.convert_overrides(name, self._overrides[name])
342-
elif name in self._raw_config:
343-
self.__dict__[name] = self._raw_config[name]
324+
return int(value)
344325
except ValueError as exc:
345-
logger.warning("%s", exc)
326+
raise ValueError(__('invalid number %r for config value %r, ignoring') %
327+
(value, name)) from exc
328+
elif callable(default):
329+
return value
330+
elif default is not None and not isinstance(default, str):
331+
raise ValueError(__('cannot override config setting %r with unsupported '
332+
'type, ignoring') % name)
333+
else:
334+
return value
346335

347-
def init_values(self) -> None:
348-
config = self._raw_config
349-
for name, value in self._overrides.items():
350-
try:
351-
if '.' in name:
352-
real_name, key = name.split('.', 1)
353-
config.setdefault(real_name, {})[key] = value
354-
continue
355-
if name not in self._options:
356-
logger.warning(__('unknown config value %r in override, ignoring'),
357-
name)
358-
continue
359-
if isinstance(value, str):
360-
config[name] = self.convert_overrides(name, value)
361-
else:
362-
config[name] = value
363-
except ValueError as exc:
364-
logger.warning("%s", exc)
365-
for name in config:
366-
if name in self._options:
367-
self.__dict__[name] = config[name]
336+
@staticmethod
337+
def pre_init_values() -> None:
338+
# method only retained for compatability
339+
pass
340+
# warnings.warn(
341+
# 'Config.pre_init_values() will be removed in Sphinx 9.0 or later',
342+
# RemovedInSphinx90Warning, stacklevel=2)
368343

369-
def post_init_values(self) -> None:
370-
"""
371-
Initialize additional config variables that are added after init_values() called.
372-
"""
373-
config = self._raw_config
374-
for name in config:
375-
if name not in self.__dict__ and name in self._options:
376-
self.__dict__[name] = config[name]
344+
def init_values(self) -> None:
345+
# method only retained for compatability
346+
self._report_override_warnings()
347+
# warnings.warn(
348+
# 'Config.init_values() will be removed in Sphinx 9.0 or later',
349+
# RemovedInSphinx90Warning, stacklevel=2)
377350

378-
check_confval_types(None, self)
351+
def _report_override_warnings(self) -> None:
352+
for name in self._overrides:
353+
if name not in self._options:
354+
logger.warning(__('unknown config value %r in override, ignoring'), name)
379355

380356
def __repr__(self):
381357
values = []
@@ -388,14 +364,35 @@ def __repr__(self):
388364
return self.__class__.__qualname__ + '(' + ', '.join(values) + ')'
389365

390366
def __getattr__(self, name: str) -> Any:
367+
if name in self._options:
368+
# first check command-line overrides
369+
if name in self._overrides:
370+
value = self._overrides[name]
371+
if not isinstance(value, str):
372+
self.__dict__[name] = value
373+
return value
374+
try:
375+
value = self.convert_overrides(name, value)
376+
except ValueError as exc:
377+
logger.warning("%s", exc)
378+
else:
379+
self.__dict__[name] = value
380+
return value
381+
# then check values from 'conf.py'
382+
if name in self._raw_config:
383+
self.__dict__[name] = value = self._raw_config[name]
384+
return value
385+
# finally, fall back to the default value
386+
default = self._options[name].default
387+
if callable(default):
388+
return default(self)
389+
self.__dict__[name] = default
390+
return default
391391
if name.startswith('_'):
392-
raise AttributeError(name)
393-
if name not in self._options:
394-
raise AttributeError(__('No such config value: %s') % name)
395-
default = self._options[name].default
396-
if callable(default):
397-
return default(self)
398-
return default
392+
msg = f'{self.__class__.__name__!r} object has no attribute {name!r}'
393+
raise AttributeError(msg)
394+
msg = __('No such config value: %r') % name
395+
raise AttributeError(msg)
399396

400397
def __getitem__(self, name: str) -> Any:
401398
return getattr(self, name)
@@ -452,10 +449,12 @@ def __getstate__(self) -> dict:
452449
return __dict__
453450

454451
def __setstate__(self, state: dict) -> None:
452+
self._overrides = {}
455453
self._options = {
456454
name: _Opt(real_value, rebuild, ())
457455
for name, (real_value, rebuild) in state.pop('_options').items()
458456
}
457+
self._raw_config = {}
459458
self.__dict__.update(state)
460459

461460

sphinx/ext/autosummary/generate.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ def __init__(self, translator: NullTranslations) -> None:
7474
self.config.add('autosummary_context', {}, 'env', ())
7575
self.config.add('autosummary_filename_map', {}, 'env', ())
7676
self.config.add('autosummary_ignore_module_all', True, 'env', bool)
77-
self.config.init_values()
7877

7978
def emit_firstresult(self, *args: Any) -> None:
8079
pass

sphinx/theming.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import contextlib
2020

2121
from sphinx import package_dir
22+
from sphinx.config import check_confval_types as _config_post_init
2223
from sphinx.errors import ThemeError
2324
from sphinx.locale import __
2425
from sphinx.util import logging
@@ -188,7 +189,7 @@ def load_extra_theme(self, name: str) -> None:
188189
pass
189190
else:
190191
self.app.registry.load_extension(self.app, entry_point.module)
191-
self.app.config.post_init_values()
192+
_config_post_init(None, self.app.config)
192193

193194
def find_themes(self, theme_path: str) -> dict[str, str]:
194195
"""Search themes from specified directory."""

tests/test_build_latex.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1594,7 +1594,6 @@ def test_default_latex_documents():
15941594
config = Config({'root_doc': 'index',
15951595
'project': 'STASI™ Documentation',
15961596
'author': "Wolfgang Schäuble & G'Beckstein."})
1597-
config.init_values()
15981597
config.add('latex_engine', None, True, None)
15991598
config.add('latex_theme', 'manual', True, None)
16001599
expected = [('index', 'stasi.tex', 'STASI™ Documentation',

tests/test_build_manpage.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ def test_default_man_pages():
9292
config = Config({'project': 'STASI™ Documentation',
9393
'author': "Wolfgang Schäuble & G'Beckstein",
9494
'release': '1.0'})
95-
config.init_values()
9695
expected = [('index', 'stasi', 'STASI™ Documentation 1.0',
9796
["Wolfgang Schäuble & G'Beckstein"], 1)]
9897
assert default_man_pages(config) == expected

tests/test_build_texinfo.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ def test_texinfo_citation(app, status, warning):
8484
def test_default_texinfo_documents():
8585
config = Config({'project': 'STASI™ Documentation',
8686
'author': "Wolfgang Schäuble & G'Beckstein"})
87-
config.init_values()
8887
expected = [('index', 'stasi', 'STASI™ Documentation',
8988
"Wolfgang Schäuble & G'Beckstein", 'stasi',
9089
'One line description of project', 'Miscellaneous')]

0 commit comments

Comments
 (0)