Repository navigation
fix(templates): put each render together over its extends chain as Django places blocks - #287
Merged
Merged
Conversation
The template read for the filter walk keeps, for each block, the block it stands in, None for one standing in none: what putting a render together over its extends chain needs to place the blocks a definition holds. Nothing reads it yet.
… its order A parent named by an expression or not found, and an extends cycle, keep every block placed, and the text outside the blocks of a template extending another never renders; the templates not found stay listed among the unread ones. A chain lists its filters in the order of a walk through its extends: what it applies, then what it includes from the root down.
…ango places blocks Whether a filter or an include of an extended template rendered depended only on the blocks it stood in and those the templates extending it replaced, not on where the parents place their blocks (#285). A block no template of the chain places counted as rendered, and so did a block whose only placeholder a child replaces; a block a child redefines with {{ block.super }} inside a block it replaces did not render the parent's definition, nor what it includes. Each template's render is now put together over its extends chain: the root's blocks are placed, a placed block renders its most derived definition and, through {{ block.super }}, the next ones up, and the blocks a rendered definition holds are placed in turn. A filter or an include renders when the definition of the innermost block it stands in renders. When the chain reaches no known root, every block of the chain counts as placed, so that no filter is dropped on a placement that cannot be established. The fixpoint of #284 runs on the templates themselves, the pairs with the blocks overridden being no longer needed. The comparison with the walk path by path puts each render together over its chain, reading the blocks' nesting from the text; with this reference, 18 generated projects exceed the unchanged budget, so up to 20 may stay unverified.
Each block placed looked up the blocks it holds among all the blocks of its template, so placing a template's blocks took time quadratic in their number: a 501 KB template of 16,000 empty blocks took more than 8 s to collect, where main takes 0.07 s, and the full analysis 11.6 s instead of 0.5 s. The blocks each block holds, and those standing in no block, are now indexed once per chain, while the blocks are listed. The same 16,000 blocks are placed in 0.08 s, and the full analysis takes 0.3 s. What is placed does not change.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #285.
Whether a filter or an include of an extended template rendered depended only on the blocks it stood in, and on those the templates extending it replaced. It did not depend on where the parents place their blocks, so the walk was wrong both ways. Django 5.1.15 confirms each case:
create.htmlandgeneric/form.html){{ block.super }}inside a block it replaces (wagtail'sgeneric/listing.html)Each template's render is now put together over its extends chain, as Django puts it:
{{ block.super }}, the next ones up. The blocks a rendered definition holds are placed in turn.Coverage.unread_templates.What changes in the code.
_Templaterecords the block each block stands in._Template.rendered,replacedand_Pairare gone.The design and its measurements are in the issue.
refactor(templates): record the block each block of a template stands in_Template.enclosing, read with the blocks; nothing reads it yettest(templates): pin what a chain keeps when its root is unknown, and its orderfix(templates): put each render together over its extends chain as Django places blocksdocs: state where a template's blocks render along its extends chaindocs/usage.mddocs(changelog): announce the blocks placed along the extends chainCHANGELOG.mdfix(templates): place the blocks of a chain in time linear in its blocksResults
Generated projects. 1,500 projects of four templates extending and including one another, inside and outside nested blocks, with cycles. Django 5.1.15 renders each template.
Without conditions, the result is exactly what Django applies on every template it renders. With conditions, the walk follows every branch, so it applies more than one render does, but it misses nothing.
Unknown roots.
Fixpoint. On all 1,500 projects, the fixpoint closes, and the lists are the same whatever order the templates are inserted in: 4,500 runs out of 4,500.
Local corpus. On 53 projects, 52 are identical, order included. wagtail changes:
wagtailadmin/shared/header.htmlthroughgeneric/listing.html'sheader, which renders{{ block.super }}.explorable_index.htmlreplacesheader, which heldmain_header. Six forms replacecontentwithout definingmain_content, directly or throughedit.html.Each change was checked in the templates.
End to end. The reproduction of #285 reports nothing. Its control, where the parent places the block, still reports
exploitable-vulnerability.Time.
{{ block.super }}: 0.12 s instead of 0.07 s.main(0.07 s); the full analysis 0.32 s. Before the index of the blocks each block holds, more than 8 s, and 11.6 s.Tests
New file.
tests/test_template_block_placement.pyholds 8 tests, 12 cases. Each expectation is what Django 5.1.15 applies:block.super: a block redefined with{{ block.super }}inside a replaced block, and its control without it.Existing test changed. One test changed,
tests/test_template_filter_walk.py::test_the_filters_are_those_of_every_path_with_each_cycle_turned_until_it_settles. The changes were agreed in the review of the design.extendswith the former block rule. It now puts each render together over its chain, reading the blocks' nesting from the templates' text, apart from the engine, then follows the includes path by path as before.Before the fix. On the parent commit, 5 tests fail for the expected reason:
The controls, the unknown roots and the order pass, as they do on
main.Mutations. Each of these makes tests fail:
{{ block.super }}never reaching the next definition;Verification
Python 3.12, on
mainat8846ebb:ruff check .andmypy: clean on each commit;