Skip to content

Implement {% extends %} and {% block %} tags - #299

Draft
LilyFirefly wants to merge 62 commits into
mainfrom
extends-and-block
Draft

Implement {% extends %} and {% block %} tags#299
LilyFirefly wants to merge 62 commits into
mainfrom
extends-and-block

Conversation

@LilyFirefly

Copy link
Copy Markdown
Owner

No description provided.

@LilyFirefly LilyFirefly self-assigned this Jan 8, 2026
@LilyFirefly LilyFirefly added enhancement New feature or request 5.2 Needed for Django 5.2 support labels Jan 8, 2026
@codecov

codecov Bot commented Jan 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.96473% with 37 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/parse.rs 94.60% 14 Missing and 3 partials ⚠️
src/render/tags.rs 95.54% 2 Missing and 7 partials ⚠️
src/render/common.rs 92.42% 3 Missing and 2 partials ⚠️
src/loaders.rs 97.96% 4 Missing ⚠️
src/template.rs 95.83% 0 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jan 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 14 untouched benchmarks


Comparing extends-and-block (4740e9a) with main (32a248f)

Open in CodSpeed

Base automatically changed from tag-element-lexer to main January 8, 2026 08:03
@github-actions

github-actions Bot commented Jan 8, 2026

Copy link
Copy Markdown

Django test suite conformance

ℹ️ Test pass rate changed from 48.48% to 51.71%.
--- main_test_output	2026-05-22 19:27:45.241023495 +0000
+++ branch_test_output	2026-05-22 19:28:08.759065900 +0000
@@ -1,11 +1,11 @@
-Django test suite passing: 48.48%
-735 ERROR / 77 FAIL / 764 OK
+Django test suite passing: 51.71%
+676 ERROR / 85 FAIL / 815 OK
 # 16770 -- The template system doesn't wrap exceptions, but annotates ... ERROR
 # 16770 -- The template system doesn't wrap exceptions, but annotates ... ERROR
 #10043: widthratio should allow max_width to be a variable ... ERROR
 #11911 - Check urlize keeps balanced parentheses ... ok
 #12183 - Check urlize adds nofollow properly - see #12183 ... ok
-#12787 -- The correct template is identified as not existing ... ERROR
+#12787 -- The correct template is identified as not existing ... ok
 #13704 - Check urlize handles IDN correctly ... ok
 #15092 - Also accept simple quotes ... ok
 #15721 -- ``{% include %}`` and ``RequestContext`` should work ... ERROR
@@ -50,9 +50,9 @@
 19819 -- Make sure the correct token is highlighted for ... FAIL
 62.5 should round to 62 ... ERROR
 71.4 should round to 71 ... ERROR
-A block defined only in a child template shouldn't be displayed ... ERROR
-A block within another block (level 2) ... ERROR
-A block within another block ... ERROR
+A block defined only in a child template shouldn't be displayed ... ok
+A block within another block (level 2) ... ok
+A block within another block ... ok
 A named {% cycle %} tag works inside an {% ifchanged %} block and a ... ERROR
 A single loopvar doesn't truncate the list in val. ... ok
 A timeout of None means "cache forever". ... ERROR
@@ -64,9 +64,9 @@
 An empty dirs list in loaders overrides top level dirs. ... ok
 Attribute syntax allows a template to call a dictionary key's ... ok
 Attribute syntax allows a template to call an object's attribute ... ok
-Base template, putting block in a conditional {% if %} tag ... ERROR
-Base template, putting block in a conditional {% if %} tag ... ERROR
-Base template, putting block in a {% for %} tag ... ERROR
+Base template, putting block in a conditional {% if %} tag ... ok
+Base template, putting block in a conditional {% if %} tag ... ok
+Base template, putting block in a {% for %} tag ... ok
 Basic filter usage ... ok
 But list-index lookup wins out when dict's key is an int, which ... ok
 Call methods in the top level of the context. ... ok
