Skip to content

Commit 70f888e

Browse files
SilverRainZMiMoCode
andcommitted
refactor: Clean up registry APIs
- Expose ExtraContextRegistry with add() method, remove internal get/get_names - Add JinjaRegistry with add_filter() and add_extension() methods - Update Registry class with extra_context and jinja_env properties - Move default Jinja extensions to setup() for proper initialization - Update documentation and tests Co-authored-by: MiMoCode <mimo@xiaomi.com>
1 parent 3652e03 commit 70f888e

8 files changed

Lines changed: 140 additions & 56 deletions

File tree

docs/api.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Base Directive Classes
2929

3030
.. autoclass:: sphinxnotes.render.BaseContextDirective
3131
:show-inheritance:
32-
:members: process_pending_node, queue_pending_node, current_raw_data, current_context, current_template
32+
:members: process_pending_node, queue_pending_node, current_context, current_template
3333

3434
.. autoclass:: sphinxnotes.render.BaseDataDefineDirective
3535
:show-inheritance:
@@ -104,11 +104,19 @@ See :doc:`tmpl` for built-in extra-context names such as ``doc`` and
104104
:members:
105105
:undoc-members:
106106

107+
.. autoclass:: sphinxnotes.render.ExtraContextRegistry
108+
:members:
109+
:undoc-members:
110+
107111
Filters
108112
=======
109113

110114
.. autodecorator:: sphinxnotes.render.filter
111115

116+
.. autoclass:: sphinxnotes.render.JinjaRegistry
117+
:members:
118+
:undoc-members:
119+
112120
Data, Field and Schema
113121
======================
114122

@@ -148,3 +156,7 @@ or add new extra context) by adding new items to
148156
.. autoclass:: sphinxnotes.render.Registry
149157

150158
.. autoproperty:: data
159+
160+
.. autoproperty:: extra_context
161+
162+
.. autoproperty:: jinja_env

src/sphinxnotes/render/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
extra_context,
2929
ExtraContext,
3030
ExtraContextRequest,
31+
ExtraContextRegistry,
32+
REGISTRY as EXTRA_CONTEXT_REGISTRY,
3133
)
3234
from .pipeline import BaseContextRole, BaseContextDirective
3335
from .sources import (
@@ -36,7 +38,7 @@
3638
BaseDataDefineDirective,
3739
StrictDataDefineDirective,
3840
)
39-
from .jinja import filter
41+
from .jinja import filter, JinjaRegistry, REGISTRY as JINJA_REGISTRY
4042

4143
if TYPE_CHECKING:
4244
from sphinx.application import Sphinx
@@ -58,6 +60,8 @@
5860
'ResolvedContext',
5961
'ExtraContext',
6062
'ExtraContextRequest',
63+
'ExtraContextRegistry',
64+
'EXTRA_CONTEXT_REGISTRY',
6165
'extra_context',
6266
'pending_node',
6367
'BaseContextRole',
@@ -67,6 +71,8 @@
6771
'BaseDataDefineDirective',
6872
'StrictDataDefineDirective',
6973
'filter',
74+
'JinjaRegistry',
75+
'JINJA_REGISTRY',
7076
]
7177

7278

@@ -77,6 +83,14 @@ class Registry:
7783
def data(self) -> DataRegistry:
7884
return DATA_REGISTRY
7985

86+
@property
87+
def extra_context(self) -> ExtraContextRegistry:
88+
return EXTRA_CONTEXT_REGISTRY
89+
90+
@property
91+
def jinja_env(self) -> JinjaRegistry:
92+
return JINJA_REGISTRY
93+
8094

8195
REGISTRY = Registry()
8296

src/sphinxnotes/render/ctxnodes.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,30 @@
2929
class pending_node(nodes.Element):
3030
"""A docutils node to be rendered."""
3131

32-
# The context to be rendered by Jinja template.
32+
#: The context to be rendered by Jinja template.
3333
ctx: UnresolvedContext | ResolvedContext
3434
#: Jinja template for rendering the context.
3535
template: Template
3636
#: Whether rendering to inline nodes.
3737
inline: bool
3838
#: Whether the rendering pipeline is finished (failed is also finished).
3939
rendered: bool
40-
#: Stored pickling error for later-phase unresolved context.
40+
41+
# Stored pickling error for later-phase unresolved context.
4142
_ctx_pickle_error: Exception | None
4243

