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,
NodeList,
Origin,
PartialTemplate,
Template,
Variable,
)

View File

@ -88,6 +88,11 @@ UNKNOWN_SOURCE = "<unknown source>"
# than instantiating SimpleLazyObject with _lazy_re_compile().
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")
@ -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):
yield 0
p = template_source.find("\n")

View File

@ -12,6 +12,7 @@ from itertools import groupby
from django.conf import settings
from django.http import QueryDict
from django.utils import timezone
from django.utils.datastructures import DeferredSubDict
from django.utils.html import conditional_escape, escape, format_html
from django.utils.lorem_ipsum import paragraphs, words
from django.utils.safestring import mark_safe
@ -29,6 +30,7 @@ from .base import (
VARIABLE_TAG_START,
Node,
NodeList,
PartialTemplate,
TemplateSyntaxError,
VariableDoesNotExist,
kwarg_re,
@ -408,6 +410,31 @@ class NowNode(Node):
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):
def __init__(self, node):
self.node = node
@ -1174,6 +1201,75 @@ def now(parser, token):
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)
def querystring(context, *args, **kwargs):
"""

View File

@ -174,11 +174,31 @@ class Engine:
Return a compiled Template object for the given template name,
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)
if not hasattr(template, "render"):
# template needs to be compiled
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):
"""

View File

@ -24,7 +24,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.core.signals import request_started, setting_changed
from django.db import DEFAULT_DB_ALIAS, connections, reset_queries
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.urls import get_script_prefix, set_script_prefix
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"
saved_data.template_render = Template._render
saved_data.partial_template_render = PartialTemplate._render
Template._render = instrumented_test_render
PartialTemplate._render = instrumented_test_render
mail.outbox = []
@ -165,6 +167,7 @@ def teardown_test_environment():
settings.DEBUG = saved_data.debug
settings.EMAIL_BACKEND = saved_data.email_backend
Template._render = saved_data.template_render
PartialTemplate._render = saved_data.partial_template_render
del _TestState.saved_data
del mail.outbox

View File

@ -345,3 +345,21 @@ class CaseInsensitiveMapping(Mapping):
"Element key %r invalid, only strings are allowed" % elem[0]
)
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 %}
{% 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
``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
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
.. _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
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
--------------

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
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)
``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.utils.datastructures import (
CaseInsensitiveMapping,
DeferredSubDict,
DictWrapper,
ImmutableList,
MultiValueDict,
@ -367,3 +368,42 @@ class CaseInsensitiveMappingTests(SimpleTestCase):
with self.assertRaisesMessage(TypeError, msg):
self.dict1["New Key"] = 1
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"]