Skip to content

Commit dee0acc

Browse files
SilverRainZMiMoCode
andauthored
feat: Simplify filter decorator with pass_build_env option (#23)
The `@filter` decorator now supports a pass_build_env parameter. When set to True, the filter receives BuildEnvironment as its first argument, eliminating the need for factory functions. Co-authored-by: MiMoCode <mimo@xiaomi.com>
1 parent 4fa86d5 commit dee0acc

5 files changed

Lines changed: 62 additions & 48 deletions

File tree

docs/ext.rst

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,30 +109,37 @@ To accept custom parameters in your extra context, add ``*args`` and
109109
110110
.. _ext-filters:
111111

112-
Extending ilters
113-
=================
112+
Extending Filters
113+
==================
114114

115115
Template filters are registered by a
116116
:py:deco:`sphinxnotes.render.filter` function decorator.
117117

118-
The decorated function takes a :py:class:`sphinx.environment.BuildEnvironment`
119-
as argument and returns a filter function.
118+
.. literalinclude:: ../tests/roots/test-filter-example/conf.py
119+
:language: python
120+
:start-after: [literalinclude catify-start]
121+
:end-before: [literalinclude catify-end]
120122

121-
.. note::
123+
.. example::
122124

123-
The decorator is used to **decorate the filter function factory, NOT
124-
the filter function itself**.
125+
.. data.render::
126+
127+
{{ "Hello world" | catify }}
128+
129+
If your filter needs access to the Sphinx build environment
130+
:py:class:`sphinx.environment.BuildEnvironment`
131+
(e.g., to access configuration or document metadata), use ``pass_build_env=True``:
125132

126133
.. literalinclude:: ../tests/roots/test-filter-example/conf.py
127134
:language: python
128-
:start-after: [literalinclude start]
129-
:end-before: [literalinclude end]
135+
:start-after: [literalinclude author-start]
136+
:end-before: [literalinclude author-end]
130137

131138
.. example::
132139

133140
.. data.render::
134141
135-
{{ "Hello world" | catify }}
142+
{{ "author: Hello world" | format_author }}
136143
137144
.. _ext-directives:
138145
.. _ext-roles:

docs/tmpl.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ The following extra contexts are available:
220220
221221
{%
222222
set m = load_extra('markup')
223-
| jsonify
223+
| jsonify(indent=2)
224224
%}
225225
226226
.. code::
@@ -295,7 +295,7 @@ __ https://jinja.palletsprojects.com/en/stable/templates/#builtin-filters
295295
{% set text = {'name': 'mimi'} %}
296296
297297
:Strify: ``{{ text }}``
298-
:JSONify: ``{{ text | jsonify | replace('\n', '')}}``
298+
:JSONify: ``{{ text | jsonify }}``
299299
300300
.. seealso:: :ref:`ext-filters`
301301

src/sphinxnotes/render/ext/filters.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,25 @@
1717

1818
if TYPE_CHECKING:
1919
from sphinx.application import Sphinx
20-
from sphinx.environment import BuildEnvironment
2120

2221

2322
@filter('roles')
24-
def roles(_: BuildEnvironment):
25-
"""
26-
Converting list of string to list of reStructuredText role.
23+
def roles(value: Iterable[str], role: str) -> Iterable[str]:
24+
"""Converting list of string to list of reStructuredText role.
2725
2826
For example::
2927
3028
{{ ["foo", "bar"] | roles("doc") }}
3129
3230
Produces ``[":doc:`foo`", ":doc:`bar`"]``.
3331
"""
34-
35-
def _filter(value: Iterable[str], role: str) -> Iterable[str]:
36-
return map(lambda x: ':%s:`%s`' % (role, x), value)
37-
38-
return _filter
32+
return map(lambda x, role=role: ':%s:`%s`' % (role, x), value)
3933

4034

4135
@filter('jsonify')
42-
def jsonify(_: BuildEnvironment):
36+
def jsonify(value: Any, indent: str | None = None) -> Any:
4337
"""Converting value to JSON."""
44-
45-
def _filter(value: Any) -> Any:
46-
return json.dumps(value, indent=' ')
47-
48-
return _filter
38+
return json.dumps(value, indent=indent)
4939

5040

5141
def setup(app: Sphinx):

src/sphinxnotes/render/jinja.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,30 @@ class JinjaRegistry:
3131
rendering environment used by this extension.
3232
"""
3333

34-
_filters: dict[str, Callable[[BuildEnvironment], Callable]]
34+
_filters: dict[str, tuple[Callable, bool]] # (func, pass_build_env)
3535
_extensions: list[str]
3636

3737
def __init__(self) -> None:
3838
self._filters = {}
3939
self._extensions = []
4040

4141
def add_filter(
42-
self, name: str, factory: Callable[[BuildEnvironment], Callable]
42+
self, name: str, func: Callable, pass_build_env: bool = False
4343
) -> None:
44-
"""Register a filter factory.
44+
"""Register a filter.
4545
4646
:param name: The filter name, used in Jinja templates as ``{{ value|name }}``
47-
:param factory: A callable that takes a :py:class:`~sphinx.environment.BuildEnvironment`
48-
and returns a filter callable
47+
:param func: The filter callable
48+
:param pass_build_env: If True, filter receives
49+
:py:class:`sphinx.environment.BuildEnvironment`
50+
as first arg
4951
5052
.. note:: Using the :py:deco:`filter` decorator is recommended for most cases.
5153
5254
"""
5355
if name in self._filters:
5456
raise ValueError(f'Jinja filter "{name}" already registered')
55-
self._filters[name] = factory
57+
self._filters[name] = (func, pass_build_env)
5658

5759
def add_extension(self, extension: str) -> None:
5860
"""Add a Jinja2 extension.
@@ -111,8 +113,14 @@ class _JinjaEnv(SandboxedEnvironment):
111113

112114
def __init__(self, *args, **kwargs):
113115
super().__init__(*args, **kwargs)
114-
for name, factory in REGISTRY._filters.items():
115-
self.filters[name] = factory(self._env)
116+
for name, (func, pass_build_env) in REGISTRY._filters.items():
117+
if pass_build_env:
118+
env = self._env
119+
self.filters[name] = lambda value, *args, _func=func, **kwargs: _func(
120+
env, value, *args, **kwargs
121+
)
122+
else:
123+
self.filters[name] = func
116124

117125
@classmethod
118126
def on_builder_inited(cls, app: Sphinx):
@@ -132,20 +140,22 @@ def is_safe_attribute(self, obj, attr, value=None):
132140
return super().is_safe_attribute(obj, attr, value)
133141

134142

135-
def filter(name: str):
143+
def filter(name: str, pass_build_env: bool = False):
136144
"""Decorator for adding a filter to the Jinja environment.
137145
138146
Usage::
139147
140148
@filter('my_filter')
141-
def my_filter(env: BuildEnvironment):
142-
def _filter(value):
143-
return value.upper()
144-
return _filter
149+
def my_filter(value):
150+
return value.upper()
151+
152+
@filter('my_filter_with_env', pass_build_env=True)
153+
def my_filter_with_env(env: BuildEnvironment, value):
154+
return value.upper()
145155
"""
146156

147157
def decorator(ff):
148-
REGISTRY.add_filter(name, ff)
158+
REGISTRY.add_filter(name, ff, pass_build_env)
149159
return ff
150160

151161
return decorator

tests/roots/test-filter-example/conf.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,27 @@
22
from sphinx.environment import BuildEnvironment
33

44

5-
# [literalinclude start]
5+
# fmt: off
6+
# [literalinclude catify-start]
67
@filter('catify')
7-
def catify(_: BuildEnvironment):
8+
def catify(value: str) -> str:
89
"""Speak in a cat-like tone"""
10+
return value + ', meow~'
11+
# [literalinclude catify-end]
12+
# fmt: on
913

10-
def _filter(value: str) -> str:
11-
return value + ', meow~'
1214

13-
return _filter
15+
# fmt: off
16+
# [literalinclude author-start]
17+
@filter('format_author', pass_build_env=True)
18+
def format_author(env: BuildEnvironment, value: str) -> str:
19+
"""Replace 'author' in value with the Sphinx document author"""
20+
return value.replace('author', env.config.author)
21+
# [literalinclude author-end]
22+
# fmt: on
1423

1524

1625
# [literalinclude end]
17-
18-
1926
extensions = ['sphinxnotes.render.ext']
2027

2128

0 commit comments

Comments
 (0)