44+
# Types for hook functions.
45+
type UnresolvedContextHook = Callable[[pending_node, UnresolvedContext], None]
46+
type ResolvedContextHook = Callable[[pending_node, ResolvedContext], None]
47+
type MarkupTextHook = Callable[[pending_node, str], str]
48+
type RenderedNodesHook = Callable[[pending_node, list[nodes.Node]], None]
49+
50+
# Hooks for processing render intermediate products.
51+
_unresolved_context_hooks: list[UnresolvedContextHook]
52+
_resolved_context_hooks: list[ResolvedContextHook]
53+
_markup_text_hooks: list[MarkupTextHook]
54+
_rendered_nodes_hooks: list[RenderedNodesHook]
55+
4356
def __init__(
4457
self,
4558
ctx: UnresolvedContext | ResolvedContext,
@@ -50,16 +63,18 @@ def __init__(
5063
**attributes,
5164
) -> None:
5265
super().__init__(rawsource, *children, **attributes)
66+
self.ctx = ctx
67+
self.template = tmpl
68+
self.inline = inline
69+
self.rendered = False
70+
71+
# Test whehter ctx pickle-able.
5372
self._ctx_pickle_error = None
5473
if isinstance(ctx, UnresolvedContext) and tmpl.phase != Phase.Parsing:
5574
try:
5675
pickle.dumps(ctx)
5776
except Exception as exc:
5877
self._ctx_pickle_error = exc
59-
self.ctx = ctx
60-
self.template = tmpl
61-
self.inline = inline
62-
self.rendered = False
6378

6479
# Init hook lists.
6580
self._unresolved_context_hooks = []
@@ -241,16 +256,6 @@ def unwrap_and_replace_self_inline(self, inliner: Report.Inliner) -> None:
241256

242257
"""Hooks for processing render intermediate products."""
243258

244-
type UnresolvedContextHook = Callable[[pending_node, UnresolvedContext], None]
245-
type ResolvedContextHook = Callable[[pending_node, ResolvedContext], None]
246-
type MarkupTextHook = Callable[[pending_node, str], str]
247-
type RenderedNodesHook = Callable[[pending_node, list[nodes.Node]], None]
248-
249-
_unresolved_context_hooks: list[UnresolvedContextHook]
250-
_resolved_context_hooks: list[ResolvedContextHook]
251-
_markup_text_hooks: list[MarkupTextHook]
252-
_rendered_nodes_hooks: list[RenderedNodesHook]
253-
254259
def hook_unresolved_context(self, hook: UnresolvedContextHook) -> None:
255260
self._unresolved_context_hooks.append(hook)
256261

src/sphinxnotes/render/extractx.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,34 @@ def generate(self, req: ExtraContextRequest, *args, **kwargs) -> Any: ...
4141
# ==========================
4242

4343

44-
class _ExtraContextRegistry:
45-
ctxs: dict[str, ExtraContext]
44+
class ExtraContextRegistry:
45+
"""Registry for extra contexts."""
46+
47+
_ctxs: dict[str, ExtraContext]
4648

4749
def __init__(self) -> None:
48-
self.ctxs = {}
50+
self._ctxs = {}
4951

50-
def register(self, name: str, ctx: ExtraContext) -> None:
51-
if name in self.ctxs:
52-
raise ValueError(f'Extra context "{name}" already registered')
53-
self.ctxs[name] = ctx
52+
def add(self, name: str, ctx: ExtraContext) -> None:
53+
"""Register an extra context.
5454
55-
def get(self, name: str) -> ExtraContext | None:
56-
if name not in self.ctxs:
57-
return None
58-
return self.ctxs[name]
55+
:param name: The context name, used in templates via ``load_extra('name')``
56+
:param ctx: An :py:class:`ExtraContext` instance
57+
58+
.. note:: Using the :py:deco:`extra_context` decorator is recommended for most cases.
59+
"""
60+
if name in self._ctxs:
61+
raise ValueError(f'Extra context "{name}" already registered')
62+
self._ctxs[name] = ctx
5963

60-
def get_names(self) -> set[str]:
61-
return set(self.ctxs.keys())
6264

65+
REGISTRY = ExtraContextRegistry()
66+
"""The global registry for extra contexts.
6367
64-
_REGISTRY = _ExtraContextRegistry()
68+
This is the underlying registry used by the :py:func:`extra_context` decorator.
69+
Using the decorator is recommended for most cases, but you can also register
70+
extra contexts directly via this registry.
71+
"""
6572

6673

6774
def extra_context(name: str):
@@ -71,19 +78,19 @@ def extra_context(name: str):
7178
"""
7279

7380
def decorator(cls):
74-
_REGISTRY.register(name, cls())
81+
REGISTRY.add(name, cls())
7582
return cls
7683

7784
return decorator
7885

7986

8087
def extra_context_names() -> set[str]:
81-
return _REGISTRY.get_names()
88+
return set(REGISTRY._ctxs.keys())
8289

8390

8491
def extra_context_loader(request: ExtraContextRequest):
8592
def load_extra(name: str, *args, **kwargs) -> Any:
86-
ctx = _REGISTRY.get(name)
93+
ctx = REGISTRY._ctxs.get(name)
8794
if ctx is None:
8895
raise ValueError(
8996
f'Extra context "{name}" is not registered. '

src/sphinxnotes/render/jinja.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from jinja2 import StrictUndefined, DebugUndefined
1717

1818
from .data import ParsedData
19-
from .utils import Report
2019

2120
if TYPE_CHECKING:
2221
from typing import Any
@@ -25,6 +24,61 @@
2524
from .ctx import ResolvedContext
2625

2726

27+
class JinjaRegistry:
28+
"""Registry for customizing the Jinja2 environment.
29+
30+
Provides methods to add custom filters and extensions to the Jinja2
31+
rendering environment used by this extension.
32+
33+
Usage::
34+
35+
from sphinxnotes.render.jinja import REGISTRY
36+
37+
def my_filter_factory(env):
38+
def _filter(value):
39+
return value.upper()
40+
return _filter
41+
42+
REGISTRY.add_filter('my_filter', my_filter_factory)
43+
REGISTRY.add_extension('jinja2.ext.i18n')
44+
"""
45+
46+
_filters: dict[str, Callable[[BuildEnvironment], Callable]]
47+
_extensions: list[str]
48+
49+
def __init__(self) -> None:
50+
self._filters = {}
51+
self._extensions = []
52+
53+
def add_filter(
54+
self, name: str, factory: Callable[[BuildEnvironment], Callable]
55+
) -> None:
56+
"""Register a filter factory.
57+
58+
:param name: The filter name, used in Jinja templates as ``{{ value|name }}``
59+
:param factory: A callable that takes a :py:class:`~sphinx.environment.BuildEnvironment`
60+
and returns a filter callable
61+
62+
.. note:: Using the :py:deco:`filter` decorator is recommended for most cases.
63+
64+
"""
65+
self._filters[name] = factory
66+
67+
def add_extension(self, extension: str) -> None:
68+
"""Add a Jinja2 extension.
69+
70+
See `Jinja2 Extensions <https://jinja.palletsprojects.com/en/stable/extensions/>`_
71+
for available bulitin extensions.
72+
73+
:param extension: The extension module path, e.g. ``'jinja2.ext.i18n'``
74+
"""
75+
self._extensions.append(extension)
76+
77+
78+
REGISTRY = JinjaRegistry()
79+
"""The global registry for Jinja2 filter factories."""
80+
81+
2882
@dataclass
2983
class TemplateRenderer:
3084
text: str
@@ -48,10 +102,7 @@ def render(
48102
return self._render(ctx, debug=debug)
49103

50104
def _render(self, ctx: dict[str, Any], debug: bool = False) -> str:
51-
extensions = [
52-
'jinja2.ext.loopcontrols', # enable {% break %}, {% continue %}
53-
'jinja2.ext.do', # enable {% do ... %}
54-
]
105+
extensions = list(REGISTRY._extensions)
55106
if debug:
56107
extensions.append('jinja2.ext.debug')
57108

@@ -63,27 +114,19 @@ def _render(self, ctx: dict[str, Any], debug: bool = False) -> str:
63114

64115
return env.from_string(self.text).render(ctx)
65116

66-
def _report_self(self, reporter: Report) -> None:
67-
reporter.code(self.text, lang='jinja', caption='Template:')
68-
69117

70118
class _JinjaEnv(SandboxedEnvironment):
71119
_env: ClassVar[BuildEnvironment]
72-
_filter_factories: ClassVar[dict[str, Callable[[BuildEnvironment], Callable]]] = {}
73120

74121
def __init__(self, *args, **kwargs):
75122
super().__init__(*args, **kwargs)
76-
for name, factory in self._filter_factories.items():
123+
for name, factory in REGISTRY._filters.items():
77124
self.filters[name] = factory(self._env)
78125

79126
@classmethod
80127
def on_builder_inited(cls, app: Sphinx):
81128
cls._env = app.env
82129

83-
@classmethod
84-
def add_filter(cls, name: str, factory: Callable[[BuildEnvironment], Callable]):
85-
cls._filter_factories[name] = factory
86-
87130
@override
88131
def is_safe_attribute(self, obj, attr, value=None):
89132
"""
@@ -111,11 +154,14 @@ def _filter(value):
111154
"""
112155

113156
def decorator(ff):
114-
_JinjaEnv.add_filter(name, ff)
157+
REGISTRY.add_filter(name, ff)
115158
return ff
116159

117160
return decorator
118161

119162

120163
def setup(app: Sphinx):
121164
app.connect('builder-inited', _JinjaEnv.on_builder_inited)
165+
166+
REGISTRY.add_extension('jinja2.ext.loopcontrols')
167+
REGISTRY.add_extension('jinja2.ext.do')

tests/roots/test-extra-context-params/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ def generate(self, req: ExtraContextRequest, *args, **kwargs):
1919

2020
keep_warnings = True
2121

22+
2223
def setup(app): ...

tests/test_e2e.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ def test_extra_context_params(app, status, warning):
6868
assert '[&#39;index&#39;]' in html
6969

7070

71-
7271
@pytest.mark.sphinx('html', testroot='extra-context-rebuild')
7372
def test_extra_context_rebuild(app, status, warning):
7473
app.build()

0 commit comments

Comments
 (0)