1
0
mirror of https://github.com/django/django.git synced 2025-08-21 01:09:13 +00:00

Fixed #36410 -- Added support for Template Partials to the Django Template Language.

Introduced `{% partialdef %}` and `{% partial %}` template tags to
define and render reusable named fragments within a template file.
Partials can also be accessed using the `template_name#partial_name`
syntax via `get_template()`, `render()`, `{% include %}`, and other
template-loading tools.

Adjusted `get_template()` behavior to support partial resolution, with
appropriate error handling for invalid names and edge cases. Introduced
`PartialTemplate` to encapsulate partial rendering behavior.

Includes tests and internal refactors to support partial context
binding, exception reporting, and tag validation.

Co-authored-by: Carlton Gibson <carlton@noumenal.es>
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
Co-authored-by: Nick Pope <nick@nickpope.me.uk>
This commit is contained in:
farhan 2025-06-03 10:32:34 +05:00 committed by nessita
parent fda3c1712a
commit 5e06b97095
16 changed files with 1587 additions and 2 deletions

View File

@ -61,6 +61,7 @@ from .base import ( # NOQA isort:skip
Node, Node,
NodeList, NodeList,
Origin, Origin,
PartialTemplate,
Template, Template,
Variable, Variable,
) )

View File

@ -88,6 +88,11 @@ UNKNOWN_SOURCE = "<unknown source>"
# than instantiating SimpleLazyObject with _lazy_re_compile(). # than instantiating SimpleLazyObject with _lazy_re_compile().
tag_re = re.compile(r"({%.*?%}|{{.*?}}|{#.*?#})") tag_re = re.compile(r"({%.*?%}|{{.*?}}|{#.*?#})")
combined_partial_re = re.compile(
r"{%\s*partialdef\s+(?P<name>[\w-]+)(?:\s+inline)?\s*%}"
r"|{%\s*endpartialdef(?:\s+[\w-]+)?\s*%}"
)
logger = logging.getLogger("django.template") logger = logging.getLogger("django.template")
@ -288,6 +293,57 @@ class Template:
} }
class PartialTemplate:
"""
A lightweight Template lookalike used for template partials.
Wraps nodelist as a partial, in order to be able to bind context.
"""
def __init__(self, nodelist, origin, name):
self.nodelist = nodelist
self.origin = origin
self.name = name
def get_exception_info(self, exception, token):
template = self.origin.loader.get_template(self.origin.template_name)
return template.get_exception_info(exception, token)
def find_partial_source(self, full_source, partial_name):
start_match = None
nesting = 0
for match in combined_partial_re.finditer(full_source):
if name := match["name"]: # Opening tag.
if start_match is None and name == partial_name:
start_match = match
if start_match is not None:
nesting += 1
elif start_match is not None:
nesting -= 1
if nesting == 0:
return full_source[start_match.start() : match.end()]
return ""
@property
def source(self):
template = self.origin.loader.get_template(self.origin.template_name)
return self.find_partial_source(template.source, self.name)
def _render(self, context):
return self.nodelist.render(context)
def render(self, context):
with context.render_context.push_state(self):
if context.template is None:
with context.bind_template(self):
context.template_name = self.name
return self._render(context)
else:
return self._render(context)
def linebreak_iter(template_source): def linebreak_iter(template_source):
yield 0 yield 0
p = template_source.find("\n") p = template_source.find("\n")

View File

