summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfarhan <farhanalirazaazeemi@gmail.com>2025-09-02 15:00:40 -0400
committerJacob Walls <jacobtylerwalls@gmail.com>2025-09-03 10:59:58 -0400
commitd82f25d3f0f4eb7be721a72d0e79a8d13d394d32 (patch)
tree376c5b2eeb5252e3ea252d5d71e8952eff2c5c7f
parent3485599ef08811e2fd066458aa083b2567f3cb84 (diff)
Fixed #36559 -- Respected verbatim and comment blocks in PartialTemplate.source.
-rw-r--r--django/template/base.py34
-rw-r--r--django/template/defaulttags.py15
-rw-r--r--tests/template_tests/test_partials.py203
3 files changed, 185 insertions, 67 deletions
diff --git a/django/template/base.py b/django/template/base.py
index d7bc59d668..37e7243d5c 100644
--- a/django/template/base.py
+++ b/django/template/base.py
@@ -89,11 +89,6 @@ 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")
@@ -301,29 +296,26 @@ class PartialTemplate:
Wraps nodelist as a partial, in order to be able to bind context.
"""
- def __init__(self, nodelist, origin, name):
+ def __init__(self, nodelist, origin, name, source_start=None, source_end=None):
self.nodelist = nodelist
self.origin = origin
self.name = name
+ # If available (debug mode), the absolute character offsets in the
+ # template.source correspond to the full partial region.
+ self._source_start = source_start
+ self._source_end = source_end
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()]
+ def find_partial_source(self, full_source):
+ if (
+ self._source_start is not None
+ and self._source_end is not None
+ and 0 <= self._source_start <= self._source_end <= len(full_source)
+ ):
+ return full_source[self._source_start : self._source_end]
return ""
@@ -337,7 +329,7 @@ class PartialTemplate:
RuntimeWarning,
stacklevel=2,
)
- return self.find_partial_source(template.source, self.name)
+ return self.find_partial_source(template.source)
def _render(self, context):
return self.nodelist.render(context)
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index 4cbaf852e1..ac3d5de901 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -1235,11 +1235,18 @@ def partialdef_func(parser, token):
# Parse the content until the end tag.
valid_endpartials = ("endpartialdef", f"endpartialdef {partial_name}")
+
+ pos_open = getattr(token, "position", None)
+ source_start = pos_open[0] if isinstance(pos_open, tuple) else None
+
nodelist = parser.parse(valid_endpartials)
endpartial = parser.next_token()
if endpartial.contents not in valid_endpartials:
parser.invalid_block_tag(endpartial, "endpartialdef", valid_endpartials)
+ pos_close = getattr(endpartial, "position", None)
+ source_end = pos_close[1] if isinstance(pos_close, tuple) else None
+
# Store the partial nodelist in the parser.extra_data attribute.
partials = parser.extra_data.setdefault("partials", {})
if partial_name in partials:
@@ -1247,7 +1254,13 @@ def partialdef_func(parser, token):
f"Partial '{partial_name}' is already defined in the "
f"'{parser.origin.name}' template."
)
- partials[partial_name] = PartialTemplate(nodelist, parser.origin, partial_name)
+ partials[partial_name] = PartialTemplate(
+ nodelist,
+ parser.origin,
+ partial_name,
+ source_start=source_start,
+ source_end=source_end,
+ )
return PartialDefNode(partial_name, inline, nodelist)
diff --git a/tests/template_tests/test_partials.py b/tests/template_tests/test_partials.py
index cc3ef7fb25..9762436fdc 100644
--- a/tests/template_tests/test_partials.py
+++ b/tests/template_tests/test_partials.py
@@ -32,33 +32,6 @@ class PartialTagsTests(TestCase):
):
engine.get_template(template_name)
- def test_template_source_is_correct(self):
- partial = engine.get_template("partial_examples.html#test-partial")
- msg = (
- "PartialTemplate.source is only available when "
- "template debugging is enabled."
- )
- with self.assertRaisesMessage(RuntimeWarning, msg):
- self.assertEqual(
- partial.template.source,
- "{% partialdef test-partial %}\n"
- "TEST-PARTIAL-CONTENT\n"
- "{% endpartialdef %}",
- )
-
- def test_template_source_inline_is_correct(self):
- partial = engine.get_template("partial_examples.html#inline-partial")
- msg = (
- "PartialTemplate.source is only available when "
- "template debugging is enabled."
- )
- with self.assertRaisesMessage(RuntimeWarning, msg):
- 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({})
@@ -172,12 +145,7 @@ class PartialTagsTests(TestCase):
"PartialTemplate.source is only available when template "
"debugging is enabled.",
):
- self.assertEqual(
- partial.template.source,
- "{% partialdef test-partial %}\n"
- "TEST-PARTIAL-CONTENT\n"
- "{% endpartialdef %}",
- )
+ self.assertEqual(partial.template.source, "")
class RobustPartialHandlingTests(TestCase):
@@ -287,6 +255,20 @@ INLINE-CONTENT
{% endpartialdef %}"""
self.assertEqual(partial_proxy.source.strip(), expected.strip())
+ def test_find_partial_source_fallback_cases(self):
+ cases = {"None offsets": (None, None), "Out of bounds offsets": (10, 20)}
+ for name, (source_start, source_end) in cases.items():
+ with self.subTest(name):
+ partial = PartialTemplate(
+ NodeList(),
+ Origin("test"),
+ "test",
+ source_start=source_start,
+ source_end=source_end,
+ )
+ result = partial.find_partial_source("nonexistent-partial")
+ self.assertEqual(result, "")
+
@setup(
{
"empty_partial_template": ("{% partialdef empty %}{% endpartialdef %}"),
@@ -297,7 +279,7 @@ INLINE-CONTENT
template = self.engine.get_template("empty_partial_template")
partial_proxy = template.extra_data["partials"]["empty"]
- result = partial_proxy.find_partial_source(template.source, "empty")
+ result = partial_proxy.find_partial_source(template.source)
self.assertEqual(result, "{% partialdef empty %}{% endpartialdef %}")
@setup(
@@ -315,10 +297,10 @@ INLINE-CONTENT
empty_proxy = template.extra_data["partials"]["empty"]
other_proxy = template.extra_data["partials"]["other"]
- empty_result = empty_proxy.find_partial_source(template.source, "empty")
+ empty_result = empty_proxy.find_partial_source(template.source)
self.assertEqual(empty_result, "{% partialdef empty %}{% endpartialdef %}")
- other_result = other_proxy.find_partial_source(template.source, "other")
+ other_result = other_proxy.find_partial_source(template.source)
self.assertEqual(other_result, "{% partialdef other %}...{% endpartialdef %}")
def test_partials_with_duplicate_names(self):
@@ -368,7 +350,7 @@ INLINE-CONTENT
template = self.engine.get_template("named_end_tag_template")
partial_proxy = template.extra_data["partials"]["thing"]
- result = partial_proxy.find_partial_source(template.source, "thing")
+ result = partial_proxy.find_partial_source(template.source)
self.assertEqual(
result, "{% partialdef thing %}CONTENT{% endpartialdef thing %}"
)
@@ -389,7 +371,7 @@ INLINE-CONTENT
empty_proxy = template.extra_data["partials"]["outer"]
other_proxy = template.extra_data["partials"]["inner"]
- outer_result = empty_proxy.find_partial_source(template.source, "outer")
+ outer_result = empty_proxy.find_partial_source(template.source)
self.assertEqual(
outer_result,
(
@@ -398,7 +380,7 @@ INLINE-CONTENT
),
)
- inner_result = other_proxy.find_partial_source(template.source, "inner")
+ inner_result = other_proxy.find_partial_source(template.source)
self.assertEqual(inner_result, "{% partialdef inner %}...{% endpartialdef %}")
@setup(
@@ -417,7 +399,7 @@ INLINE-CONTENT
empty_proxy = template.extra_data["partials"]["outer"]
other_proxy = template.extra_data["partials"]["inner"]
- outer_result = empty_proxy.find_partial_source(template.source, "outer")
+ outer_result = empty_proxy.find_partial_source(template.source)
self.assertEqual(
outer_result,
(
@@ -426,7 +408,7 @@ INLINE-CONTENT
),
)
- inner_result = other_proxy.find_partial_source(template.source, "inner")
+ inner_result = other_proxy.find_partial_source(template.source)
self.assertEqual(
inner_result, "{% partialdef inner %}...{% endpartialdef inner %}"
)
@@ -447,7 +429,7 @@ INLINE-CONTENT
empty_proxy = template.extra_data["partials"]["outer"]
other_proxy = template.extra_data["partials"]["inner"]
- outer_result = empty_proxy.find_partial_source(template.source, "outer")
+ outer_result = empty_proxy.find_partial_source(template.source)
self.assertEqual(
outer_result,
(
@@ -456,7 +438,7 @@ INLINE-CONTENT
),
)
- inner_result = other_proxy.find_partial_source(template.source, "inner")
+ inner_result = other_proxy.find_partial_source(template.source)
self.assertEqual(inner_result, "{% partialdef inner %}...{% endpartialdef %}")
@setup(
@@ -475,7 +457,7 @@ INLINE-CONTENT
empty_proxy = template.extra_data["partials"]["outer"]
other_proxy = template.extra_data["partials"]["inner"]
- outer_result = empty_proxy.find_partial_source(template.source, "outer")
+ outer_result = empty_proxy.find_partial_source(template.source)
self.assertEqual(
outer_result,
(
@@ -484,7 +466,138 @@ INLINE-CONTENT
),
)
- inner_result = other_proxy.find_partial_source(template.source, "inner")
+ inner_result = other_proxy.find_partial_source(template.source)
self.assertEqual(
inner_result, "{% partialdef inner %}...{% endpartialdef inner %}"
)
+
+ @setup(
+ {
+ "partial_embedded_in_verbatim": (
+ "{% verbatim %}\n"
+ "{% partialdef testing-name %}\n"
+ "<p>Should be ignored</p>"
+ "{% endpartialdef testing-name %}\n"
+ "{% endverbatim %}\n"
+ "{% partialdef testing-name %}\n"
+ "<p>Content</p>\n"
+ "{% endpartialdef %}\n"
+ ),
+ },
+ debug_only=True,
+ )
+ def test_partial_template_embedded_in_verbatim(self):
+ template = self.engine.get_template("partial_embedded_in_verbatim")
+ partial_template = template.extra_data["partials"]["testing-name"]
+ self.assertEqual(
+ partial_template.source,
+ "{% partialdef testing-name %}\n<p>Content</p>\n{% endpartialdef %}",
+ )
+
+ @setup(
+ {
+ "partial_debug_source": (
+ "{% partialdef testing-name %}\n"
+ "<p>Content</p>\n"
+ "{% endpartialdef %}\n"
+ ),
+ },
+ debug_only=True,
+ )
+ def test_partial_source_uses_offsets_in_debug(self):
+ template = self.engine.get_template("partial_debug_source")
+ partial_template = template.extra_data["partials"]["testing-name"]
+
+ self.assertEqual(partial_template._source_start, 0)
+ self.assertEqual(partial_template._source_end, 64)
+ expected = template.source[
+ partial_template._source_start : partial_template._source_end
+ ]
+ self.assertEqual(partial_template.source, expected)
+
+ @setup(
+ {
+ "partial_embedded_in_named_verbatim": (
+ "{% verbatim block1 %}\n"
+ "{% partialdef testing-name %}\n"
+ "{% endverbatim block1 %}\n"
+ "{% partialdef testing-name %}\n"
+ "<p>Named Content</p>\n"
+ "{% endpartialdef %}\n"
+ ),
+ },
+ debug_only=True,
+ )
+ def test_partial_template_embedded_in_named_verbatim(self):
+ template = self.engine.get_template("partial_embedded_in_named_verbatim")
+ partial_template = template.extra_data["partials"]["testing-name"]
+ self.assertEqual(
+ "{% partialdef testing-name %}\n<p>Named Content</p>\n{% endpartialdef %}",
+ partial_template.source,
+ )
+
+ @setup(
+ {
+ "partial_embedded_in_comment_block": (
+ "{% comment %}\n"
+ "{% partialdef testing-name %}\n"
+ "{% endcomment %}\n"
+ "{% partialdef testing-name %}\n"
+ "<p>Comment Content</p>\n"
+ "{% endpartialdef %}\n"
+ ),
+ },
+ debug_only=True,
+ )
+ def test_partial_template_embedded_in_comment_block(self):
+ template = self.engine.get_template("partial_embedded_in_comment_block")
+ partial_template = template.extra_data["partials"]["testing-name"]
+ self.assertEqual(
+ partial_template.source,
+ "{% partialdef testing-name %}\n"
+ "<p>Comment Content</p>\n"
+ "{% endpartialdef %}",
+ )
+
+ @setup(
+ {
+ "partial_embedded_in_inline_comment": (
+ "{# {% partialdef testing-name %} #}\n"
+ "{% partialdef testing-name %}\n"
+ "<p>Inline Comment Content</p>\n"
+ "{% endpartialdef %}\n"
+ ),
+ },
+ debug_only=True,
+ )
+ def test_partial_template_embedded_in_inline_comment(self):
+ template = self.engine.get_template("partial_embedded_in_inline_comment")
+ partial_template = template.extra_data["partials"]["testing-name"]
+ self.assertEqual(
+ partial_template.source,
+ "{% partialdef testing-name %}\n"
+ "<p>Inline Comment Content</p>\n"
+ "{% endpartialdef %}",
+ )
+
+ @setup(
+ {
+ "partial_contains_fake_end_inside_verbatim": (
+ "{% partialdef testing-name %}\n"
+ "{% verbatim %}{% endpartialdef %}{% endverbatim %}\n"
+ "<p>Body</p>\n"
+ "{% endpartialdef %}\n"
+ ),
+ },
+ debug_only=True,
+ )
+ def test_partial_template_contains_fake_end_inside_verbatim(self):
+ template = self.engine.get_template("partial_contains_fake_end_inside_verbatim")
+ partial_template = template.extra_data["partials"]["testing-name"]
+ self.assertEqual(
+ partial_template.source,
+ "{% partialdef testing-name %}\n"
+ "{% verbatim %}{% endpartialdef %}{% endverbatim %}\n"
+ "<p>Body</p>\n"
+ "{% endpartialdef %}",
+ )