Skip to content

Commit 6fd8b30

Browse files
authored
Parse theme.conf to a new _ConfigFile type (sphinx-doc#12254)
1 parent dbedb52 commit 6fd8b30

File tree

3 files changed

+137
-34
lines changed

3 files changed

+137
-34
lines changed

sphinx/builders/html/__init__.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,7 @@ def _get_style_filenames(self) -> Iterator[str]:
265265
elif self.config.html_style is not None:
266266
yield from self.config.html_style
267267
elif self.theme:
268-
stylesheet = self.theme.get_config('theme', 'stylesheet')
269-
yield from map(str.strip, stylesheet.split(','))
268+
yield from self.theme.stylesheets
270269
else:
271270
yield 'default.css'
272271

@@ -286,13 +285,15 @@ def init_highlighter(self) -> None:
286285
if self.config.pygments_style is not None:
287286
style = self.config.pygments_style
288287
elif self.theme:
289-
style = self.theme.get_config('theme', 'pygments_style', 'none')
288+
# From the ``pygments_style`` theme setting
289+
style = self.theme.pygments_style_default or 'none'
290290
else:
291291
style = 'sphinx'
292292
self.highlighter = PygmentsBridge('html', style)
293293

294294
if self.theme:
295-
dark_style = self.theme.get_config('theme', 'pygments_dark_style', None)
295+
# From the ``pygments_dark_style`` theme setting
296+
dark_style = self.theme.pygments_style_dark
296297
else:
297298
dark_style = None
298299

@@ -960,13 +961,11 @@ def add_sidebars(self, pagename: str, ctx: dict) -> None:
960961
def has_wildcard(pattern: str) -> bool:
961962
return any(char in pattern for char in '*?[')
962963

963-
sidebars = None
964964
matched = None
965965
customsidebar = None
966966

967967
# default sidebars settings for selected theme
968-
if theme_default_sidebars := self.theme.get_config('theme', 'sidebars', None):
969-
sidebars = [name.strip() for name in theme_default_sidebars.split(',')]
968+
sidebars = list(self.theme.sidebar_templates)
970969

971970
# user sidebar settings
972971
html_sidebars = self.get_builder_config('sidebars', 'html')
@@ -985,7 +984,7 @@ def has_wildcard(pattern: str) -> bool:
985984
matched = pattern
986985
sidebars = patsidebars
987986

988-
if sidebars is None:
987+
if len(sidebars) == 0:
989988
# keep defaults
990989
pass
991990

sphinx/theming.py

Lines changed: 128 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
from importlib_metadata import entry_points # type: ignore[import-not-found]
2828

2929
if TYPE_CHECKING:
30+
from collections.abc import Iterable
31+
3032
from sphinx.application import Sphinx
3133

3234
logger = logging.getLogger(__name__)
@@ -45,36 +47,57 @@ def __init__(
4547
self,
4648
name: str,
4749
*,
48-
configs: dict[str, configparser.RawConfigParser],
50+
configs: dict[str, _ConfigFile],
4951
paths: list[str],
5052
tmp_dirs: list[str],
5153
) -> None:
5254
self.name = name
53-
self._dirs = paths
55+
self._dirs = tuple(paths)
5456
self._tmp_dirs = tmp_dirs
5557

56-
theme: dict[str, Any] = {}
5758
options: dict[str, Any] = {}
59+
self.stylesheets: tuple[str, ...] = ()
60+
self.sidebar_templates: tuple[str, ...] = ()
61+
self.pygments_style_default: str | None = None
62+
self.pygments_style_dark: str | None = None
5863
for config in reversed(configs.values()):
59-
theme |= dict(config.items('theme'))
60-
if config.has_section('options'):
61-
options |= dict(config.items('options'))
64+
options |= config.options
65+
if len(config.stylesheets):
66+
self.stylesheets = config.stylesheets
67+
if len(config.sidebar_templates):
68+
self.sidebar_templates = config.sidebar_templates
69+
if config.pygments_style_default is not None:
70+
self.pygments_style_default = config.pygments_style_default
71+
if config.pygments_style_dark is not None:
72+
self.pygments_style_dark = config.pygments_style_dark
6273

63-
self._settings = theme
6474
self._options = options
6575

76+
if len(self.stylesheets) == 0:
77+
msg = __("No loaded theme defines 'theme.stylesheet' in the configuration")
78+
raise ThemeError(msg) from None
79+
6680
def get_theme_dirs(self) -> list[str]:
6781
"""Return a list of theme directories, beginning with this theme's,
6882
then the base theme's, then that one's base theme's, etc.
6983
"""
70-
return self._dirs.copy()
84+
return list(self._dirs)
7185

7286
def get_config(self, section: str, name: str, default: Any = _NO_DEFAULT) -> Any:
7387
"""Return the value for a theme configuration setting, searching the
7488
base theme chain.
7589
"""
7690
if section == 'theme':
77-
value = self._settings.get(name, default)
91+
if name == 'stylesheet':
92+
value = ', '.join(self.stylesheets) or default
93+
elif name == 'sidebars':
94+
value = ', '.join(self.sidebar_templates) or default
95+
elif name == 'pygments_style':
96+
value = self.pygments_style_default or default
97+
elif name == 'pygments_dark_style':
98+
value = self.pygments_style_dark or default
99+
else:
100+
value = default
78101
elif section == 'options':
79102
value = self._options.get(name, default)
80103
else:
@@ -196,8 +219,8 @@ def _is_archived_theme(filename: str, /) -> bool:
196219

197220
def _load_theme_with_ancestors(
198221
theme_paths: dict[str, str], name: str, /
199-
) -> tuple[dict[str, configparser.RawConfigParser], list[str], list[str]]:
200-
themes: dict[str, configparser.RawConfigParser] = {}
222+
) -> tuple[dict[str, _ConfigFile], list[str], list[str]]:
223+
themes: dict[str, _ConfigFile] = {}
201224
theme_dirs: list[str] = []
202225
tmp_dirs: list[str] = []
203226

