diff --git a/docs/conf.py b/docs/conf.py index ffa0712..13d0e9d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -164,6 +164,7 @@ def setup(app): sys.path.insert(0, test_roots) __import__('test-extra-context.conf').conf.setup(app) + __import__('test-extra-context-params.conf').conf.setup(app) __import__('test-base-context-directive-example.conf').conf.setup(app) __import__('test-base-data-define-directive-example.conf').conf.setup(app) __import__('test-strict-data-define-directive-example.conf').conf.setup(app) diff --git a/docs/ext.rst b/docs/ext.rst index 9d6025f..e73190e 100644 --- a/docs/ext.rst +++ b/docs/ext.rst @@ -70,7 +70,10 @@ Extra contexts are registered by a :py:deco:`sphinxnotes.render.extra_context` class decorator. The decorated class must be a subclass of -:py:class:`~sphinxnotes.render.ExtraContext`. +:py:class:`~sphinxnotes.render.ExtraContext`. The ``generate()`` method +accepts the ``ExtraContextRequest`` as the first argument, plus any +positional and keyword arguments passed by the template via +``load_extra()``. .. literalinclude:: ../tests/roots/test-extra-context/conf.py :language: python @@ -88,6 +91,24 @@ The decorated class must be a subclass of {{ load_extra('cat').name }} +To accept custom parameters in your extra context, add ``*args`` and +``**kwargs`` to the ``generate()`` method signature: + +.. literalinclude:: ../tests/roots/test-extra-context-params/conf.py + :language: python + :start-after: [literalinclude start] + :end-before: [literalinclude end] + +.. example:: + :style: grid + + .. data.render:: + + {% set docs = load_extra('all_docs', 3) %} + {% for doc in docs %} + - {{ doc }} + {% endfor %} + .. _ext-filters: Extending ilters diff --git a/docs/tmpl.rst b/docs/tmpl.rst index adaed74..da1b566 100644 --- a/docs/tmpl.rst +++ b/docs/tmpl.rst @@ -159,7 +159,9 @@ which comes from the directive/role itself, extra context lets you fetch data that was prepared beforehand. Extra contexts are generated on demand. Load them in the template using the -``load_extra()`` function. +``load_extra()`` function. You can also pass positional and keyword arguments +to ``load_extra()``, which are forwarded to the extra context's +``generate()`` method. .. example:: :style: grid @@ -170,6 +172,12 @@ Extra contexts are generated on demand. Load them in the template using the Document Title: "{{ doc.title }}" + .. data.render:: + + {% set docs = load_extra('all_docs', count=3) %} + + {{ docs | join(', ') }} + Built-in Extra Contexts ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/sphinxnotes/render/ext/extractx.py b/src/sphinxnotes/render/ext/extractx.py index 7b241fe..d7684a1 100644 --- a/src/sphinxnotes/render/ext/extractx.py +++ b/src/sphinxnotes/render/ext/extractx.py @@ -29,7 +29,7 @@ @extra_context('markup') class MarkupExtraContext(ExtraContext): @override - def generate(self, req: ExtraContextRequest) -> Any: + def generate(self, req: ExtraContextRequest, *args, **kwargs) -> Any: host = req.host if not isinstance(host, (SphinxDirective, SphinxRole)): raise ValueError( @@ -47,14 +47,14 @@ def generate(self, req: ExtraContextRequest) -> Any: @extra_context('doc') class DocExtraContext(ExtraContext): @override - def generate(self, req: ExtraContextRequest) -> Any: + def generate(self, req: ExtraContextRequest, *args, **kwargs) -> Any: return proxy(HostWrapper(req.host).doctree) @extra_context('section') class SectionExtraContext(ExtraContext): @override - def generate(self, req: ExtraContextRequest) -> Any: + def generate(self, req: ExtraContextRequest, *args, **kwargs) -> Any: if req.phase == Phase.Parsing: raise ValueError( f'Extra context "section" is not available at phase {req.phase}.' @@ -73,14 +73,14 @@ def generate(self, req: ExtraContextRequest) -> Any: @extra_context('app') class SphinxAppExtraContext(ExtraContext): @override - def generate(self, req: ExtraContextRequest) -> Any: + def generate(self, req: ExtraContextRequest, *args, **kwargs) -> Any: return proxy(req.env.app) @extra_context('env') class SphinxBuildEnvExtraContext(ExtraContext): @override - def generate(self, req: ExtraContextRequest) -> Any: + def generate(self, req: ExtraContextRequest, *args, **kwargs) -> Any: return proxy(req.env) diff --git a/src/sphinxnotes/render/extractx.py b/src/sphinxnotes/render/extractx.py index a0f2034..febaaa1 100644 --- a/src/sphinxnotes/render/extractx.py +++ b/src/sphinxnotes/render/extractx.py @@ -33,7 +33,7 @@ class ExtraContext(ABC): """Base class of extra context.""" @abstractmethod - def generate(self, req: ExtraContextRequest) -> Any: ... + def generate(self, req: ExtraContextRequest, *args, **kwargs) -> Any: ... # ========================== @@ -82,7 +82,7 @@ def extra_context_names() -> set[str]: def extra_context_loader(request: ExtraContextRequest): - def load_extra(name: str) -> Any: + def load_extra(name: str, *args, **kwargs) -> Any: ctx = _REGISTRY.get(name) if ctx is None: raise ValueError( @@ -91,7 +91,7 @@ def load_extra(name: str) -> Any: ) try: - return ctx.generate(request) + return ctx.generate(request, *args, **kwargs) except Exception as e: raise ValueError(f'Failed to load extra context "{name}".') from e diff --git a/tests/roots/test-extra-context-params/conf.py b/tests/roots/test-extra-context-params/conf.py new file mode 100644 index 0000000..28760ef --- /dev/null +++ b/tests/roots/test-extra-context-params/conf.py @@ -0,0 +1,21 @@ +# [literalinclude start] +from sphinxnotes.render import ( + extra_context, + ExtraContext, + ExtraContextRequest, +) + + +@extra_context('all_docs') +class AllDocsExtraContext(ExtraContext): + def generate(self, req: ExtraContextRequest, *args, **kwargs): + count = args[0] if args else kwargs.get('count', 5) + return sorted(req.env.all_docs.keys())[:count] + + +# [literalinclude end] + +extensions = ['sphinxnotes.render.ext'] + + +def setup(app): ... diff --git a/tests/roots/test-extra-context-params/index.rst b/tests/roots/test-extra-context-params/index.rst new file mode 100644 index 0000000..c6093d5 --- /dev/null +++ b/tests/roots/test-extra-context-params/index.rst @@ -0,0 +1,6 @@ +Extra Context Params Test +========================= + +.. data.render:: + + {{ load_extra('all_docs', 3) | length }} diff --git a/tests/roots/test-extra-context/conf.py b/tests/roots/test-extra-context/conf.py index 29a4525..0a2851a 100644 --- a/tests/roots/test-extra-context/conf.py +++ b/tests/roots/test-extra-context/conf.py @@ -11,7 +11,7 @@ @extra_context('cat') class CatExtraContext(ExtraContext): - def generate(self, req: ExtraContextRequest): + def generate(self, req: ExtraContextRequest, *args, **kwargs): with open(path.join(path.dirname(__file__), 'cat.json')) as f: return json.loads(f.read()) diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 97956c0..7d8e5fa 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -59,6 +59,15 @@ def test_extra_context_custom_loader(app, status, warning): assert 'mimi' in html +@pytest.mark.sphinx('html', testroot='extra-context-params') +def test_extra_context_params(app, status, warning): + app.build() + + html = (app.outdir / 'index.html').read_text(encoding='utf-8') + + assert '3' in html + + @pytest.mark.sphinx('html', testroot='extra-context-rebuild') def test_extra_context_rebuild(app, status, warning): app.build() diff --git a/tests/test_extractx.py b/tests/test_extractx.py index fdfb365..fcc2cf9 100644 --- a/tests/test_extractx.py +++ b/tests/test_extractx.py @@ -14,7 +14,7 @@ class CountingExtraContext(ExtraContext): def __init__(self) -> None: self.calls = 0 - def generate(self, req): + def generate(self, req, *args, **kwargs): self.calls += 1 return self.calls @@ -34,3 +34,24 @@ def test_extra_context_loader_does_not_cache_values(): assert load_extra(name) == 2 finally: _REGISTRY.ctxs.pop(name, None) + + +class ParamExtraContext(ExtraContext): + def generate(self, req, *args, **kwargs): + return {'args': args, 'kwargs': kwargs} + + +def test_extra_context_loader_passes_parameters(): + name = 'test_params' + _REGISTRY.register(name, ParamExtraContext()) + + try: + node = pending_node({}, Template('')) + host = SimpleNamespace(env=SimpleNamespace()) + req = ExtraContextRequest(Template('').phase, node, host.env, host) + load_extra = extra_context_loader(req) + + result = load_extra(name, 10, limit=20) + assert result == {'args': (10,), 'kwargs': {'limit': 20}} + finally: + _REGISTRY.ctxs.pop(name, None)