Skip to content

Commit 445b555

Browse files
SilverRainZMiMoCodeDeepSeek
committed
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> Co-authored-by: DeepSeek <service@deepseek.com>
1 parent 3652e03 commit 445b555

9 files changed

Lines changed: 151 additions & 61 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

src/sphinxnotes/render/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
extra_context,
2929
ExtraContext,
3030
ExtraContextRequest,
31+
Registry as 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, Registry as JinjaRegistry, REGISTRY as JINJA_REGISTRY
4042

4143
if TYPE_CHECKING:
4244
from sphinx.application import Sphinx
@@ -45,6 +47,7 @@
4547
"""Python API for other Sphinx extensions."""
4648
__all__ = [
4749
'Registry',
50+
'DataRegistry',
4851
'PlainValue',
4952
'Value',
5053
'ValueWrapper',
@@ -58,6 +61,8 @@
5861
'ResolvedContext',
5962
'ExtraContext',
6063
'ExtraContextRequest',
64+
'ExtraContextRegistry',
65+
'EXTRA_CONTEXT_REGISTRY',
6166
'extra_context',
6267
'pending_node',
6368
'BaseContextRole',
@@ -67,6 +72,8 @@
6772
'BaseDataDefineDirective',
6873
'StrictDataDefineDirective',
6974
'filter',
75+
'JinjaRegistry',
76+
'JINJA_REGISTRY',
7077
]
7178

7279

@@ -77,6 +84,14 @@ class Registry:
7784
def data(self) -> DataRegistry:
7885
return DATA_REGISTRY
7986

87+
@property
88+
def extra_context(self) -> ExtraContextRegistry:
89+
return EXTRA_CONTEXT_REGISTRY
90+
91+
@property
92+
def jinja(self) -> JinjaRegistry:
93+
return JINJA_REGISTRY
94+
8095

8196
REGISTRY = Registry()
8297

src/sphinxnotes/render/ctxnodes.py

Lines changed: 21 additions & 18 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 = []
@@ -239,18 +254,6 @@ def unwrap_and_replace_self_inline(self, inliner: Report.Inliner) -> None:
239254
# Replace self with inline nodes.
240255
self.replace_self(ns)
241256

242-
"""Hooks for processing render intermediate products."""
243-
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-
254257
def hook_unresolved_context(self, hook: UnresolvedContextHook) -> None:
255258
self._unresolved_context_hooks.append(hook)
256259

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 Registry:
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 = Registry()
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: 64 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,64 @@
2524
from .ctx import ResolvedContext
2625

2726

27+
class Registry:
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+
if name in self._filters:
66+
raise ValueError(f'Jinja filter "{name}" already registered')
67+
self._filters[name] = factory
68+
69+
def add_extension(self, extension: str) -> None:
70+
"""Add a Jinja2 extension.
71+
72+
See `Jinja2 Extensions <https://jinja.palletsprojects.com/en/stable/extensions/>`_
73+
for available builtin extensions.
74+
75+
:param extension: The extension module path, e.g. ``'jinja2.ext.i18n'``
76+
"""
77+
if extension not in self._extensions:
78+
self._extensions.append(extension)
79+
80+
81+
REGISTRY = Registry()
82+
"""The global registry for Jinja2 filter factories."""
83+
84+
2885
@dataclass
2986
class TemplateRenderer:
3087
text: str
@@ -48,10 +105,7 @@ def render(
48105
return self._render(ctx, debug=debug)
49106

50107
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-
]
108+
extensions = list(REGISTRY._extensions)
55109
if debug:
56110
extensions.append('jinja2.ext.debug')
57111

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

64118
return env.from_string(self.text).render(ctx)
65119

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

70121
class _JinjaEnv(SandboxedEnvironment):
71122
_env: ClassVar[BuildEnvironment]
72-
_filter_factories: ClassVar[dict[str, Callable[[BuildEnvironment], Callable]]] = {}
73123

74124
def __init__(self, *args, **kwargs):
75125
super().__init__(*args, **kwargs)
76-
for name, factory in self._filter_factories.items():
126+
for name, factory in REGISTRY._filters.items():
77127
self.filters[name] = factory(self._env)
78128

79129
@classmethod
80130
def on_builder_inited(cls, app: Sphinx):
81131
cls._env = app.env
82132

83-
@classmethod
84-
def add_filter(cls, name: str, factory: Callable[[BuildEnvironment], Callable]):
85-
cls._filter_factories[name] = factory
86-
87133
@override
88134
def is_safe_attribute(self, obj, attr, value=None):
89135
"""
@@ -111,11 +157,14 @@ def _filter(value):
111157
"""
112158

113159
def decorator(ff):
114-
_JinjaEnv.add_filter(name, ff)
160+
REGISTRY.add_filter(name, ff)
115161
return ff
116162

117163
return decorator
118164

119165

120166
def setup(app: Sphinx):
121167
app.connect('builder-inited', _JinjaEnv.on_builder_inited)
168+
169+
REGISTRY.add_extension('jinja2.ext.loopcontrols')
170+
REGISTRY.add_extension('jinja2.ext.do')

src/sphinxnotes/render/markup.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ def _render(self, text: str) -> list[Node]:
5555
parser = self.host.app.registry.create_source_parser(
5656
self.host.app, 'rst'
5757
)
58-
doc = new_document(self.host.env.docname, settings=self._get_settings(parser, self.host.document))
58+
doc = new_document(
59+
self.host.env.docname,
60+
settings=self._get_settings(parser, self.host.document),
61+
)
5962
parser.parse(text, doc)
6063

6164
# NOTE: Nodes produced by standalone source parser should be fixed
@@ -128,8 +131,9 @@ def _get_settings(self, parser: SphinxParser, doctree: nodes.document) -> Values
128131
if version_info[0] >= 9:
129132
try:
130133
from sphinx.util.docutils import _get_settings
131-
settings = _get_settings(parser,
132-
defaults=self.host.env.settings, read_config_files=True
134+
135+
settings = _get_settings(
136+
parser, defaults=self.host.env.settings, read_config_files=True
133137
)
134138
except Exception as e:
135139
logger.warning(

0 commit comments

Comments
 (0)