diff --git a/sphinx/application.py b/sphinx/application.py index 3874a6afa52..8117eecf340 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -255,15 +255,16 @@ def __init__( self.statuscode = 0 # read config + overrides = confoverrides or {} self.tags = Tags(tags) if confdir is None: # set confdir to srcdir if -C given (!= no confdir); a few pieces # of code expect a confdir to be set self.confdir = self.srcdir - self.config = Config({}, confoverrides or {}) + self.config = Config({}, overrides) else: self.confdir = _StrPath(confdir).resolve() - self.config = Config.read(self.confdir, confoverrides or {}, self.tags) + self.config = Config.read(self.confdir, overrides=overrides, tags=self.tags) self.config._verbosity = -1 if self.quiet else self.verbosity # set up translation infrastructure diff --git a/sphinx/config.py b/sphinx/config.py index 3e16c151ebd..a43b6cc82d0 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -351,8 +351,9 @@ def verbosity(self) -> int: def read( cls: type[Config], confdir: str | os.PathLike[str], - overrides: dict[str, Any] | None = None, - tags: Tags | None = None, + *, + overrides: dict[str, Any], + tags: Tags, ) -> Config: """Create a Config object from configuration file.""" filename = Path(confdir, CONFIG_FILENAME) @@ -360,23 +361,7 @@ def read( 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' - - return cls(namespace, overrides) + return _read_conf_py(filename, overrides=overrides, tags=tags) def convert_overrides(self, name: str, value: str) -> Any: opt = self._options[name] @@ -589,12 +574,28 @@ def __setstate__(self, state: dict[str, Any]) -> None: self.__dict__.update(state) -def eval_config_file( - filename: str | os.PathLike[str], tags: Tags | None -) -> dict[str, Any]: - """Evaluate a config file.""" - filename = Path(filename) +def _read_conf_py(conf_path: Path, *, overrides: dict[str, Any], tags: Tags) -> Config: + """Create a Config object from a conf.py file.""" + namespace = eval_config_file(conf_path, 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' + return Config(namespace, overrides) + + +def eval_config_file(filename: Path, tags: Tags) -> dict[str, Any]: + """Evaluate a config file.""" namespace: dict[str, Any] = { '__file__': str(filename), 'tags': tags, diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index fc1ba4c7321..b3392e654b2 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -19,6 +19,7 @@ ) from sphinx.deprecation import RemovedInSphinx90Warning from sphinx.errors import ConfigError, ExtensionError, VersionRequirementError +from sphinx.util.tags import Tags if TYPE_CHECKING: from collections.abc import Iterable @@ -139,7 +140,7 @@ def test_core_config(app: SphinxTestApp) -> None: def test_config_not_found(tmp_path): with pytest.raises(ConfigError): - Config.read(tmp_path) + Config.read(tmp_path, overrides={}, tags=Tags()) @pytest.mark.parametrize('protocol', list(range(pickle.HIGHEST_PROTOCOL))) @@ -394,12 +395,12 @@ def test_errors_warnings(logger, tmp_path): # test the error for syntax errors in the config file (tmp_path / 'conf.py').write_text('project = \n', encoding='ascii') with pytest.raises(ConfigError) as excinfo: - Config.read(tmp_path, {}, None) + Config.read(tmp_path, overrides={}, tags=Tags()) assert 'conf.py' in str(excinfo.value) # test the automatic conversion of 2.x only code in configs (tmp_path / 'conf.py').write_text('project = u"Jägermeister"\n', encoding='utf8') - cfg = Config.read(tmp_path, {}, None) + cfg = Config.read(tmp_path, overrides={}, tags=Tags()) assert cfg.project == 'Jägermeister' assert logger.called is False @@ -440,7 +441,7 @@ def test_config_eol(logger, tmp_path): configfile = tmp_path / 'conf.py' for eol in (b'\n', b'\r\n'): configfile.write_bytes(b'project = "spam"' + eol) - cfg = Config.read(tmp_path, {}, None) + cfg = Config.read(tmp_path, overrides={}, tags=Tags()) assert cfg.project == 'spam' assert logger.called is False @@ -678,7 +679,7 @@ def test_conf_py_language_none(tmp_path): (tmp_path / 'conf.py').write_text('language = None', encoding='utf-8') # When we load conf.py into a Config object - cfg = Config.read(tmp_path, {}, None) + cfg = Config.read(tmp_path, overrides={}, tags=Tags()) # Then the language is coerced to English assert cfg.language == 'en' @@ -691,7 +692,7 @@ def test_conf_py_language_none_warning(logger, tmp_path): (tmp_path / 'conf.py').write_text('language = None', encoding='utf-8') # When we load conf.py into a Config object - Config.read(tmp_path, {}, None) + Config.read(tmp_path, overrides={}, tags=Tags()) # Then a warning is raised assert logger.warning.called @@ -708,7 +709,7 @@ def test_conf_py_no_language(tmp_path): (tmp_path / 'conf.py').touch() # When we load conf.py into a Config object - cfg = Config.read(tmp_path, {}, None) + cfg = Config.read(tmp_path, overrides={}, tags=Tags()) # Then the language is coerced to English assert cfg.language == 'en' @@ -720,7 +721,7 @@ def test_conf_py_nitpick_ignore_list(tmp_path): (tmp_path / 'conf.py').touch() # When we load conf.py into a Config object - cfg = Config.read(tmp_path, {}, None) + cfg = Config.read(tmp_path, overrides={}, tags=Tags()) # Then the default nitpick_ignore[_regex] is an empty list assert cfg.nitpick_ignore == []