@ -12,6 +12,7 @@ from itertools import groupby
from django.conf import settings from django.conf import settings
from django.http import QueryDict from django.http import QueryDict
from django.utils import timezone from django.utils import timezone
from django.utils.datastructures import DeferredSubDict
from django.utils.html import conditional_escape, escape, format_html from django.utils.html import conditional_escape, escape, format_html
from django.utils.lorem_ipsum import paragraphs, words from django.utils.lorem_ipsum import paragraphs, words
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -29,6 +30,7 @@ from .base import (
VARIABLE_TAG_START, VARIABLE_TAG_START,
Node, Node,
NodeList, NodeList,
PartialTemplate,
TemplateSyntaxError, TemplateSyntaxError,
VariableDoesNotExist, VariableDoesNotExist,
kwarg_re, kwarg_re,
@ -408,6 +410,31 @@ class NowNode(Node):
return formatted return formatted
class PartialDefNode(Node):
def __init__(self, partial_name, inline, nodelist):
self.partial_name = partial_name
self.inline = inline
self.nodelist = nodelist
def render(self, context):
return self.nodelist.render(context) if self.inline else ""
class PartialNode(Node):
def __init__(self, partial_name, partial_mapping):
# Defer lookup in `partial_mapping` and nodelist to runtime.
self.partial_name = partial_name
self.partial_mapping = partial_mapping
def render(self, context):
try:
return self.partial_mapping[self.partial_name].render(context)
except KeyError:
raise TemplateSyntaxError(
f"Partial '{self.partial_name}' is not defined in the current template."
)
class ResetCycleNode(Node): class ResetCycleNode(Node):
def __init__(self, node): def __init__(self, node):
self.node = node self.node = node
@ -1174,6 +1201,75 @@ def now(parser, token):
return NowNode(format_string, asvar) return NowNode(format_string, asvar)
@register.tag(name="partialdef")
def partialdef_func(parser, token):
"""
Declare a partial that can be used in the template.
Usage::
{% partialdef partial_name %}
Content goes here.
{% endpartialdef %}
Store the nodelist in the context under the key "partials". It can be
retrieved using the ``{% partial %}`` tag.
The optional ``inline`` argument renders the partial's contents
immediately, at the point where it is defined.
"""
match token.split_contents():
case "partialdef", partial_name, "inline":
inline = True
case "partialdef", partial_name, _:
raise TemplateSyntaxError(
"The 'inline' argument does not have any parameters; either use "
"'inline' or remove it completely."
)
case "partialdef", partial_name:
inline = False
case ["partialdef"]:
raise TemplateSyntaxError("'partialdef' tag requires a name")
case _:
raise TemplateSyntaxError("'partialdef' tag takes at most 2 arguments")
# Parse the content until the end tag.
valid_endpartials = ("endpartialdef", f"endpartialdef {partial_name}")
nodelist = parser.parse(valid_endpartials)
endpartial = parser.next_token()
if endpartial.contents not in valid_endpartials:
parser.invalid_block_tag(endpartial, "endpartialdef", valid_endpartials)
# Store the partial nodelist in the parser.extra_data attribute.
partials = parser.extra_data.setdefault("partials", {})
if partial_name in partials:
raise TemplateSyntaxError(
f"Partial '{partial_name}' is already defined in the "
f"'{parser.origin.name}' template."
)
partials[partial_name] = PartialTemplate(nodelist, parser.origin, partial_name)
return PartialDefNode(partial_name, inline, nodelist)
@register.tag(name="partial")
def partial_func(parser, token):
"""
Render a partial previously declared with the ``{% partialdef %}`` tag.
Usage::
{% partial partial_name %}
"""
match token.split_contents():
case "partial", partial_name:
extra_data = parser.extra_data
partial_mapping = DeferredSubDict(extra_data, "partials")
return PartialNode(partial_name, partial_mapping=partial_mapping)
case _:
raise TemplateSyntaxError("'partial' tag requires a single argument")
@register.simple_tag(name="querystring", takes_context=True) @register.simple_tag(name="querystring", takes_context=True)
def querystring(context, *args, **kwargs): def querystring(context, *args, **kwargs):
""" """

View File

@ -174,11 +174,31 @@ class Engine:
Return a compiled Template object for the given template name, Return a compiled Template object for the given template name,
handling template inheritance recursively. handling template inheritance recursively.
""" """
original_name = template_name
try:
template_name, _, partial_name = template_name.partition("#")
except AttributeError:
raise TemplateDoesNotExist(original_name)
if not template_name:
raise TemplateDoesNotExist(original_name)
template, origin = self.find_template(template_name) template, origin = self.find_template(template_name)
if not hasattr(template, "render"): if not hasattr(template, "render"):
# template needs to be compiled # template needs to be compiled
template = Template(template, origin, template_name, engine=self) template = Template(template, origin, template_name, engine=self)
return template
if not partial_name:
return template
extra_data = getattr(template, "extra_data", {})
try:
partial = extra_data["partials"][partial_name]
except (KeyError, TypeError):
raise TemplateDoesNotExist(partial_name, tried=[template_name])
partial.engine = self
return partial
def render_to_string(self, template_name, context=None): def render_to_string(self, template_name, context=None):
""" """

View File

@ -24,7 +24,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.core.signals import request_started, setting_changed from django.core.signals import request_started, setting_changed
from django.db import DEFAULT_DB_ALIAS, connections, reset_queries from django.db import DEFAULT_DB_ALIAS, connections, reset_queries
from django.db.models.options import Options from django.db.models.options import Options
from django.template import Template from django.template import PartialTemplate, Template
from django.test.signals import template_rendered from django.test.signals import template_rendered
from django.urls import get_script_prefix, set_script_prefix from django.urls import get_script_prefix, set_script_prefix
from django.utils.translation import deactivate from django.utils.translation import deactivate
@ -147,7 +147,9 @@ def setup_test_environment(debug=None):
settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
saved_data.template_render = Template._render saved_data.template_render = Template._render
saved_data.partial_template_render = PartialTemplate._render
Template._render = instrumented_test_render Template._render = instrumented_test_render
PartialTemplate._render = instrumented_test_render
mail.outbox = [] mail.outbox = []
@ -165,6 +167,7 @@ def teardown_test_environment():
settings.DEBUG = saved_data.debug settings.DEBUG = saved_data.debug
settings.EMAIL_BACKEND = saved_data.email_backend settings.EMAIL_BACKEND = saved_data.email_backend
Template._render = saved_data.template_render Template._render = saved_data.template_render
PartialTemplate._render = saved_data.partial_template_render
del _TestState.saved_data del _TestState.saved_data
del mail.outbox del mail.outbox

View File

@ -345,3 +345,21 @@ class CaseInsensitiveMapping(Mapping):
"Element key %r invalid, only strings are allowed" % elem[0] "Element key %r invalid, only strings are allowed" % elem[0]
) )
yield elem yield elem
class DeferredSubDict:
"""
Wrap a dict, allowing deferred access to a sub-dict under a given key.
The value at ``deferred_key`` must itself be a dict. Accessing
``DeferredSubDict(parent_dict, deferred_key)[key]`` retrieves
``parent_dict[deferred_key][key]`` at access time, so updates to
the parent dict are reflected.
"""
def __init__(self, parent_dict, deferred_key):
self.parent_dict = parent_dict
self.deferred_key = deferred_key
def __getitem__(self, key):
return self.parent_dict[self.deferred_key][key]

View File

