Skip to content

fix(templates): collect each template's filters once per set of overridden blocks - #282

Merged
SizzleUnrlsd merged 7 commits into
mainfrom
fix/279-filter-walk
Oct 23, 2026
Merged

SizzleUnrlsd merged 7 commits into
mainfrom
fix/279-filter-walk

Conversation

@SizzleUnrlsd

@SizzleUnrlsd SizzleUnrlsd commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #279.

To collect the filters a render reaches, project_templates followed every path through includes and extends (_reaching). Two includes of one template at each of n levels made it follow 2^n paths, and every directory check runs that walk.

Each template is now walked once for each set of blocks the templates extending it override:

  • Same result: a walk keeps its first occurrences, in order, and maps them through each reference. That gives the same filters in the same order as a walk path by path.
  • One analysis only: the results are kept in a dictionary that each call to project_templates creates, so projects analysed one after the other in one process do not share them.
  • Cycles: a walk cut short by a cycle of includes depends on where the cycle was entered. It is not kept, and it is walked again where it is needed.
  • Order of the walk: a walk keeps the templates it went through. It is reused only where none of them is on the way; elsewhere a cycle would cut it, so it is walked again. The filters do not depend on which template is walked first.

Limit. Templates in a cycle of includes are still walked again wherever the cycle cuts their walk. With two includes of one template at each level of such a cycle, the cost stays exponential, as on main.

Commit Change
test(templates): pin the filters reached through overridden blocks, cycles and successive analyses the results to keep, those of main
fix(templates): collect each template's filters once per set of overridden blocks the walk, and its test of time
docs(changelog): announce the linear walk of template filters CHANGELOG.md
refactor(templates): return a template's walk of filters as a named tuple _Walk, no behavior change
fix(templates): make the filters of a template independent of the order of the walk the templates a walk went through, and the case of the review
test(templates): compare the walk of template filters with a walk path by path 1,500 generated projects against the walk of main
docs(changelog): state that templates in a cycle of includes are walked again the limit above

Review

A walk that no cycle cut was reused even where a template it had gone through was on the way. There, a walk path by path is cut. In the case of the review:

  • the child a.html extends b.html and overrides its body;
  • b.html includes a.html with v=other in that block.

Walked from b.html, a.html stops at b.html. When a.html was walked first, its kept walk was reused inside b.html, which then got striptags reached with other. When b.html was walked first, it did not. The walk now keeps the templates it went through, and is reused only where none of them is on the way.

Results

Time. level0.html holds {{ name|upper }}, and level{n}.html includes level{n-1}.html twice:

Levels main This change
12 0.08 s 0.00 s
16 1.87 s 0.00 s
18 7.19 s 0.00 s
20 37.19 s 0.00 s
40 more than 60 s 0.01 s

The same, with level0.html also including the top level, which closes a cycle through every level:

Levels main This change
12 0.15 s 0.14 s
16 3.12 s 3.14 s
18 15.38 s 12.74 s
20 more than 60 s 57.23 s

Same filters.

Tests

The new file tests/test_template_filter_walk.py holds 6 tests, 7 cases. No existing test changes.

  • Successive analyses: three projects analysed one after the other in one process, the first again after the second. Each test checks the filters, where they stand, the template applying each, the names reaching their value and argument (an include … with among them), and their order.
  • Overridden blocks: a template reached directly and through a child overriding its block.
  • Cycles: a template reached inside a cycle of includes, then from outside it.
  • Order of the walk: the case of the review, under both namings, so that the child is walked first in one and the parent in the other.
  • Walk path by path: the walk of main is written in the test. On 1,500 generated projects of four templates extending and including one another, inside and outside overridden blocks, with cycles among them, the filters are those of that walk. Each include binds the variable to a name of its own, so that what reaches a filter tells the paths apart.
  • Time: 30 levels of doubled includes, in a subprocess bounded at 60 s.

Before the fix.

  • On main: the test of time does not end within 60 s. The three tests of the first commit pass: they pin the results of main, which the fix keeps.
  • On the first version of this change: the case of the review fails when the child is walked first, and so does the comparison with the walk path by path.

Mutations. Each of these makes the test written for it fail:

  • the cache keyed without the blocks overridden;
  • the results cut short by a cycle kept;
  • one cache for every analysis;
  • a kept walk reused without checking the templates it went through;
  • those templates reduced to the template walked;
  • those templates without the template walked.

Verification

Python 3.12, on main at 0da4cc8:

  • ruff check . and mypy: clean on each commit;
  • unit suite: 2,630, 2,631, 2,631, 2,631, 2,633, 2,634 and 2,634 passed, commit after commit;
  • regression: 19 of 19, snapshots unchanged;
  • Enterprise plugins: 978 passed.

This change and the fix of #278 touch different parts of taint/templates.py. Merged in either order, they give the same tree, on which ruff and mypy are clean, the unit suite passes (2,649) and the regression too (19 of 19).

…ycles and successive analyses

These tests check, in order, which filters a template's render reaches, where they
stand, the template applying each, and the names reaching their value and argument.
They cover projects analysed one after the other, a template reached directly and
through a child overriding its block, and a template reached inside a cycle of includes
and from outside it. The expected results are those of main, which the next commit
must keep.
…idden blocks

project_templates followed every path through includes and extends to collect the
filters a render reaches. Two includes of one template at each of n levels made it
follow 2^n paths: 20 levels took 31 s (#279).

Each template is now walked once for each set of blocks the templates extending it
override. Its first occurrences are kept in order, then mapped through each reference,
which gives the same filters in the same order. The results last for one analysis, and
a walk cut short by a cycle of includes is not kept. 30 levels now take a few
milliseconds.
@SizzleUnrlsd SizzleUnrlsd added the bug Something isn't working label Oct 23, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Oct 23, 2026
…uple

_reaching returned the filters it found and whether a cycle cut its walk short as a bare
pair. It now returns a _Walk naming both, which the cache keeps whole. No behavior
changes.
…er of the walk

A walk of a template is cut where it reaches a template already on the way. The cache
kept a walk that no cycle cut, and returned it to any later walk of that template with
the same blocks overridden, even where a template it had gone through was then on the
way. The filters then depended on the template walked first: a child extending a parent
that includes it back, in the block the child overrides, got its own filter reached
through the parent's include, which a walk path by path cuts.

Each walk now keeps the templates it went through, and is reused only where none of
them is on the way. Elsewhere the walk would be cut, and is walked again.
…h by path

1,500 generated projects of four templates extend and include one another, inside and
outside overridden blocks, with cycles among them; each include binds the variable to a
name of its own. The filters of every template must be those of the walk of main,
written in the test, which follows every path: which filters, where, the template
applying each, the names reaching them, and their order.
…ed again

A walk cut short by a cycle of includes is not kept, so templates in such a cycle are
walked again wherever the cycle cuts their walk: with two includes of one template at
each level, the cost there stays exponential, as it was.
@SizzleUnrlsd
SizzleUnrlsd merged commit f947e68 into main Oct 23, 2026
10 checks passed
@SizzleUnrlsd
SizzleUnrlsd deleted the fix/279-filter-walk branch October 23, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

templates: collecting filter calls takes time exponential in nested includes

1 participant