2727 from importlib_metadata import entry_points # type: ignore[import-not-found]
2828
2929if TYPE_CHECKING :
30+ from collections .abc import Iterable
31+
3032 from sphinx .application import Sphinx
3133
3234logger = 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
197220def _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+ ))
0 commit comments