@@ -41,93 +41,57 @@ class Theme:
4141 This class supports both theme directory and theme archive (zipped theme).
4242 """
4343
44- def __init__ (self , name : str , theme_path : str , theme_factory : HTMLThemeFactory ) -> None :
45- self .name = name
46- self ._base : Theme | None = None
47-
48- if path .isdir (theme_path ):
49- # already a directory, do nothing
50- self ._root_dir = None
51- self ._theme_dir = theme_path
52- else :
53- # extract the theme to a temp directory
54- self ._root_dir = tempfile .mkdtemp ('sxt' )
55- self ._theme_dir = path .join (self ._root_dir , name )
56- _extract_zip (theme_path , self ._theme_dir )
57-
58- self .config = _load_theme_conf (self ._theme_dir )
59-
60- try :
61- inherit = self .config .get ('theme' , 'inherit' )
62- except configparser .NoSectionError as exc :
63- raise ThemeError (__ ('theme %r doesn\' t have "theme" setting' ) % name ) from exc
64- except configparser .NoOptionError as exc :
65- raise ThemeError (__ ('theme %r doesn\' t have "inherit" setting' ) % name ) from exc
66- self ._load_ancestor_theme (inherit , theme_factory , name )
67-
68- try :
69- self ._options = dict (self .config .items ('options' ))
70- except configparser .NoSectionError :
71- self ._options = {}
72-
73- self .inherit = inherit
74- try :
75- self .stylesheet = self .get_config ('theme' , 'stylesheet' )
76- except configparser .NoOptionError :
77- msg = __ ("No loaded theme defines 'theme.stylesheet' in the configuration" )
78- raise ThemeError (msg ) from None
79- self .sidebars = self .get_config ('theme' , 'sidebars' , None )
80- self .pygments_style = self .get_config ('theme' , 'pygments_style' , None )
81- self .pygments_dark_style = self .get_config ('theme' , 'pygments_dark_style' , None )
82-
83- def _load_ancestor_theme (
44+ def __init__ (
8445 self ,
85- inherit : str ,
86- theme_factory : HTMLThemeFactory ,
8746 name : str ,
47+ * ,
48+ configs : dict [str , configparser .RawConfigParser ],
49+ paths : list [str ],
50+ tmp_dirs : list [str ],
8851 ) -> None :
89- if inherit != 'none' :
90- try :
91- self ._base = theme_factory .create (inherit )
92- except ThemeError as exc :
93- raise ThemeError (__ ('no theme named %r found, inherited by %r' ) %
94- (inherit , name )) from exc
52+ self .name = name
53+ self ._dirs = paths
54+ self ._tmp_dirs = tmp_dirs
55+
56+ theme : dict [str , Any ] = {}
57+ options : dict [str , Any ] = {}
58+ for config in reversed (configs .values ()):
59+ theme |= dict (config .items ('theme' ))
60+ if config .has_section ('options' ):
61+ options |= dict (config .items ('options' ))
62+
63+ self ._settings = theme
64+ self ._options = options
9565
9666 def get_theme_dirs (self ) -> list [str ]:
9767 """Return a list of theme directories, beginning with this theme's,
9868 then the base theme's, then that one's base theme's, etc.
9969 """
100- if self ._base is None :
101- return [self ._theme_dir ]
102- else :
103- return [self ._theme_dir ] + self ._base .get_theme_dirs ()
70+ return self ._dirs .copy ()
10471
10572 def get_config (self , section : str , name : str , default : Any = _NO_DEFAULT ) -> Any :
10673 """Return the value for a theme configuration setting, searching the
10774 base theme chain.
10875 """
109- try :
110- return self .config .get (section , name )
111- except (configparser .NoOptionError , configparser .NoSectionError ):
112- if self ._base :
113- return self ._base .get_config (section , name , default )
114-
115- if default is _NO_DEFAULT :
116- raise ThemeError (__ ('setting %s.%s occurs in none of the '
117- 'searched theme configs' ) % (section , name )) from None
118- return default
76+ if section == 'theme' :
77+ value = self ._settings .get (name , default )
78+ elif section == 'options' :
79+ value = self ._options .get (name , default )
80+ else :
81+ value = _NO_DEFAULT
82+ if value is _NO_DEFAULT :
83+ msg = __ (
84+ 'setting %s.%s occurs in none of the searched theme configs' ,
85+ ) % (section , name )
86+ raise ThemeError (msg )
87+ return value
11988
12089 def get_options (self , overrides : dict [str , Any ] | None = None ) -> dict [str , Any ]:
12190 """Return a dictionary of theme options and their values."""
12291 if overrides is None :
12392 overrides = {}
12493
125- if self ._base is not None :
126- options = self ._base .get_options ()
127- else :
128- options = {}
129- options |= self ._options
130-
94+ options = self ._options .copy ()
13195 for option , value in overrides .items ():
13296 if option not in options :
13397 logger .warning (__ ('unsupported theme option %r given' ) % option )
@@ -138,12 +102,9 @@ def get_options(self, overrides: dict[str, Any] | None = None) -> dict[str, Any]
138102
139103 def _cleanup (self ) -> None :
140104 """Remove temporary directories."""
141- if self ._root_dir :
105+ for tmp_dir in self ._tmp_dirs :
142106 with contextlib .suppress (Exception ):
143- shutil .rmtree (self ._root_dir )
144-
145- if self ._base is not None :
146- self ._base ._cleanup ()
107+ shutil .rmtree (tmp_dir )
147108
148109
149110class HTMLThemeFactory :
@@ -214,7 +175,8 @@ def create(self, name: str) -> Theme:
214175 if name not in self ._themes :
215176 raise ThemeError (__ ('no theme named %r found (missing theme.conf?)' ) % name )
216177
217- return Theme (name , self ._themes [name ], self )
178+ themes , theme_dirs , tmp_dirs = _load_theme_with_ancestors (self ._themes , name )
179+ return Theme (name , configs = themes , paths = theme_dirs , tmp_dirs = tmp_dirs )
218180
219181
220182def _is_archived_theme (filename : str , / ) -> bool :
@@ -226,6 +188,62 @@ def _is_archived_theme(filename: str, /) -> bool:
226188 return False
227189
228190
191+ def _load_theme_with_ancestors (
192+ theme_paths : dict [str , str ],
193+ name : str , / ,
194+ ) -> tuple [dict [str , configparser .RawConfigParser ], list [str ], list [str ]]:
195+ themes : dict [str , configparser .RawConfigParser ] = {}
196+ theme_dirs : list [str ] = []
197+ tmp_dirs : list [str ] = []
198+
199+ # having 10+ theme ancestors is ludicrous
200+ for _ in range (10 ):
201+ inherit , theme_dir , tmp_dir , config = _load_theme (name , theme_paths [name ])
202+ theme_dirs .append (theme_dir )
203+ if tmp_dir is not None :
204+ tmp_dirs .append (tmp_dir )
205+ themes [name ] = config
206+ if inherit == 'none' :
207+ break
208+ if inherit in themes :
209+ msg = __ ('The %r theme has circular inheritance' ) % name
210+ raise ThemeError (msg )
211+ if inherit not in theme_paths :
212+ msg = __ (
213+ 'The %r theme inherits from %r, which is not a loaded theme. '
214+ 'Loaded themes are: %s' ,
215+ ) % (name , inherit , ', ' .join (sorted (theme_paths )))
216+ raise ThemeError (msg )
217+ name = inherit
218+ else :
219+ msg = __ ('The %r theme has too many ancestors' ) % name
220+ raise ThemeError (msg )
221+
222+ return themes , theme_dirs , tmp_dirs
223+
224+
225+ def _load_theme (
226+ name : str , theme_path : str , / ,
227+ ) -> tuple [str , str , str | None , configparser .RawConfigParser ]:
228+ if path .isdir (theme_path ):
229+ # already a directory, do nothing
230+ tmp_dir = None
231+ theme_dir = theme_path
232+ else :
233+ # extract the theme to a temp directory
234+ tmp_dir = tempfile .mkdtemp ('sxt' )
235+ theme_dir = path .join (tmp_dir , name )
236+ _extract_zip (theme_path , theme_dir )
237+
238+ config = _load_theme_conf (theme_dir )
239+ try :
240+ inherit = config .get ('theme' , 'inherit' )
241+ except (configparser .NoOptionError , configparser .NoSectionError ):
242+ msg = __ ('The %r theme must define the "theme.inherit" setting' ) % name
243+ raise ThemeError (msg ) from None
244+ return inherit , theme_dir , tmp_dir , config
245+
246+
229247def _extract_zip (filename : str , target_dir : str , / ) -> None :
230248 """Extract zip file to target directory."""
231249 ensuredir (target_dir )
0 commit comments