Skip to content

Commit dc0f8d5

Browse files
committed
Add Environment.extract_parsed_names to support tracking dynamic inheritance or inclusion
1 parent ae53ea5 commit dc0f8d5

5 files changed

Lines changed: 65 additions & 1 deletion

File tree

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Version 3.2.0
55

66
Unreleased
77

8+
- Add ``Environment.extract_parsed_names`` to support tracking dynamic
9+
inheritance or inclusion. :issue:`1776`
10+
811

912
Version 3.1.2
1013
-------------

docs/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ useful if you want to dig deeper into Jinja or :ref:`develop extensions
140140

141141
.. automethod:: overlay([options])
142142

143+
.. automethod:: extract_parsed_names()
144+
143145
.. method:: undefined([hint, obj, name, exc])
144146

145147
Creates a new :class:`Undefined` object for `name`. This is useful

src/jinja2/environment.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ class Environment:
259259
`enable_async`
260260
If set to true this enables async template execution which
261261
allows using async functions and generators.
262+
263+
`remember_parsed_names`
264+
Should we remember parsed names? This is useful for dynamic
265+
dependency tracking, see `extract_parsed_names` for details.
266+
Default is ``False``.
262267
"""
263268

264269
#: if this environment is sandboxed. Modifying this variable won't make
@@ -313,6 +318,7 @@ def __init__(
313318
auto_reload: bool = True,
314319
bytecode_cache: t.Optional["BytecodeCache"] = None,
315320
enable_async: bool = False,
321+
remember_parsed_names: bool = False,
316322
):
317323
# !!Important notice!!
318324
# The constructor accepts quite a few arguments that should be
@@ -365,6 +371,11 @@ def __init__(
365371
self.is_async = enable_async
366372
_environment_config_check(self)
367373

374+
# dependency tracking
375+
self.parsed_names: t.Optional[t.List[str]] = (
376+
[] if remember_parsed_names else None
377+
)
378+
368379
def add_extension(self, extension: t.Union[str, t.Type["Extension"]]) -> None:
369380
"""Adds an extension after the environment was created.
370381
@@ -614,8 +625,36 @@ def _parse(
614625
self, source: str, name: t.Optional[str], filename: t.Optional[str]
615626
) -> nodes.Template:
616627
"""Internal parsing function used by `parse` and `compile`."""
628+
if name is not None and self.parsed_names is not None:
629+
self.parsed_names.append(name)
617630
return Parser(self, source, name, filename).parse()
618631

632+
def extract_parsed_names(self) -> t.Optional[t.List[str]]:
633+
"""Return all template names that have been parsed so far, and clear the list.
634+
635+
This is enabled if `remember_parsed_names = True` was passed to the
636+
`Environment` constructor, otherwise it returns `None`. It can be used
637+
after `Template.render()` to extract dependency information. Compared
638+
to `jinja2.meta.find_referenced_templates()`, it:
639+
640+
a. works on dynamic inheritance and includes
641+
b. does not work unless and until you actually render the template
642+
643+
Many buildsystems are unable to support (b), but some do e.g. [1], the
644+
key point being that if the target file does not exist, dependency
645+
information is not needed since the target file must be built anyway.
646+
In such cases, you may prefer this function due to (a).
647+
648+
[1] https://make.mad-scientist.net/papers/advanced-auto-dependency-generation/
649+
650+
.. versionadded:: 3.2
651+
"""
652+
if self.parsed_names is None:
653+
return None
654+
names = self.parsed_names[:]
655+
self.parsed_names.clear()
656+
return names
657+
619658
def lex(
620659
self,
621660
source: str,

src/jinja2/meta.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ def find_referenced_templates(ast: nodes.Template) -> t.Iterator[t.Optional[str]
7171
['layout.html', None]
7272
7373
This function is useful for dependency tracking. For example if you want
74-
to rebuild parts of the website after a layout template has changed.
74+
to rebuild parts of the website after a layout template has changed. For
75+
an alternative method with different pros and cons, see
76+
`Environment.extract_parsed_names()`.
7577
"""
7678
template_name: t.Any
7779

tests/test_api.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ def test_item_and_attribute(self, env):
3636
tmpl = env.from_string('{{ foo["items"] }}')
3737
assert tmpl.render(foo={"items": 42}) == "42"
3838

39+
def test_extract_parsed_names(self, env):
40+
templates = DictLoader(
41+
{
42+
"main": "{% set tpl = 'ba' + 'se' %}{% extends tpl %}",
43+
"base": "{% set tpl = 'INC' %}{% include tpl.lower() %}",
44+
"inc": "whatever",
45+
}
46+
)
47+
env.loader = templates
48+
assert env.get_template("main").render() == "whatever"
49+
assert env.extract_parsed_names() is None
50+
51+
env = Environment(remember_parsed_names=True)
52+
env.loader = templates
53+
assert env.get_template("main").render() == "whatever"
54+
assert env.extract_parsed_names() == ["main", "base", "inc"]
55+
assert env.extract_parsed_names() == []
56+
3957
def test_finalize(self):
4058
e = Environment(finalize=lambda v: "" if v is None else v)
4159
t = e.from_string("{% for item in seq %}|{{ item }}{% endfor %}")

0 commit comments

Comments
 (0)