@@ -227,9 +250,7 @@ def _load_theme_with_ancestors(
227250
return themes, theme_dirs, tmp_dirs
228251

229252

230-
def _load_theme(
231-
name: str, theme_path: str, /
232-
) -> tuple[str, str, str | None, configparser.RawConfigParser]:
253+
def _load_theme(name: str, theme_path: str, /) -> tuple[str, str, str | None, _ConfigFile]:
233254
if path.isdir(theme_path):
234255
# already a directory, do nothing
235256
tmp_dir = None
@@ -240,12 +261,13 @@ def _load_theme(
240261
theme_dir = path.join(tmp_dir, name)
241262
_extract_zip(theme_path, theme_dir)
242263

243-
config = _load_theme_conf(theme_dir)
244-
try:
245-
inherit = config.get('theme', 'inherit')
246-
except (configparser.NoOptionError, configparser.NoSectionError):
247-
msg = __('The %r theme must define the "theme.inherit" setting') % name
248-
raise ThemeError(msg) from None
264+
if os.path.isfile(conf_path := path.join(theme_dir, _THEME_CONF)):
265+
_cfg_parser = _load_theme_conf(conf_path)
266+
inherit = _validate_theme_conf(_cfg_parser, name)
267+
config = _convert_theme_conf(_cfg_parser)
268+
else:
269+
raise ThemeError(__('no theme configuration file found in %r') % theme_dir)
270+
249271
return inherit, theme_dir, tmp_dir, config
250272

251273

@@ -263,10 +285,92 @@ def _extract_zip(filename: str, target_dir: str, /) -> None:
263285
fp.write(archive.read(name))
264286

265287

266-
def _load_theme_conf(theme_dir: os.PathLike[str] | str, /) -> configparser.RawConfigParser:
288+
def _load_theme_conf(config_file_path: str, /) -> configparser.RawConfigParser:
267289
c = configparser.RawConfigParser()
268-
config_file_path = path.join(theme_dir, _THEME_CONF)
269-
if not os.path.isfile(config_file_path):
270-
raise ThemeError(__('theme configuration file %r not found') % config_file_path)
271290
c.read(config_file_path, encoding='utf-8')
272291
return c
292+
293+
294+
def _validate_theme_conf(cfg: configparser.RawConfigParser, name: str) -> str:
295+
if not cfg.has_section('theme'):
296+
raise ThemeError(__('theme %r doesn\'t have the "theme" table') % name)
297+
if inherit := cfg.get('theme', 'inherit', fallback=None):
298+
return inherit
299+
msg = __('The %r theme must define the "theme.inherit" setting') % name
300+
raise ThemeError(msg)
301+
302+
303+
def _convert_theme_conf(cfg: configparser.RawConfigParser, /) -> _ConfigFile:
304+
if stylesheet := cfg.get('theme', 'stylesheet', fallback=''):
305+
stylesheets: tuple[str, ...] = tuple(map(str.strip, stylesheet.split(',')))
306+
else:
307+
stylesheets = ()
308+
if sidebar := cfg.get('theme', 'sidebars', fallback=''):
309+
sidebar_templates: tuple[str, ...] = tuple(map(str.strip, sidebar.split(',')))
310+
else:
311+
sidebar_templates = ()
312+
pygments_style_default: str | None = cfg.get('theme', 'pygments_style', fallback=None)
313+
pygments_style_dark: str | None = cfg.get('theme', 'pygments_dark_style', fallback=None)
314+
options = dict(cfg.items('options')) if cfg.has_section('options') else {}
315+
return _ConfigFile(
316+
stylesheets=stylesheets,
317+
sidebar_templates=sidebar_templates,
318+
pygments_style_default=pygments_style_default,
319+
pygments_style_dark=pygments_style_dark,
320+
options=options,
321+
)
322+
323+
324+
class _ConfigFile:
325+
__slots__ = (
326+
'stylesheets',
327+
'sidebar_templates',
328+
'pygments_style_default',
329+
'pygments_style_dark',
330+
'options',
331+
)
332+
333+
def __init__(
334+
self,
335+
stylesheets: Iterable[str],
336+
sidebar_templates: Iterable[str],
337+
pygments_style_default: str | None,
338+
pygments_style_dark: str | None,
339+
options: dict[str, str],
340+
) -> None:
341+
self.stylesheets: tuple[str, ...] = tuple(stylesheets)
342+
self.sidebar_templates: tuple[str, ...] = tuple(sidebar_templates)
343+
self.pygments_style_default: str | None = pygments_style_default
344+
self.pygments_style_dark: str | None = pygments_style_dark
345+
self.options: dict[str, str] = options.copy()
346+
347+
def __repr__(self) -> str:
348+
return (
349+
f'{self.__class__.__qualname__}('
350+
f'stylesheets={self.stylesheets!r}, '
351+
f'sidebar_templates={self.sidebar_templates!r}, '
352+
f'pygments_style_default={self.pygments_style_default!r}, '
353+
f'pygments_style_dark={self.pygments_style_dark!r}, '
354+
f'options={self.options!r})'
355+
)
356+
357+
def __eq__(self, other: object) -> bool:
358+
if isinstance(other, _ConfigFile):
359+
return (
360+
self.stylesheets == other.stylesheets
361+
and self.sidebar_templates == other.sidebar_templates
362+
and self.pygments_style_default == other.pygments_style_default
363+
and self.pygments_style_dark == other.pygments_style_dark
364+
and self.options == other.options
365+
)
366+
return NotImplemented
367+
368+
def __hash__(self) -> int:
369+
return hash((
370+
self.__class__.__qualname__,
371+
self.stylesheets,
372+
self.sidebar_templates,
373+
self.pygments_style_default,
374+
self.pygments_style_dark,
375+
self.options,
376+
))

tests/test_theming/test_theming.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import sphinx.builders.html
1111
from sphinx.errors import ThemeError
12-
from sphinx.theming import _load_theme_conf
12+
from sphinx.theming import _load_theme
1313

1414

1515
@pytest.mark.sphinx(
@@ -81,7 +81,7 @@ def test_nonexistent_theme_conf(tmp_path):
8181
# Check that error occurs with a non-existent theme.conf
8282
# (https://github.com/sphinx-doc/sphinx/issues/11668)
8383
with pytest.raises(ThemeError):
84-
_load_theme_conf(tmp_path)
84+
_load_theme('', str(tmp_path))
8585

8686

8787
@pytest.mark.sphinx(testroot='double-inheriting-theme')

0 commit comments

Comments
 (0)