@@ -86,8 +86,8 @@
 Error in translation file should not crash template rendering (#16516). ... ERROR
 Error in translation file should not crash template rendering (#18393). ... ERROR
 Error in translation file should not crash template rendering (#18393). ... ERROR
-Error messages should include the unexpected block name and be in all ... ERROR
-Error messages should include the unexpected block name and be in all ... ERROR
+Error messages should include the unexpected block name and be in all ... FAIL
+Error messages should include the unexpected block name and be in all ... FAIL
 Errors in a child of {% extends %} are displayed correctly. ... ERROR
 Errors in a child of {% extends %} are displayed correctly. ... ERROR
 Errors in extended block are displayed correctly. ... ERROR
@@ -123,14 +123,14 @@
 In attribute and dict lookups that raise an unexpected exception ... ERROR
 In methods that raise an exception without a ... ok
 Inclusion tag passes down `use_l10n` of context to the ... ERROR
-Inherit from a template with block wrapped in an {% for %} tag ... ERROR
-Inherit from a template with block wrapped in an {% for %} tag ... ERROR
-Inherit from a template with block wrapped in an {% if %} tag ... ERROR
-Inherit from a template with block wrapped in an {% if %} tag ... ERROR
-Inheritance from a template that doesn't have any blocks ... ERROR
-Inheritance from a template with a space in its name should work. ... ERROR
-Inheritance from local context with variable parent template ... ERROR
-Inheritance from local context without use of template loader ... ERROR
+Inherit from a template with block wrapped in an {% for %} tag ... FAIL
+Inherit from a template with block wrapped in an {% for %} tag ... ok
+Inherit from a template with block wrapped in an {% if %} tag ... ok
+Inherit from a template with block wrapped in an {% if %} tag ... ok
+Inheritance from a template that doesn't have any blocks ... ok
+Inheritance from a template with a space in its name should work. ... ok
+Inheritance from local context with variable parent template ... ok
+Inheritance from local context without use of template loader ... ok
 Iterating over strings outputs safe characters. ... ok
 Let's just make sure setup runs cases in the right order. ... ok
 List-index syntax allows a template to access a certain item of a ... ok
@@ -138,7 +138,7 @@
 Literal string as argument is always "safe" from auto-escaping. ... FAIL
 Literal strings are safe. ... ERROR
 Logically the same as above, just written with explicit ifchanged ... ERROR
-Mixed named and unnamed endblocks ... ERROR
+Mixed named and unnamed endblocks ... ok
 More than one replacement variable is allowed in a template ... ok
 Multiple levels of attribute access are allowed. ... ok
 Nonexistent args ... ok
@@ -155,17 +155,17 @@
 Raise TemplateSyntaxError for invalid block tags ... ERROR
 Raise TemplateSyntaxError when trying to access a filter containing ... FAIL
 Raise TemplateSyntaxError when trying to access a variable ... ok
-Raise exception for block.super used in base template ... ERROR
+Raise exception for block.super used in base template ... ok
 Raise exception for custom tags used in child with {% load %} tag in ... ERROR
-Raise exception for extra {% extends %} tags ... ERROR
-Raise exception for invalid template name ... ERROR
+Raise exception for extra {% extends %} tags ... ok
+Raise exception for invalid template name ... ok
 Raise exception for invalid variable template name ... ERROR
 Regression test for #11270. ... ERROR
 Regression test for #7460. ... ERROR
 Regression tests for #17675 ... ERROR
 Rendering a template response triggers the post-render callbacks ... ok
 Set up a base template to extend ... ok
-Set up a base template with a space in it. ... ERROR
+Set up a base template with a space in it. ... ok
 Simple baseline behavior with one locale for all the supported i18n ... ERROR
 Simple baseline behavior with one locale for all the supported i18n ... ERROR
 Simple baseline behavior with one locale for all the supported i18n ... ERROR
@@ -173,8 +173,8 @@
 Simple baseline behavior with one locale for all the supported i18n ... ERROR
 Simple case ... ok
 Since dictsort uses dict.get()/getattr() under the hood, it can sort ... ok
-Standard template with no inheritance ... ERROR
-Standard two-level inheritance ... ERROR
+Standard template with no inheritance ... ok
+Standard two-level inheritance ... ok
 Strings are converted to bytestrings in the final output. ... ok
 Support any render() supporting object ... ok
 Templates should not crash when rendering methods for built-in types ... ERROR
@@ -195,22 +195,22 @@
 The is_bad() function should not be evaluated. If it is, an ... ok
 The join template filter has needs_autoescape = True ... ERROR
 The order of template loader works. Refs #21460. ... ERROR
-Three-level inheritance with {{ block.super }} from grandparent ... ERROR
-Three-level inheritance with {{ block.super }} from parent ... ERROR
-Three-level inheritance with {{ block.super }} from parent and ... ERROR
-Three-level with both blocks defined on this level, but none on ... ERROR
-Three-level with no redefinitions on third level ... ERROR
-Three-level with one block defined on this level, two blocks ... ERROR
-Three-level with second and third levels blank ... ERROR
-Three-level with space NOT in a block -- should be ignored ... ERROR
-Three-level with this level overriding second level ... ERROR
-Three-level with this level providing one and second level ... ERROR
-Three-level with variable parent-template name ... ERROR
+Three-level inheritance with {{ block.super }} from grandparent ... ok
+Three-level inheritance with {{ block.super }} from parent ... ok
+Three-level inheritance with {{ block.super }} from parent and ... ok
+Three-level with both blocks defined on this level, but none on ... ok
+Three-level with no redefinitions on third level ... ok
+Three-level with one block defined on this level, two blocks ... ok
+Three-level with second and third levels blank ... ok
+Three-level with space NOT in a block -- should be ignored ... ok
+Three-level with this level overriding second level ... ok
+Three-level with this level providing one and second level ... ok
+Three-level with variable parent-template name ... ok
 Treat "moo #} {{ cow" as the variable. Not ideal, but costly to work ... FAIL
-Two-level inheritance with {{ block.super }} ... ERROR
-Two-level with double quotes instead of single quotes ... ERROR
-Two-level with no redefinitions on second level ... ERROR
-Two-level with one block defined, one block not defined ... ERROR
+Two-level inheritance with {{ block.super }} ... ok
+Two-level with double quotes instead of single quotes ... ok
+Two-level with no redefinitions on second level ... ok
+Two-level with one block defined, one block not defined ... ok
 Using filters with the {% translate %} tag (#5972). ... ERROR
 Variable as argument ... ok
 Variable names that aren't resolved as literals. ... ok
@@ -442,7 +442,7 @@
 test_directory_security (template_tests.test_loaders.FileSystemLoaderTests.test_directory_security) ... ERROR
 test_do_not_call (template_tests.test_callables.CallableVariablesTests.test_do_not_call) ... ERROR
 test_do_not_call_and_alters_data (template_tests.test_callables.CallableVariablesTests.test_do_not_call_and_alters_data) ... ERROR
-test_duplicate_block (template_tests.syntax_tests.test_extends.InheritanceTests.test_duplicate_block) ... ERROR
+test_duplicate_block (template_tests.syntax_tests.test_extends.InheritanceTests.test_duplicate_block) ... ok
 test_else_if_tag_error01 (template_tests.syntax_tests.test_if.IfTagTests.test_else_if_tag_error01) ... FAIL
 test_email (template_tests.filter_tests.test_urlize.FunctionTests.test_email) ... ok
 test_empty_dict (template_tests.filter_tests.test_slice.FunctionTests.test_empty_dict) ... ok
@@ -465,14 +465,14 @@
 test_explicit_date (template_tests.filter_tests.test_timesince.FunctionTests.test_explicit_date) ... ok
 test_explicit_date (template_tests.filter_tests.test_timeuntil.FunctionTests.test_explicit_date) ... ok
 test_extend_cached (template_tests.test_extends.ExtendsBehaviorTests.test_extend_cached) ... ERROR
-test_extend_error (template_tests.test_extends_relative.ExtendsRelativeBehaviorTests.test_extend_error) ... ERROR
+test_extend_error (template_tests.test_extends_relative.ExtendsRelativeBehaviorTests.test_extend_error) ... FAIL
 test_extend_missing (template_tests.test_extends.ExtendsBehaviorTests.test_extend_missing) ... ERROR
 test_extend_recursive (template_tests.test_extends.ExtendsBehaviorTests.test_extend_recursive) ... ERROR
-test_extends_duplicate (template_tests.syntax_tests.test_extends.InheritanceTests.test_extends_duplicate) ... ERROR
-test_extends_include_missing_cachedloader (template_tests.syntax_tests.test_include.IncludeTests.test_extends_include_missing_cachedloader) ... ERROR
+test_extends_duplicate (template_tests.syntax_tests.test_extends.InheritanceTests.test_extends_duplicate) ... ok
+test_extends_include_missing_cachedloader (template_tests.syntax_tests.test_include.IncludeTests.test_extends_include_missing_cachedloader) ... ok
 test_extends_node_repr (template_tests.syntax_tests.test_extends.ExtendsNodeTests.test_extends_node_repr) ... ok
-test_extends_not_first_tag_in_extended_template (template_tests.test_extends.ExtendsBehaviorTests.test_extends_not_first_tag_in_extended_template) ... ERROR
-test_extends_not_first_tag_in_extended_template_from_string (template_tests.test_extends.ExtendsBehaviorTests.test_extends_not_first_tag_in_extended_template_from_string) ... ERROR
+test_extends_not_first_tag_in_extended_template (template_tests.test_extends.ExtendsBehaviorTests.test_extends_not_first_tag_in_extended_template) ... ok
+test_extends_not_first_tag_in_extended_template_from_string (template_tests.test_extends.ExtendsBehaviorTests.test_extends_not_first_tag_in_extended_template_from_string) ... ok
 test_fail_silently (template_tests.filter_tests.test_slice.FunctionTests.test_fail_silently) ... ok
 test_fail_silently_incorrect_arg (template_tests.filter_tests.test_truncatechars.TruncatecharsTests.test_fail_silently_incorrect_arg) ... ERROR
 test_false (template_tests.filter_tests.test_divisibleby.FunctionTests.test_false) ... ok
@@ -773,12 +773,12 @@
 test_index (template_tests.filter_tests.test_slice.FunctionTests.test_index) ... ok
 test_index_integer (template_tests.filter_tests.test_slice.FunctionTests.test_index_integer) ... ok
 test_infinity (template_tests.filter_tests.test_floatformat.FunctionTests.test_infinity) ... ok
-test_inheritance31 (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance31) ... ERROR
-test_inheritance32 (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance32) ... ERROR
-test_inheritance39 (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance39) ... ERROR
-test_inheritance40 (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance40) ... ERROR
-test_inheritance41 (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance41) ... ERROR
-test_inheritance_empty (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance_empty) ... ERROR
+test_inheritance31 (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance31) ... ok
+test_inheritance32 (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance32) ... ok
+test_inheritance39 (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance39) ... ok
+test_inheritance40 (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance40) ... ok
+test_inheritance41 (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance41) ... FAIL
+test_inheritance_empty (template_tests.syntax_tests.test_extends.InheritanceTests.test_inheritance_empty) ... FAIL
 test_inputs (template_tests.filter_tests.test_floatformat.FunctionTests.test_inputs) ... ok
 test_inputs (template_tests.filter_tests.test_time.FunctionTests.test_inputs) ... ok
 test_integer (template_tests.filter_tests.test_make_list.FunctionTests.test_integer) ... ok
@@ -895,7 +895,7 @@
 test_middleware_caching (template_tests.test_response.CacheMiddlewareTest.test_middleware_caching) ... ok
 test_mixing1 (template_tests.test_extends_relative.ExtendsMixedBehaviorTests.test_mixing1) ... ERROR
 test_mixing2 (template_tests.test_extends_relative.ExtendsMixedBehaviorTests.test_mixing2) ... ERROR
-test_mixing_loop (template_tests.test_extends_relative.ExtendsMixedBehaviorTests.test_mixing_loop) ... ERROR
+test_mixing_loop (template_tests.test_extends_relative.ExtendsMixedBehaviorTests.test_mixing_loop) ... FAIL
 test_modify_context_and_render (template_tests.test_context.RequestContextTests.test_modify_context_and_render) ... ERROR
 test_modules (template_tests.syntax_tests.test_debug.DebugTests.test_modules) ... ERROR
 test_multiline01 (template_tests.syntax_tests.test_multiline.MultilineTests.test_multiline01) ... ok
@@ -922,12 +922,12 @@
 test_multiple_locale_trans (template_tests.syntax_tests.i18n.test_translate.MultipleLocaleActivationTransTagTests.test_multiple_locale_trans) ... ERROR
 test_multiple_locale_trans (template_tests.syntax_tests.i18n.test_translate.MultipleLocaleActivationTranslateTagTests.test_multiple_locale_trans) ... ERROR
 test_naive_aware_type_error (template_tests.filter_tests.test_timeuntil.TimeuntilTests.test_naive_aware_type_error) ... ERROR
-test_namedendblocks01 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks01) ... ERROR
-test_namedendblocks02 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks02) ... ERROR
-test_namedendblocks03 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks03) ... ERROR
-test_namedendblocks04 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks04) ... ERROR
-test_namedendblocks05 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks05) ... ERROR
-test_namedendblocks07 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks07) ... ERROR
+test_namedendblocks01 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks01) ... ok
+test_namedendblocks02 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks02) ... ok
+test_namedendblocks03 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks03) ... ok
+test_namedendblocks04 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks04) ... ok
+test_namedendblocks05 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks05) ... ok
+test_namedendblocks07 (template_tests.syntax_tests.test_named_endblock.NamedEndblockTests.test_namedendblocks07) ... ok
 test_negative_index (template_tests.filter_tests.test_slice.FunctionTests.test_negative_index) ... ok
 test_negative_numbers (template_tests.filter_tests.test_filesizeformat.FunctionTests.test_negative_numbers) ... ok
 test_negative_zero_values (template_tests.filter_tests.test_floatformat.FunctionTests.test_negative_zero_values) ... ok