@ -957,6 +957,80 @@ output (as a string) inside a variable. This is useful if you want to use
{% now "Y" as current_year %} {% now "Y" as current_year %}
{% blocktranslate %}Copyright {{ current_year }}{% endblocktranslate %} {% blocktranslate %}Copyright {{ current_year }}{% endblocktranslate %}
.. templatetag:: partial
``partial``
-----------
.. versionadded:: 6.0
Renders a template fragment that was defined with :ttag:`partialdef`, inserting
the matching partial at this location.
Usage:
.. code-block:: html+django
{% partial partial_name %}
The ``partial_name`` argument is the name of the template fragment to render.
In the following example, a partial named ``button`` is defined and then
rendered three times:
.. code-block:: html+django
{% partialdef button %}
<button>Submit</button>
{% endpartialdef %}
{% partial button %}
{% partial button %}
{% partial button %}
.. templatetag:: partialdef
``partialdef``
--------------
.. versionadded:: 6.0
Defines a reusable template fragment that can be rendered multiple times within
the same template or accessed directly via :ref:`template loading or inclusion
<template-partials-direct-access>`.
Usage:
.. code-block:: html+django
{% partialdef partial_name %}
{# Reusable content. #}
{% endpartialdef %}
The ``partial_name`` argument is required and must be a valid template
identifier.
In the following example, a new fragment named ``card`` is defined:
.. code-block:: html+django
{% partialdef card %}
<div class="card">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
{% endpartialdef %}
This partial can then be rendered using the :ttag:`partial` tag:
.. code-block:: html+django
{% partial card %}
{% partial card %}
To :ref:`render a fragment immediately in place <template-partials-inline>`,
use the ``inline`` option. The partial is still stored and can be reused later.
.. templatetag:: querystring .. templatetag:: querystring
``querystring`` ``querystring``

View File

@ -475,6 +475,150 @@ it also defines the content that fills the hole in the *parent*. If there were
two similarly-named :ttag:`block` tags in a template, that template's parent two similarly-named :ttag:`block` tags in a template, that template's parent
wouldn't know which one of the blocks' content to use. wouldn't know which one of the blocks' content to use.
.. _template-partials:
Template partials
=================
.. versionadded:: 6.0
Template partials define reusable template fragments within a template file.
They are self-contained components that can be rendered multiple times within
the same template, helping to avoid repetition and maintain consistent output.
Basic syntax
------------
A partial can be defined using the :ttag:`partialdef` tag:
.. code-block:: html+django
:caption: ``authors.html``
{% partialdef user-info %}
<div id="user-info-{{ user.username }}">
<h3>{{ user.name }}</h3>
<p>{{ user.bio }}</p>
</div>
{% endpartialdef %}
For extra readability, the name can be included in the ``{% endpartialdef %}``
closing tag:
.. code-block:: html+django
{% partialdef user-info %}
{# ... #}
{% endpartialdef user-info %}
The template fragment can be rendered using the :ttag:`partial` tag:
.. code-block:: html+django
{% partial user-info %}
Fragment reuse
--------------
A template fragment can be reused multiple times:
.. code-block:: html+django
:caption: ``authors.html``
{% block content %}
<h2>Authors</h2>
{% for user in authors %}
{% partial user-info %}
{% endfor %}
<h2>Editors</h2>
{% for user in editors %}
{% partial user-info %}
{% endfor %}
{% endblock %}
The partial content is rendered each time the named partial is used, with the
current template context.
.. _template-partials-inline:
Inline partials
---------------
A template fragment can be defined and rendered in-place using the ``inline``
argument. This defines the partial for later reuse while also rendering it
immediately at its definition:
.. code-block:: html+django
{# Define and render immediately. #}
{% partialdef user-info inline %}
<div id="user-info-{{ user.username }}">
<h3>{{ user.name }}</h3>
<p>{{ user.bio }}</p>
</div>
{% endpartialdef %}
{# Other page content here. #}
{# Reuse later elsewhere in the template. #}
<section class="featured-authors">
<h2>Featured Authors</h2>
{% for user in featured %}
{% partial user-info %}
{% endfor %}
</section>
.. _template-partials-direct-access:
Accessing partials directly
---------------------------
Template fragments defined with :ttag:`partialdef` can be accessed directly via
template loading or inclusion using the ``template.html#partial_name`` syntax.
For example, using the :func:`~django.shortcuts.render` shortcut, the following
code renders only the partial named ``user-info`` defined in the
``authors.html`` template::
from django.contrib.auth.models import User
from django.shortcuts import get_object_or_404, render
def user_info_partial(request, user_id):
user = get_object_or_404(User, id=user_id)
return render(request, "authors.html#user-info", {"user": user})
This approach is particularly useful for AJAX-style requests that update only
specific portions of a page with the rendered template fragment.
Template partials can also be included using the :ttag:`include` template tag
with the same ``#`` directive:
.. code-block:: html+django
{% include "authors.html#user-info" %}
Context handling
----------------
Template partials are rendered with the current template context. They work as
expected in loops and with context variables:
.. code-block:: html+django
{% for user in users %}
{% partial user-info %}
{% endfor %}
Context variables can be adjusted using the :ttag:`with` tag:
.. code-block:: html+django
{% with user=featured_author %}
<h2>Featured Author of the Month</h2>
{% partial user-info %}
{% endwith %}
.. _next section: #automatic-html-escaping .. _next section: #automatic-html-escaping
.. _automatic-html-escaping: .. _automatic-html-escaping:

View File

@ -91,6 +91,21 @@ Notably, the return type of the :class:`EmailMessage.message()
previous ``SafeMIMEText`` and ``SafeMIMEMultipart`` return types, but is not an previous ``SafeMIMEText`` and ``SafeMIMEMultipart`` return types, but is not an
instance of those now-deprecated classes. instance of those now-deprecated classes.
Template Partials
-----------------
The :ref:`Django Template Language <template-language-intro>` now supports
:ref:`template partials <template-partials>` , making it easier to encapsulate
and reuse small named fragments within a template file. The new tags
:ttag:`{% partialdef %} <partialdef>` and :ttag:`{% partial %} <partial>`
define a partial and render it, respectively.
Partials can also be referenced using the ``template_name#partial_name`` syntax
with :func:`~django.template.Engine.get_template`,
:func:`~django.shortcuts.render`, :ttag:`{% include %}<include>`, and other
template-loading tools, enabling more modular and maintainable templates
without needing to split components into separate files.
Minor features Minor features
-------------- --------------

View File

@ -306,6 +306,33 @@ The ``django.template.loader`` module defines two functions to load templates.
If you want to restrict the search to a particular template engine, pass If you want to restrict the search to a particular template engine, pass
the engine's :setting:`NAME <TEMPLATES-NAME>` in the ``using`` argument. the engine's :setting:`NAME <TEMPLATES-NAME>` in the ``using`` argument.
**Partial loading:**
When using the :class:`~django.template.backends.django.DjangoTemplates`
backend, a specific fragment from a template can be loaded by name. This
fragment should have been previously defined using the :ttag:`partialdef`
template tag::
from django.template.loader import get_template
# Load an entire template.
template = get_template("template.html")
# Load a specific fragment from a template.
partial = get_template("template.html#partial_name")
When loading a partial, the returned object behaves like a regular
``Template`` but contains only the partial's content.
See :ref:`template-partials` for more information about defining and using
template fragments.
.. versionchanged:: 6.0
Support for loading template partials when using the
:class:`~django.template.backends.django.DjangoTemplates` backend was
added.
.. function:: select_template(template_name_list, using=None) .. function:: select_template(template_name_list, using=None)
``select_template()`` is just like ``get_template()``, except it takes a ``select_template()`` is just like ``get_template()``, except it takes a

View File

@ -0,0 +1,652 @@
from django.template import (
Context,
TemplateDoesNotExist,
TemplateSyntaxError,
VariableDoesNotExist,
)
from django.template.base import Token, TokenType
from django.test import SimpleTestCase
from django.views.debug import ExceptionReporter
from ..utils import setup
partial_templates = {
"partial_base.html": (
"<main>{% block main %}Default main content.{% endblock main %}</main>"
),
"partial_included.html": (
"INCLUDED TEMPLATE START\n"
"{% partialdef included-partial %}\n"
"THIS IS CONTENT FROM THE INCLUDED PARTIAL\n"
"{% endpartialdef %}\n\n"
"Now using the partial: {% partial included-partial %}\n"
"INCLUDED TEMPLATE END\n"
),
}
valid_partialdef_names = (
"dot.in.name",
"'space in name'",
"exclamation!",
"@at",
"slash/something",
"inline",
"inline-inline",
"INLINE" "with+plus",
"with&amp",
"with%percent",
"with,comma",
"with:colon",
"with;semicolon",
"[brackets]",
"(parens)",
"{curly}",
)
def gen_partial_template(name, *args, **kwargs):
if args or kwargs:
extra = " ".join((args, *("{k}={v}" for k, v in kwargs.items()))) + " "
else:
extra = ""
return (
f"{{% partialdef {name} {extra}%}}TEST with {name}!{{% endpartialdef %}}"
f"{{% partial {name} %}}"
)
class PartialTagTests(SimpleTestCase):
libraries = {"bad_tag": "template_tests.templatetags.bad_tag"}
@setup({name: gen_partial_template(name) for name in valid_partialdef_names})
def test_valid_partialdef_names(self):
for template_name in valid_partialdef_names:
with self.subTest(template_name=template_name):
output = self.engine.render_to_string(template_name)
self.assertEqual(output, f"TEST with {template_name}!")
@setup(
{
"basic": (
"{% partialdef testing-name %}"
"HERE IS THE TEST CONTENT"
"{% endpartialdef %}"
"{% partial testing-name %}"
),
"basic_inline": (
"{% partialdef testing-name inline %}"
"HERE IS THE TEST CONTENT"
"{% endpartialdef %}"
),
"inline_inline": (
"{% partialdef inline inline %}"
"HERE IS THE TEST CONTENT"
"{% endpartialdef %}"
),
"with_newlines": (
"{% partialdef testing-name %}\n"
"HERE IS THE TEST CONTENT\n"
"{% endpartialdef testing-name %}\n"
"{% partial testing-name %}"
),
}
)
def test_basic_usage(self):
for template_name in (
"basic",
"basic_inline",
"inline_inline",
"with_newlines",
):
with self.subTest(template_name=template_name):
output = self.engine.render_to_string(template_name)
self.assertEqual(output.strip(), "HERE IS THE TEST CONTENT")
@setup(
{
"inline_partial_with_context": (
"BEFORE\n"
"{% partialdef testing-name inline %}"
"HERE IS THE TEST CONTENT"
"{% endpartialdef %}\n"
"AFTER"
)
}
)
def test_partial_inline_only_with_before_and_after_content(self):
output = self.engine.render_to_string("inline_partial_with_context")
self.assertEqual(output.strip(), "BEFORE\nHERE IS THE TEST CONTENT\nAFTER")
@setup(
{
"inline_partial_explicit_end": (
"{% partialdef testing-name inline %}"
"HERE IS THE TEST CONTENT"
"{% endpartialdef testing-name %}\n"
"{% partial testing-name %}"
)
}
)
def test_partial_inline_and_used_once(self):
output = self.engine.render_to_string("inline_partial_explicit_end")
self.assertEqual(output, "HERE IS THE TEST CONTENT\nHERE IS THE TEST CONTENT")
@setup(
{
"inline_partial_with_usage": (
"BEFORE\n"
"{% partialdef content_snippet inline %}"
"HERE IS THE TEST CONTENT"
"{% endpartialdef %}\n"
"AFTER\n"
"{% partial content_snippet %}"
)
}
)
def test_partial_inline_and_used_once_with_before_and_after_content(self):
output = self.engine.render_to_string("inline_partial_with_usage")
self.assertEqual(
output.strip(),
"BEFORE\nHERE IS THE TEST CONTENT\nAFTER\nHERE IS THE TEST CONTENT",
)
@setup(
{
"partial_used_before_definition": (
"TEMPLATE START\n"
"{% partial testing-name %}\n"
"MIDDLE CONTENT\n"
"{% partialdef testing-name %}\n"
"THIS IS THE PARTIAL CONTENT\n"
"{% endpartialdef %}\n"
"TEMPLATE END"
),
}
)
def test_partial_used_before_definition(self):
output = self.engine.render_to_string("partial_used_before_definition")
expected = (
"TEMPLATE START\n\nTHIS IS THE PARTIAL CONTENT\n\n"
"MIDDLE CONTENT\n\nTEMPLATE END"
)
self.assertEqual(output, expected)
@setup(
{
"partial_with_extends": (
"{% extends 'partial_base.html' %}"
"{% partialdef testing-name %}Inside Content{% endpartialdef %}"
"{% block main %}"
"Main content with {% partial testing-name %}"
"{% endblock %}"
),
},
partial_templates,
)
def test_partial_defined_outside_main_block(self):
output = self.engine.render_to_string("partial_with_extends")
self.assertIn("<main>Main content with Inside Content</main>", output)
@setup(
{
"partial_with_extends_and_block_super": (
"{% extends 'partial_base.html' %}"
"{% partialdef testing-name %}Inside Content{% endpartialdef %}"
"{% block main %}{{ block.super }} "
"Main content with {% partial testing-name %}"
"{% endblock %}"
),
},
partial_templates,
)
def test_partial_used_with_block_super(self):
output = self.engine.render_to_string("partial_with_extends_and_block_super")
self.assertIn(
"<main>Default main content. Main content with Inside Content</main>",
output,
)
@setup(
{
"partial_with_include": (
"MAIN TEMPLATE START\n"
"{% include 'partial_included.html' %}\n"
"MAIN TEMPLATE END"
)
},
partial_templates,
)
def test_partial_in_included_template(self):
output = self.engine.render_to_string("partial_with_include")
expected = (
"MAIN TEMPLATE START\nINCLUDED TEMPLATE START\n\n\n"
"Now using the partial: \n"
"THIS IS CONTENT FROM THE INCLUDED PARTIAL\n\n"
"INCLUDED TEMPLATE END\n\nMAIN TEMPLATE END"
)
self.assertEqual(output, expected)
@setup(
{
"partial_as_include_in_other_template": (
"MAIN TEMPLATE START\n"
"{% include 'partial_included.html#included-partial' %}\n"
"MAIN TEMPLATE END"
)
},
partial_templates,
)
def test_partial_as_include_in_template(self):
output = self.engine.render_to_string("partial_as_include_in_other_template")
expected = (
"MAIN TEMPLATE START\n\n"
"THIS IS CONTENT FROM THE INCLUDED PARTIAL\n\n"
"MAIN TEMPLATE END"
)
self.assertEqual(output, expected)
@setup(
{
"nested_simple": (
"{% extends 'base.html' %}"
"{% block content %}"
"This is my main page."
"{% partialdef outer inline %}"
" It hosts a couple of partials.\n"
" {% partialdef inner inline %}"
" And an inner one."
" {% endpartialdef inner %}"
"{% endpartialdef outer %}"
"{% endblock content %}"
),
"use_outer": "{% include 'nested_simple#outer' %}",
"use_inner": "{% include 'nested_simple#inner' %}",
}
)
def test_nested_partials(self):
with self.subTest(template_name="use_outer"):
output = self.engine.render_to_string("use_outer")
self.assertEqual(
[line.strip() for line in output.split("\n")],
["It hosts a couple of partials.", "And an inner one."],
)
with self.subTest(template_name="use_inner"):
output = self.engine.render_to_string("use_inner")
self.assertEqual(output.strip(), "And an inner one.")
@setup(
{
"partial_undefined_name": "{% partial undefined %}",
"partial_missing_name": "{% partial %}",
"partial_closing_tag": (
"{% partialdef testing-name %}TEST{% endpartialdef %}"
"{% partial testing-name %}{% endpartial %}"
),
"partialdef_missing_name": "{% partialdef %}{% endpartialdef %}",
"partialdef_missing_close_tag": "{% partialdef name %}TEST",
"partialdef_opening_closing_name_mismatch": (
"{% partialdef testing-name %}TEST{% endpartialdef invalid %}"
),
"partialdef_invalid_name": gen_partial_template("with\nnewline"),
"partialdef_extra_params": (
"{% partialdef testing-name inline extra %}TEST{% endpartialdef %}"
),
"partialdef_duplicated_names": (
"{% partialdef testing-name %}TEST{% endpartialdef %}"
"{% partialdef testing-name %}TEST{% endpartialdef %}"
"{% partial testing-name %}"
),
"partialdef_duplicated_nested_names": (
"{% partialdef testing-name %}"
"TEST"
"{% partialdef testing-name %}TEST{% endpartialdef %}"
"{% endpartialdef %}"
"{% partial testing-name %}"
),
},
)
def test_basic_parse_errors(self):
for template_name, error_msg in (
(
"partial_undefined_name",
"Partial 'undefined' is not defined in the current template.",
),
("partial_missing_name", "'partial' tag requires a single argument"),
("partial_closing_tag", "Invalid block tag on line 1: 'endpartial'"),
("partialdef_missing_name", "'partialdef' tag requires a name"),
("partialdef_missing_close_tag", "Unclosed tag on line 1: 'partialdef'"),
(
"partialdef_opening_closing_name_mismatch",
"expected 'endpartialdef' or 'endpartialdef testing-name'.",
),
("partialdef_invalid_name", "Invalid block tag on line 3: 'endpartialdef'"),
("partialdef_extra_params", "'partialdef' tag takes at most 2 arguments"),
(
"partialdef_duplicated_names",
"Partial 'testing-name' is already defined in the "
"'partialdef_duplicated_names' template.",
),
(
"partialdef_duplicated_nested_names",
"Partial 'testing-name' is already defined in the "
"'partialdef_duplicated_nested_names' template.",
),
):
with (
self.subTest(template_name=template_name),
self.assertRaisesMessage(TemplateSyntaxError, error_msg),
):
self.engine.render_to_string(template_name)
@setup(
{
"with_params": (
"{% partialdef testing-name inline=true %}TEST{% endpartialdef %}"
),
"uppercase": "{% partialdef testing-name INLINE %}TEST{% endpartialdef %}",
}
)
def test_partialdef_invalid_inline(self):
error_msg = "The 'inline' argument does not have any parameters"
for template_name in ("with_params", "uppercase"):
with (
self.subTest(template_name=template_name),
self.assertRaisesMessage(TemplateSyntaxError, error_msg),
):
self.engine.render_to_string(template_name)
@setup(
{
"partial_broken_unclosed": (
"<div>Before partial</div>"
"{% partialdef unclosed_partial %}"
"<p>This partial has no closing tag</p>"
"<div>After partial content</div>"
)
}
)
def test_broken_partial_unclosed_exception_info(self):
with self.assertRaises(TemplateSyntaxError) as cm:
self.engine.get_template("partial_broken_unclosed")
self.assertIn("endpartialdef", str(cm.exception))
self.assertIn("Unclosed tag", str(cm.exception))
reporter = ExceptionReporter(None, cm.exception.__class__, cm.exception, None)
traceback_data = reporter.get_traceback_data()
exception_value = str(traceback_data.get("exception_value", ""))
self.assertIn("Unclosed tag", exception_value)
@setup(
{
"partial_with_variable_error": (
"<h1>Title</h1>\n"
"{% partialdef testing-name %}\n"
"<p>{{ nonexistent|default:alsonotthere }}</p>\n"
"{% endpartialdef %}\n"
"<h2>Sub Title</h2>\n"
"{% partial testing-name %}\n"
),
}
)
def test_partial_runtime_exception_has_debug_info(self):
template = self.engine.get_template("partial_with_variable_error")
context = Context({})
if hasattr(self.engine, "string_if_invalid") and self.engine.string_if_invalid:
output = template.render(context)
# The variable should be replaced with INVALID
self.assertIn("INVALID", output)
else:
with self.assertRaises(VariableDoesNotExist) as cm:
template.render(context)
if self.engine.debug:
exc_info = cm.exception.template_debug
self.assertEqual(
exc_info["during"], "{{ nonexistent|default:alsonotthere }}"
)
self.assertEqual(exc_info["line"], 3)
self.assertEqual(exc_info["name"], "partial_with_variable_error")
self.assertIn("Failed lookup", exc_info["message"])
@setup(
{
"partial_exception_info_test": (
"<h1>Title</h1>\n"
"{% partialdef testing-name %}\n"
"<p>Content</p>\n"
"{% endpartialdef %}\n"
),
}
)
def test_partial_template_get_exception_info_delegation(self):
if self.engine.debug:
template = self.engine.get_template("partial_exception_info_test")
partial_template = template.extra_data["partials"]["testing-name"]
test_exc = Exception("Test exception")
token = Token(
token_type=TokenType.VAR,
contents="test",
position=(0, 4),
)
exc_info = partial_template.get_exception_info(test_exc, token)
self.assertIn("message", exc_info)
self.assertIn("line", exc_info)
self.assertIn("name", exc_info)
self.assertEqual(exc_info["name"], "partial_exception_info_test")
self.assertEqual(exc_info["message"], "Test exception")
@setup(
{
"partial_with_undefined_reference": (
"<h1>Header</h1>\n"
"{% partial undefined %}\n"
"<p>After undefined partial</p>\n"
),
}
)
def test_undefined_partial_exception_info(self):
template = self.engine.get_template("partial_with_undefined_reference")
with self.assertRaises(TemplateSyntaxError) as cm:
template.render(Context())
self.assertIn("undefined", str(cm.exception))
self.assertIn("is not defined", str(cm.exception))
if self.engine.debug:
exc_debug = cm.exception.template_debug
self.assertEqual(exc_debug["during"], "{% partial undefined %}")
self.assertEqual(exc_debug["line"], 2)
self.assertEqual(exc_debug["name"], "partial_with_undefined_reference")
self.assertIn("undefined", exc_debug["message"])
@setup(
{
"existing_template": (
"<h1>Header</h1><p>This template has no partials defined</p>"
),
}
)
def test_undefined_partial_exception_info_template_does_not_exist(self):
with self.assertRaises(TemplateDoesNotExist) as cm:
self.engine.get_template("existing_template#undefined")
self.assertIn("undefined", str(cm.exception))
@setup(
{
"partial_with_syntax_error": (
"<h1>Title</h1>\n"
"{% partialdef syntax_error_partial %}\n"
" {% if user %}\n"
" <p>User: {{ user.name }}</p>\n"
" {% endif\n"
" <p>Missing closing tag above</p>\n"
"{% endpartialdef %}\n"
"{% partial syntax_error_partial %}\n"
),
}
)
def test_partial_with_syntax_error_exception_info(self):
with self.assertRaises(TemplateSyntaxError) as cm:
self.engine.get_template("partial_with_syntax_error")
self.assertIn("endif", str(cm.exception).lower())
if self.engine.debug:
exc_debug = cm.exception.template_debug
self.assertIn("endpartialdef", exc_debug["during"])
self.assertEqual(exc_debug["name"], "partial_with_syntax_error")
self.assertIn("endif", exc_debug["message"].lower())
@setup(
{
"partial_with_runtime_error": (
"<h1>Title</h1>\n"
"{% load bad_tag %}\n"
"{% partialdef runtime_error_partial %}\n"
" <p>This will raise an error:</p>\n"
" {% badsimpletag %}\n"
"{% endpartialdef %}\n"
"{% partial runtime_error_partial %}\n"
),
}
)
def test_partial_runtime_error_exception_info(self):
template = self.engine.get_template("partial_with_runtime_error")
context = Context()
with self.assertRaises(RuntimeError) as cm:
template.render(context)
if self.engine.debug:
exc_debug = cm.exception.template_debug
self.assertIn("badsimpletag", exc_debug["during"])
self.assertEqual(exc_debug["line"], 5) # Line 5 is where badsimpletag is
self.assertEqual(exc_debug["name"], "partial_with_runtime_error")
self.assertIn("bad simpletag", exc_debug["message"])
@setup(
{
"nested_partial_with_undefined_var": (
"<h1>Title</h1>\n"
"{% partialdef outer_partial %}\n"
' <div class="outer">\n'
" {% partialdef inner_partial %}\n"
" <p>{{ undefined_var }}</p>\n"
" {% endpartialdef %}\n"
" {% partial inner_partial %}\n"
" </div>\n"
"{% endpartialdef %}\n"
"{% partial outer_partial %}\n"
),
}
)
def test_nested_partial_error_exception_info(self):
template = self.engine.get_template("nested_partial_with_undefined_var")
context = Context()
output = template.render(context)
# When string_if_invalid is set, it will show INVALID
# When not set, undefined variables just render as empty string
if hasattr(self.engine, "string_if_invalid") and self.engine.string_if_invalid:
self.assertIn("INVALID", output)
else:
self.assertIn("<p>", output)
self.assertIn("</p>", output)
@setup(
{
"parent.html": (
"<!DOCTYPE html>\n"
"<html>\n"
"<head>{% block title %}Default Title{% endblock %}</head>\n"
"<body>\n"
" {% block content %}{% endblock %}\n"
"</body>\n"
"</html>\n"
),
"child.html": (
"{% extends 'parent.html' %}\n"
"{% block content %}\n"
" {% partialdef content_partial %}\n"
" <p>{{ missing_variable|undefined_filter }}</p>\n"
" {% endpartialdef %}\n"
" {% partial content_partial %}\n"
"{% endblock %}\n"
),
}
)
def test_partial_in_extended_template_error(self):
with self.assertRaises(TemplateSyntaxError) as cm:
self.engine.get_template("child.html")
self.assertIn("undefined_filter", str(cm.exception))
if self.engine.debug:
exc_debug = cm.exception.template_debug
self.assertIn("undefined_filter", exc_debug["during"])
self.assertEqual(exc_debug["name"], "child.html")
self.assertIn("undefined_filter", exc_debug["message"])
@setup(
{
"partial_broken_nesting": (
"<div>Before partial</div>\n"
"{% partialdef outer %}\n"
"{% partialdef inner %}...{% endpartialdef outer %}\n"
"{% endpartialdef inner %}\n"
"<div>After partial content</div>"
)
}
)
def test_broken_partial_nesting(self):
with self.assertRaises(TemplateSyntaxError) as cm:
self.engine.get_template("partial_broken_nesting")
self.assertIn("endpartialdef", str(cm.exception))
self.assertIn("Invalid block tag", str(cm.exception))
self.assertIn("'endpartialdef inner'", str(cm.exception))
reporter = ExceptionReporter(None, cm.exception.__class__, cm.exception, None)
traceback_data = reporter.get_traceback_data()
exception_value = str(traceback_data.get("exception_value", ""))
self.assertIn("Invalid block tag", exception_value)
self.assertIn("'endpartialdef inner'", str(cm.exception))
@setup(
{
"partial_broken_nesting_mixed": (
"<div>Before partial</div>\n"
"{% partialdef outer %}\n"
"{% partialdef inner %}...{% endpartialdef %}\n"
"{% endpartialdef inner %}\n"
"<div>After partial content</div>"
)
}
)
def test_broken_partial_nesting_mixed(self):
with self.assertRaises(TemplateSyntaxError) as cm:
self.engine.get_template("partial_broken_nesting_mixed")
self.assertIn("endpartialdef", str(cm.exception))
self.assertIn("Invalid block tag", str(cm.exception))
self.assertIn("'endpartialdef outer'", str(cm.exception))
reporter = ExceptionReporter(None, cm.exception.__class__, cm.exception, None)
traceback_data = reporter.get_traceback_data()
exception_value = str(traceback_data.get("exception_value", ""))
self.assertIn("Invalid block tag", exception_value)
self.assertIn("'endpartialdef outer'", str(cm.exception))

View File

@ -0,0 +1,2 @@
{% block main %}
{% endblock main %}

View File

@ -0,0 +1,9 @@
{% extends 'partial_base.html' %}
{% partialdef extra-content %}
Extra Content
{% endpartialdef %}
{% block main %}
Main Content
{% endblock %}

View File

@ -0,0 +1,15 @@
{% partialdef test-partial %}
TEST-PARTIAL-CONTENT
{% endpartialdef %}
{% block main %}
BEGINNING
{% partial test-partial %}
MIDDLE
{% partial test-partial %}
END
{% endblock main %}
{% partialdef inline-partial inline %}
INLINE-CONTENT
{% endpartialdef %}

View File

@ -0,0 +1,413 @@
import os
from unittest import mock
from django.http import HttpResponse
from django.template import (
Context,
Origin,
Template,
TemplateDoesNotExist,
TemplateSyntaxError,
engines,
)
from django.template.backends.django import DjangoTemplates
from django.template.loader import render_to_string
from django.test import TestCase, override_settings
from django.urls import path, reverse
engine = engines["django"]
class PartialTagsTests(TestCase):
def test_invalid_template_name_raises_template_does_not_exist(self):
for template_name in [123, None, "", "#", "#name"]:
with (
self.subTest(template_name=template_name),
self.assertRaisesMessage(TemplateDoesNotExist, str(template_name)),
):
engine.get_template(template_name)
def test_template_source_is_correct(self):
partial = engine.get_template("partial_examples.html#test-partial")
self.assertEqual(
partial.template.source,
"{% partialdef test-partial %}\nTEST-PARTIAL-CONTENT\n{% endpartialdef %}",
)
def test_template_source_inline_is_correct(self):
partial = engine.get_template("partial_examples.html#inline-partial")
self.assertEqual(
partial.template.source,
"{% partialdef inline-partial inline %}\nINLINE-CONTENT\n"
"{% endpartialdef %}",
)
def test_full_template_from_loader(self):
template = engine.get_template("partial_examples.html")
rendered = template.render({})
# Check the partial was rendered twice
self.assertEqual(2, rendered.count("TEST-PARTIAL-CONTENT"))
self.assertEqual(1, rendered.count("INLINE-CONTENT"))
def test_chained_exception_forwarded(self):
with self.assertRaises(TemplateDoesNotExist) as ctx:
engine.get_template("not_there.html#not-a-partial")
exception = ctx.exception
self.assertGreater(len(exception.tried), 0)
origin, _ = exception.tried[0]
self.assertEqual(origin.template_name, "not_there.html")
def test_partials_use_cached_loader_when_configured(self):
template_dir = os.path.join(os.path.dirname(__file__), "templates")
backend = DjangoTemplates(
{
"NAME": "django",
"DIRS": [template_dir],
"APP_DIRS": False,
"OPTIONS": {
"loaders": [
(
"django.template.loaders.cached.Loader",
["django.template.loaders.filesystem.Loader"],
),
],
},
}
)
cached_loader = backend.engine.template_loaders[0]
filesystem_loader = cached_loader.loaders[0]
with mock.patch.object(
filesystem_loader, "get_contents", wraps=filesystem_loader.get_contents
) as mock_get_contents:
full_template = backend.get_template("partial_examples.html")
self.assertIn("TEST-PARTIAL-CONTENT", full_template.render({}))
partial_template = backend.get_template(
"partial_examples.html#test-partial"
)
self.assertEqual(
"TEST-PARTIAL-CONTENT", partial_template.render({}).strip()
)
mock_get_contents.assert_called_once()
def test_context_available_in_response_for_partial_template(self):
def sample_view(request):
return HttpResponse(
render_to_string("partial_examples.html#test-partial", {"foo": "bar"})
)
class PartialUrls:
urlpatterns = [path("sample/", sample_view, name="sample-view")]
with override_settings(ROOT_URLCONF=PartialUrls):
response = self.client.get(reverse("sample-view"))
self.assertContains(response, "TEST-PARTIAL-CONTENT")
self.assertEqual(response.context.get("foo"), "bar")
def test_response_with_multiple_parts(self):
context = {}
template_partials = ["partial_child.html", "partial_child.html#extra-content"]
response_whole_content_at_once = HttpResponse(
"".join(
render_to_string(template_name, context)
for template_name in template_partials
)
)
response_with_multiple_writes = HttpResponse()
for template_name in template_partials:
response_with_multiple_writes.write(
render_to_string(template_name, context)
)
response_with_generator = HttpResponse(
render_to_string(template_name, context)
for template_name in template_partials
)
for label, response in [
("response_whole_content_at_once", response_whole_content_at_once),
("response_with_multiple_writes", response_with_multiple_writes),
("response_with_generator", response_with_generator),
]:
with self.subTest(response=label):
self.assertIn(b"Main Content", response.content)
self.assertIn(b"Extra Content", response.content)
def test_partial_engine_assignment_with_real_template(self):
template_with_partial = engine.get_template(
"partial_examples.html#test-partial"
)
self.assertEqual(template_with_partial.template.engine, engine.engine)
rendered_content = template_with_partial.render({})
self.assertEqual("TEST-PARTIAL-CONTENT", rendered_content.strip())
class RobustPartialHandlingTests(TestCase):
def override_get_template(self, **kwargs):
class TemplateWithCustomAttrs:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def render(self, context):
return "rendered content"
template = TemplateWithCustomAttrs(**kwargs)
origin = self.id()
return mock.patch.object(
engine.engine,
"find_template",
return_value=(template, origin),
)
def test_template_without_extra_data_attribute(self):
partial_name = "some_partial_name"
with (
self.override_get_template(),
self.assertRaisesMessage(TemplateDoesNotExist, partial_name),
):
engine.get_template(f"some_template.html#{partial_name}")
def test_template_extract_extra_data_robust(self):
partial_name = "some_partial_name"
for extra_data in (
None,
0,
[],
{},
{"wrong-key": {}},
{"partials": None},
{"partials": {}},
{"partials": []},
{"partials": 0},
):
with (
self.subTest(extra_data=extra_data),
self.override_get_template(extra_data=extra_data),
self.assertRaisesMessage(TemplateDoesNotExist, partial_name),
):
engine.get_template(f"template.html#{partial_name}")
def test_nested_partials_rendering_with_context(self):
template_source = """
{% partialdef outer inline %}
Hello {{ name }}!
{% partialdef inner inline %}
Your age is {{ age }}.
{% endpartialdef inner %}
Nice to meet you.
{% endpartialdef outer %}
"""
template = Template(template_source, origin=Origin(name="template.html"))
context = Context({"name": "Alice", "age": 25})
rendered = template.render(context)
self.assertIn("Hello Alice!", rendered)
self.assertIn("Your age is 25.", rendered)
self.assertIn("Nice to meet you.", rendered)
class FindPartialSourceTests(TestCase):
def test_find_partial_source_success(self):
template = engine.get_template("partial_examples.html").template
partial_proxy = template.extra_data["partials"]["test-partial"]
expected = """{% partialdef test-partial %}
TEST-PARTIAL-CONTENT
{% endpartialdef %}"""
self.assertEqual(partial_proxy.source.strip(), expected.strip())
def test_find_partial_source_with_inline(self):
template = engine.get_template("partial_examples.html").template
partial_proxy = template.extra_data["partials"]["inline-partial"]
expected = """{% partialdef inline-partial inline %}
INLINE-CONTENT
{% endpartialdef %}"""
self.assertEqual(partial_proxy.source.strip(), expected.strip())
def test_find_partial_source_nonexistent_partial(self):
template = engine.get_template("partial_examples.html").template
partial_proxy = template.extra_data["partials"]["test-partial"]
result = partial_proxy.find_partial_source(
template.source, "nonexistent-partial"
)
self.assertEqual(result, "")
def test_find_partial_source_empty_partial(self):
template_source = "{% partialdef empty %}{% endpartialdef %}"
template = Template(template_source)
partial_proxy = template.extra_data["partials"]["empty"]
result = partial_proxy.find_partial_source(template_source, "empty")
self.assertEqual(result, "{% partialdef empty %}{% endpartialdef %}")
def test_find_partial_source_multiple_consecutive_partials(self):
template_source = (
"{% partialdef empty %}{% endpartialdef %}"
"{% partialdef other %}...{% endpartialdef %}"
)
template = Template(template_source)
empty_proxy = template.extra_data["partials"]["empty"]
other_proxy = template.extra_data["partials"]["other"]
empty_result = empty_proxy.find_partial_source(template_source, "empty")
self.assertEqual(empty_result, "{% partialdef empty %}{% endpartialdef %}")
other_result = other_proxy.find_partial_source(template_source, "other")
self.assertEqual(other_result, "{% partialdef other %}...{% endpartialdef %}")
def test_partials_with_duplicate_names(self):
test_cases = [
(
"nested",
"""
{% partialdef duplicate %}{% partialdef duplicate %}
CONTENT
{% endpartialdef %}{% endpartialdef %}
""",
),
(
"conditional",
"""
{% if ... %}
{% partialdef duplicate %}
CONTENT
{% endpartialdef %}
{% else %}
{% partialdef duplicate %}
OTHER-CONTENT
{% endpartialdef %}
{% endif %}
""",
),
]
for test_name, template_source in test_cases:
with self.subTest(test_name=test_name):
with self.assertRaisesMessage(
TemplateSyntaxError,
"Partial 'duplicate' is already defined in the "
"'template.html' template.",
):
Template(template_source, origin=Origin(name="template.html"))
def test_find_partial_source_supports_named_end_tag(self):
template_source = "{% partialdef thing %}CONTENT{% endpartialdef thing %}"
template = Template(template_source)
partial_proxy = template.extra_data["partials"]["thing"]
result = partial_proxy.find_partial_source(template_source, "thing")
self.assertEqual(
result, "{% partialdef thing %}CONTENT{% endpartialdef thing %}"
)
def test_find_partial_source_supports_nested_partials(self):
template_source = (
"{% partialdef outer %}"
"{% partialdef inner %}...{% endpartialdef %}"
"{% endpartialdef %}"
)
template = Template(template_source)
empty_proxy = template.extra_data["partials"]["outer"]
other_proxy = template.extra_data["partials"]["inner"]
outer_result = empty_proxy.find_partial_source(template_source, "outer")
self.assertEqual(
outer_result,
(
"{% partialdef outer %}{% partialdef inner %}"
"...{% endpartialdef %}{% endpartialdef %}"
),
)
inner_result = other_proxy.find_partial_source(template_source, "inner")
self.assertEqual(inner_result, "{% partialdef inner %}...{% endpartialdef %}")
def test_find_partial_source_supports_nested_partials_and_named_end_tags(self):
template_source = (
"{% partialdef outer %}"
"{% partialdef inner %}...{% endpartialdef inner %}"
"{% endpartialdef outer %}"
)
template = Template(template_source)
empty_proxy = template.extra_data["partials"]["outer"]
other_proxy = template.extra_data["partials"]["inner"]
outer_result = empty_proxy.find_partial_source(template_source, "outer")
self.assertEqual(
outer_result,
(
"{% partialdef outer %}{% partialdef inner %}"
"...{% endpartialdef inner %}{% endpartialdef outer %}"
),
)
inner_result = other_proxy.find_partial_source(template_source, "inner")
self.assertEqual(
inner_result, "{% partialdef inner %}...{% endpartialdef inner %}"
)
def test_find_partial_source_supports_nested_partials_and_mixed_end_tags_1(self):
template_source = (
"{% partialdef outer %}"
"{% partialdef inner %}...{% endpartialdef %}"
"{% endpartialdef outer %}"
)
template = Template(template_source)
empty_proxy = template.extra_data["partials"]["outer"]
other_proxy = template.extra_data["partials"]["inner"]
outer_result = empty_proxy.find_partial_source(template_source, "outer")
self.assertEqual(
outer_result,
(
"{% partialdef outer %}{% partialdef inner %}"
"...{% endpartialdef %}{% endpartialdef outer %}"
),
)
inner_result = other_proxy.find_partial_source(template_source, "inner")
self.assertEqual(inner_result, "{% partialdef inner %}...{% endpartialdef %}")
def test_find_partial_source_supports_nested_partials_and_mixed_end_tags_2(self):
template_source = (
"{% partialdef outer %}"
"{% partialdef inner %}...{% endpartialdef inner %}"
"{% endpartialdef %}"
)
template = Template(template_source)
empty_proxy = template.extra_data["partials"]["outer"]
other_proxy = template.extra_data["partials"]["inner"]
outer_result = empty_proxy.find_partial_source(template_source, "outer")
self.assertEqual(
outer_result,
(
"{% partialdef outer %}{% partialdef inner %}"
"...{% endpartialdef inner %}{% endpartialdef %}"
),
)
inner_result = other_proxy.find_partial_source(template_source, "inner")
self.assertEqual(
inner_result, "{% partialdef inner %}...{% endpartialdef inner %}"
)

View File

@ -9,6 +9,7 @@ import pickle
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.utils.datastructures import ( from django.utils.datastructures import (
CaseInsensitiveMapping, CaseInsensitiveMapping,
DeferredSubDict,
DictWrapper, DictWrapper,
ImmutableList, ImmutableList,
MultiValueDict, MultiValueDict,
@ -367,3 +368,42 @@ class CaseInsensitiveMappingTests(SimpleTestCase):
with self.assertRaisesMessage(TypeError, msg): with self.assertRaisesMessage(TypeError, msg):
self.dict1["New Key"] = 1 self.dict1["New Key"] = 1
self.assertEqual(len(self.dict1), 2) self.assertEqual(len(self.dict1), 2)
class DeferredSubDictTests(SimpleTestCase):
def test_basic(self):
parent = {
"settings": {"theme": "dark", "language": "en"},
"config": {"enabled": True, "timeout": 30},
}
sub = DeferredSubDict(parent, "settings")
self.assertEqual(sub["theme"], "dark")
self.assertEqual(sub["language"], "en")
with self.assertRaises(KeyError):
sub["enabled"]
def test_reflects_changes_in_parent(self):
parent = {"settings": {"theme": "dark"}}
sub = DeferredSubDict(parent, "settings")
parent["settings"]["theme"] = "light"
self.assertEqual(sub["theme"], "light")
parent["settings"]["mode"] = "tight"
self.assertEqual(sub["mode"], "tight")
def test_missing_deferred_key_raises_keyerror(self):
parent = {"settings": {"theme": "dark"}}
sub = DeferredSubDict(parent, "nonexistent")
with self.assertRaises(KeyError):
sub["anything"]
def test_missing_child_key_raises_keyerror(self):
parent = {"settings": {"theme": "dark"}}
sub = DeferredSubDict(parent, "settings")
with self.assertRaises(KeyError):
sub["nonexistent"]
def test_child_not_a_dict_raises_typeerror(self):
parent = {"bad": "not_a_dict"}
sub = DeferredSubDict(parent, "bad")
with self.assertRaises(TypeError):
sub["any_key"]