3232 from importlib_metadata import entry_points
3333
3434if TYPE_CHECKING :
35+ from collections .abc import Callable
3536 from typing import TypedDict
3637
3738 from typing_extensions import Required
@@ -156,6 +157,7 @@ class HTMLThemeFactory:
156157 def __init__ (self , app : Sphinx ) -> None :
157158 self ._app = app
158159 self ._themes = app .registry .html_themes
160+ self ._entry_point_themes : dict [str , Callable [[], None ]] = {}
159161 self ._load_builtin_themes ()
160162 if getattr (app .config , 'html_theme_path' , None ):
161163 self ._load_additional_themes (app .config .html_theme_path )
@@ -183,8 +185,16 @@ def _load_entry_point_themes(self) -> None:
183185 for entry_point in entry_points (group = 'sphinx.html_themes' ):
184186 if entry_point .name in self ._themes :
185187 continue # don't overwrite loaded themes
186- self ._app .registry .load_extension (self ._app , entry_point .module )
187- _config_post_init (self ._app , self ._app .config )
188+
189+ def _load_theme_closure (
190+ # bind variables in the function definition
191+ app : Sphinx = self ._app ,
192+ theme_module : str = entry_point .module ,
193+ ) -> None :
194+ app .setup_extension (theme_module )
195+ _config_post_init (app , app .config )
196+
197+ self ._entry_point_themes [entry_point .name ] = _load_theme_closure
188198
189199 @staticmethod
190200 def _find_themes (theme_path : str ) -> dict [str , str ]:
@@ -217,10 +227,18 @@ def _find_themes(theme_path: str) -> dict[str, str]:
217227
218228 def create (self , name : str ) -> Theme :
219229 """Create an instance of theme."""
230+ if name in self ._entry_point_themes :
231+ # Load a deferred theme from an entry point
232+ entry_point_loader = self ._entry_point_themes [name ]
233+ entry_point_loader ()
220234 if name not in self ._themes :
221235 raise ThemeError (__ ('no theme named %r found (missing theme.toml?)' ) % name )
222236
223- themes , theme_dirs , tmp_dirs = _load_theme_with_ancestors (self ._themes , name )
237+ themes , theme_dirs , tmp_dirs = _load_theme_with_ancestors (
238+ name ,
239+ self ._themes ,
240+ self ._entry_point_themes ,
241+ )
224242 return Theme (name , configs = themes , paths = theme_dirs , tmp_dirs = tmp_dirs )
225243
226244
@@ -235,7 +253,10 @@ def _is_archived_theme(filename: str, /) -> bool:
235253
236254
237255def _load_theme_with_ancestors (
238- theme_paths : dict [str , str ], name : str , /
256+ name : str ,
257+ theme_paths : dict [str , str ],
258+ entry_point_themes : dict [str , Callable [[], None ]],
259+ / ,
239260) -> tuple [dict [str , _ConfigFile ], list [str ], list [str ]]:
240261 themes : dict [str , _ConfigFile ] = {}
241262 theme_dirs : list [str ] = []
@@ -253,6 +274,10 @@ def _load_theme_with_ancestors(
253274 if inherit in themes :
254275 msg = __ ('The %r theme has circular inheritance' ) % name
255276 raise ThemeError (msg )
277+ if inherit in entry_point_themes and inherit not in theme_paths :
278+ # Load a deferred theme from an entry point
279+ entry_point_loader = entry_point_themes [inherit ]
280+ entry_point_loader ()
256281 if inherit not in theme_paths :
257282 msg = __ (
258283 'The %r theme inherits from %r, which is not a loaded theme. '
0 commit comments