@@ -1412,7 +1412,7 @@
 test_typeerror_as_var (template_tests.syntax_tests.test_width_ratio.WidthRatioTagTests.test_typeerror_as_var) ... ERROR
 test_ulitem (template_tests.filter_tests.test_unordered_list.FunctionTests.test_ulitem) ... ok
 test_ulitem_autoescape_off (template_tests.filter_tests.test_unordered_list.FunctionTests.test_ulitem_autoescape_off) ... ok
-test_unclosed_block (template_tests.syntax_tests.test_basic.BasicSyntaxTests.test_unclosed_block) ... ERROR
+test_unclosed_block (template_tests.syntax_tests.test_basic.BasicSyntaxTests.test_unclosed_block) ... FAIL
 test_unclosed_block2 (template_tests.syntax_tests.test_basic.BasicSyntaxTests.test_unclosed_block2) ... FAIL
 test_unicode (template_tests.filter_tests.test_force_escape.FunctionTests.test_unicode) ... ok
 test_unicode (template_tests.filter_tests.test_iriencode.FunctionTests.test_unicode) ... ok

@LilyFirefly
LilyFirefly force-pushed the extends-and-block branch 3 times, most recently from 1dee976 to aaa4814 Compare January 8, 2026 16:46
@LilyFirefly
LilyFirefly force-pushed the extends-and-block branch 7 times, most recently from f4eca86 to 99b8180 Compare February 13, 2026 00:06
@LilyFirefly
LilyFirefly force-pushed the extends-and-block branch 3 times, most recently from eadec1f to 2bbe43b Compare February 21, 2026 20:04
@LilyFirefly
LilyFirefly force-pushed the extends-and-block branch 4 times, most recently from 2d4ee80 to ac4a9e0 Compare March 9, 2026 18:03
@LilyFirefly
LilyFirefly force-pushed the extends-and-block branch 3 times, most recently from 2cd7fdb to da99a5e Compare March 21, 2026 16:31
We deliberately deviate from Django's rendered content here, because
Django stores the `Block Node` in the `context`, which ends up rendering
as the block's `repr` when treated as a variable.

We don't store the `Block` struct in the `context` in the same way, so
we avoid shadowing a `context` variable named `block`, which lets us
render the user's `block` variable.

Matching Django's rendered output here would be prohibitively
complicated.
Move the `Extends::render` implementation closer to the Django
implementation.
When the `FileSystemLoader` doesn't have its own `dirs` in `args` we
need to fallback to the engine's `dirs`.
We had a similar bug to Django #28071, which we caught by running
Django's own tests.

https://code.djangoproject.com/ticket/28071
Remove the explicit `Context` wrapper, since Django Rusty Templates uses
plain dictionaries instead.

Replace `assertRaisesMessage` with `assertMessage` to work around
formatting issues with the message Django Rusty Templates raises.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

5.2 Needed for Django 5.2 support enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant