1515 from sphinx .environment import BuildEnvironment
1616
1717
18- # ===================================
18+ # ============================
1919# ExtraContext ABC definitions
20- # ===================================
20+ # ============================
2121
22+ type ExtraContext = (
23+ ParsingPhaseExtraContext
24+ | ParsingPhaseExtraContext
25+ | ResolvingPhaseExtraContext
26+ | GlobalExtraContext
27+ )
28+ """Type alias of all available extra contexts."""
2229
23- class ExtraContext (ABC ):
24- """Base class for all extra context."""
2530
26-
27- class ParsingPhaseExtraContext (ExtraContext ):
31+ class ParsingPhaseExtraContext (ABC ):
2832 """Extra context generated during the :py:data:`~Phase.Parsing` phase.
2933
3034 The ``generate`` method receives the current directive or role being executed.
@@ -34,7 +38,7 @@ class ParsingPhaseExtraContext(ExtraContext):
3438 def generate (self , directive : SphinxDirective | SphinxRole ) -> Any : ...
3539
3640
37- class ParsedPhaseExtraContext (ExtraContext ):
41+ class ParsedPhaseExtraContext (ABC ):
3842 """Extra context generated during the :py:data:`~Phase.Parsed` phase.
3943
4044 The ``generate`` method receives the current Sphinx transform.
@@ -44,7 +48,7 @@ class ParsedPhaseExtraContext(ExtraContext):
4448 def generate (self , transform : SphinxTransform ) -> Any : ...
4549
4650
47- class ResolvingPhaseExtraContext (ExtraContext ):
51+ class ResolvingPhaseExtraContext (ABC ):
4852 """Extra context generated during the :py:data:`~Phase.Resolving` phase.
4953
5054 The ``generate`` method receives the current Sphinx transform.
@@ -54,7 +58,7 @@ class ResolvingPhaseExtraContext(ExtraContext):
5458 def generate (self , transform : SphinxTransform ) -> Any : ...
5559
5660
57- class GlobalExtraContext (ExtraContext ):
61+ class GlobalExtraContext (ABC ):
5862 """Extra context available in all phases.
5963
6064 The ``generate`` method receives the Sphinx build environment.
@@ -64,50 +68,66 @@ class GlobalExtraContext(ExtraContext):
6468 def generate (self , env : BuildEnvironment ) -> Any : ...
6569
6670
67- # =======================
71+ # ==========================
6872# Extra context registration
69- # =======================
73+ # ==========================
7074
7175
72- class ExtraContextRegistry :
73- _extra : dict [str , ExtraContext ]
76+ class _ExtraContextRegistry :
77+ ctxs : dict [str , ExtraContext ]
7478
7579 def __init__ (self ) -> None :
76- self ._extra = {}
80+ self .ctxs = {}
81+
82+ def is_extra_context (self , ctx : Any ) -> bool :
83+ return isinstance (
84+ ctx ,
85+ (
86+ ParsingPhaseExtraContext ,
87+ ParsingPhaseExtraContext ,
88+ ResolvingPhaseExtraContext ,
89+ GlobalExtraContext ,
90+ ),
91+ )
7792
7893 def register (self , name : str , ctx : ExtraContext ) -> None :
79- """Register an extra context.
80-
81- :param name: The context name, used in templates via ``load('name')``.
82- :param ctx: The extra context instance.
83- """
84- if name in self ._extra :
94+ if name in self .ctxs :
8595 raise ValueError (f'Extra context "{ name } " already registered' )
86- self ._extra [name ] = ctx
96+ if not self .is_extra_context (ctx ):
97+ raise TypeError (
98+ f'Invalid extra context instance "{ name } ", Expecting { ExtraContext } '
99+ )
100+ self .ctxs [name ] = ctx
87101
88102 def get (self , name : str ) -> ExtraContext | None :
89- """Get a registered extra context by name."""
90- return self ._extra .get (name )
103+ if name not in self .ctxs :
104+ return None
105+ return self .ctxs [name ]
91106
92- @property
93- def names (self ) -> list [str ]:
94- """Return all registered extra context names."""
95- return list (self ._extra .keys ())
107+ def get_names_by_phase (self , cls : type ) -> set [str ]:
108+ return {name for name , ctx in self .ctxs .items () if isinstance (ctx , cls )}
96109
110+ def get_names (self ) -> set [str ]:
111+ return set (self .ctxs .keys ())
97112
98- # Global registry instance
99- REGISTRY = ExtraContextRegistry ()
113+
114+ # Global registry instance.
115+ _REGISTRY = _ExtraContextRegistry ()
100116
101117
102118def extra_context (name : str ):
103119 """Decorator to register an extra context.
104120
105- The phase is determined by which ExtraContext subclass is used:
121+ The phase is determined by which ExtraContext class is used:
106122
107- - :py:class:`GlobalExtraContext` -> available in all phases
108- - :py:class:`ParsingPhaseExtraContext` -> available during Parsing phase
109- - :py:class:`ParsedPhaseExtraContext` -> available during Parsed phase
110- - :py:class:`ResolvingPhaseExtraContext` -> available during Resolving phase
123+ :py:class:`GlobalExtraContext`
124+ available in all phases
125+ :py:class:`ParsingPhaseExtraContext`
126+ available during Parsing phase
127+ :py:class:`ParsedPhaseExtraContext`
128+ available during Parsed phase
129+ :py:class:`ResolvingPhaseExtraContext`
130+ available during Resolving phase
111131
112132 Example::
113133
@@ -120,19 +140,74 @@ def generate(self, directive):
120140 """
121141
122142 def decorator (cls ):
123- if not issubclass (cls , ExtraContext ):
124- raise TypeError (f'{ cls .__name__ } must subclass an ExtraContext ABC' )
125-
126- instance = cls ()
127- REGISTRY .register (name , instance )
143+ _REGISTRY .register (name , cls ())
128144 return cls
129145
130146 return decorator
131147
132148
133- # ===================================
149+ # ========================
150+ # Extra Context Generation
151+ # ========================
152+
153+
154+ class ExtraContextGenerator :
155+ node : pending_node
156+ todo : set [str ]
157+ report : Report
158+
159+ def __init__ (self , node : pending_node ) -> None :
160+ self .node = node
161+ self .report = Report (
162+ 'Extra Context Generation Report' ,
163+ 'ERROR' ,
164+ source = node .source ,
165+ line = node .line ,
166+ )
167+ Reporter (node ).append (self .report )
168+
169+ # Initialize todo with requested extra contexts, validate they exist
170+ requested = set (node .template .extra )
171+ avail = _REGISTRY .get_names ()
172+ self .todo = requested & avail
173+
174+ # Report errors for non-existent contexts
175+ if nonexist := requested - avail :
176+ self .report .text (f'Extra contexts { nonexist } are not registered.' )
177+
178+ def on_anytime (self , env : BuildEnvironment ) -> None :
179+ self ._generate (GlobalExtraContext , lambda ctx : ctx .generate (env ))
180+
181+ def on_parsing (self , directive : SphinxDirective | SphinxRole ) -> None :
182+ self ._generate (ParsingPhaseExtraContext , lambda ctx : ctx .generate (directive ))
183+
184+ def on_parsed (self , transform : SphinxTransform ) -> None :
185+ self ._generate (ParsedPhaseExtraContext , lambda ctx : ctx .generate (transform ))
186+
187+ def on_resolving (self , transform : SphinxTransform ) -> None :
188+ self ._generate (ResolvingPhaseExtraContext , lambda ctx : ctx .generate (transform ))
189+
190+ def _generate (self , cls : type , gen : Callable [..., Any ]) -> None :
191+ # Get all context names available for this phase
192+ avail = _REGISTRY .get_names_by_phase (cls )
193+ # Find which ones are requested and not yet generated
194+ todo = avail & self .todo
195+
196+ for name in todo :
197+ ctx = _REGISTRY .get (name )
198+ if ctx is None :
199+ continue
200+ try :
201+ self .node .extra [name ] = gen (ctx )
202+ self .todo .discard (name )
203+ except Exception :
204+ self .report .text (f'Failed to generate extra context "{ name } ":' )
205+ self .report .traceback ()
206+
207+
208+ # =====================================
134209# Builtin extra context implementations
135- # ===================================
210+ # =====================================
136211
137212
138213@extra_context ('markup' )
@@ -169,11 +244,22 @@ def generate(self, directive: SphinxDirective | SphinxRole) -> Any:
169244
170245@extra_context ('sphinx' )
171246class SphinxExtraContext (GlobalExtraContext ):
247+ app : Sphinx
248+
249+ @override
250+ def generate (self , env : BuildEnvironment ) -> Any :
251+ from .utils .ctxproxy import proxy
252+
253+ return proxy (self .app )
254+
255+
256+ @extra_context ('env' )
257+ class SphinxBuildEnvExtraContext (GlobalExtraContext ):
172258 @override
173259 def generate (self , env : BuildEnvironment ) -> Any :
174260 from .utils .ctxproxy import proxy
175261
176- return proxy (env . app )
262+ return proxy (env )
177263
178264
179265@extra_context ('docutils' )
@@ -189,63 +275,5 @@ def generate(self, env: BuildEnvironment) -> Any:
189275 }
190276
191277
192- # ========================
193- # Extra Context Generation
194- # ========================
195-
196-
197- class ExtraContextGenerator :
198- node : pending_node
199- report : Report
200-
201- def __init__ (self , node : pending_node ) -> None :
202- self .node = node
203- self .report = Report (
204- 'Extra Context Generation Report' ,
205- 'ERROR' ,
206- source = node .source ,
207- line = node .line ,
208- )
209- Reporter (node ).append (self .report )
210-
211- def on_anytime (self , env : BuildEnvironment ) -> None :
212- """Generate global extra context for requested names."""
213- self ._generate (GlobalExtraContext , lambda ctx : ctx .generate (env ))
214-
215- def on_parsing (self , directive : SphinxDirective | SphinxRole ) -> None :
216- """Generate parsing phase extra context for requested names."""
217- self ._generate (ParsingPhaseExtraContext , lambda ctx : ctx .generate (directive ))
218-
219- def on_parsed (self , transform : SphinxTransform ) -> None :
220- """Generate parsed phase extra context for requested names."""
221- self ._generate (ParsedPhaseExtraContext , lambda ctx : ctx .generate (transform ))
222-
223- def on_resolving (self , transform : SphinxTransform ) -> None :
224- """Generate resolving phase extra context for requested names."""
225- self ._generate (ResolvingPhaseExtraContext , lambda ctx : ctx .generate (transform ))
226-
227- def _generate (self , cls : type , gen : Callable [[ExtraContext ], Any ]) -> None :
228- """Generate extra context of the given type for all requested names."""
229- for name in self .node .template .extra :
230- ctx = REGISTRY .get (name )
231- if ctx is None :
232- self .report .text (
233- f'Extra context "{ name } " is not registered. '
234- f'Available: { REGISTRY .names } '
235- )
236- continue
237- if not isinstance (ctx , cls ):
238- self .report .text (
239- f'Extra context "{ name } " has wrong type: '
240- f'expected { cls .__name__ } , got { type (ctx ).__name__ } '
241- )
242- continue
243- try :
244- self .node .extra [name ] = gen (ctx )
245- except Exception :
246- self .report .text (f'Failed to generate extra context "{ name } ":' )
247- self .report .traceback ()
248-
249-
250278def setup (app : Sphinx ):
251- pass
279+ SphinxExtraContext . app = app
0 commit comments