Skip to content

Commit 994f709

Browse files
SilverRainZMiMo v2 Pro
andcommitted
refactor: Simplify ExtraContext and improve generation logic
- ExtraContext is now a type alias instead of ABC - Each phase class is its own ABC - ExtraContextGenerator validates requested contexts in __init__ - Uses todo set to track and generate only requested contexts Co-Authored-By: MiMo v2 Pro <mimo@xiaomi.com>
1 parent ec3341d commit 994f709

2 files changed

Lines changed: 133 additions & 102 deletions

File tree

docs/api.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,19 @@ See :doc:`tmpl` for built-in extra-context names such as ``doc`` and
8787

8888
.. autoclass:: sphinxnotes.render.ParsingPhaseExtraContext
8989
:members: generate
90+
:undoc-members:
9091

9192
.. autoclass:: sphinxnotes.render.ParsedPhaseExtraContext
9293
:members: generate
94+
:undoc-members:
9395

9496
.. autoclass:: sphinxnotes.render.ResolvingPhaseExtraContext
9597
:members: generate
98+
:undoc-members:
9699

97100
.. autoclass:: sphinxnotes.render.GlobalExtraContext
98101
:members: generate
99-
102+
:undoc-members:
100103

101104
Base Roles and Directives
102105
-------------------------

src/sphinxnotes/render/extractx.py

Lines changed: 129 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,20 @@
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

102118
def 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')
171246
class 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-
250278
def setup(app: Sphinx):
251-
pass
279+
SphinxExtraContext.app = app

0 commit comments

Comments
 (0)