Skip to content

Commit d82f25d

Browse files
FarhanAliRazajacobtylerwalls
authored andcommitted
Fixed #36559 -- Respected verbatim and comment blocks in PartialTemplate.source.
1 parent 3485599 commit d82f25d

3 files changed

Lines changed: 185 additions & 67 deletions

File tree

django/template/base.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,6 @@
8989
# than instantiating SimpleLazyObject with _lazy_re_compile().
9090
tag_re = re.compile(r"({%.*?%}|{{.*?}}|{#.*?#})")
9191

92-
combined_partial_re = re.compile(
93-
r"{%\s*partialdef\s+(?P<name>[\w-]+)(?:\s+inline)?\s*%}"
94-
r"|{%\s*endpartialdef(?:\s+[\w-]+)?\s*%}"
95-
)
96-
9792
logger = logging.getLogger("django.template")
9893

9994

@@ -301,29 +296,26 @@ class PartialTemplate:
301296
Wraps nodelist as a partial, in order to be able to bind context.
302297
"""
303298

304-
def __init__(self, nodelist, origin, name):
299+
def __init__(self, nodelist, origin, name, source_start=None, source_end=None):
305300
self.nodelist = nodelist
306301
self.origin = origin
307302
self.name = name
303+
# If available (debug mode), the absolute character offsets in the
304+
# template.source correspond to the full partial region.
305+
self._source_start = source_start
306+
self._source_end = source_end
308307

309308
def get_exception_info(self, exception, token):
310309
template = self.origin.loader.get_template(self.origin.template_name)
311310
return template.get_exception_info(exception, token)
312311

313-
def find_partial_source(self, full_source, partial_name):
314-
start_match = None
315-
nesting = 0
316-
317-
for match in combined_partial_re.finditer(full_source):
318-
if name := match["name"]: # Opening tag.
319-
if start_match is None and name == partial_name:
320-
start_match = match
321-
if start_match is not None:
322-
nesting += 1
323-
elif start_match is not None:
324-
nesting -= 1
325-
if nesting == 0:
326-
return full_source[start_match.start() : match.end()]
312+
def find_partial_source(self, full_source):
313+
if (
314+
self._source_start is not None
315+
and self._source_end is not None
316+
and 0 <= self._source_start <= self._source_end <= len(full_source)
317+
):
318+
return full_source[self._source_start : self._source_end]
327319

328320
return ""
329321

@@ -337,7 +329,7 @@ def source(self):
337329
RuntimeWarning,
338330
stacklevel=2,
339331
)
340-
return self.find_partial_source(template.source, self.name)
332+
return self.find_partial_source(template.source)
341333

342334
def _render(self, context):
343335
return self.nodelist.render(context)

django/template/defaulttags.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1235,19 +1235,32 @@ def partialdef_func(parser, token):
12351235

12361236
# Parse the content until the end tag.
12371237
valid_endpartials = ("endpartialdef", f"endpartialdef {partial_name}")
1238+
1239+
pos_open = getattr(token, "position", None)
1240+
source_start = pos_open[0] if isinstance(pos_open, tuple) else None
1241+
12381242
nodelist = parser.parse(valid_endpartials)
12391243
endpartial = parser.next_token()
12401244
if endpartial.contents not in valid_endpartials:
12411245
parser.invalid_block_tag(endpartial, "endpartialdef", valid_endpartials)
12421246

1247+
pos_close = getattr(endpartial, "position", None)
1248+
source_end = pos_close[1] if isinstance(pos_close, tuple) else None
1249+
12431250
# Store the partial nodelist in the parser.extra_data attribute.
12441251
partials = parser.extra_data.setdefault("partials", {})
12451252
if partial_name in partials:
12461253
raise TemplateSyntaxError(
12471254
f"Partial '{partial_name}' is already defined in the "
12481255
f"'{parser.origin.name}' template."
12491256
)
1250-
partials[partial_name] = PartialTemplate(nodelist, parser.origin, partial_name)
1257+
partials[partial_name] = PartialTemplate(
1258+
nodelist,
1259+
parser.origin,
1260+
partial_name,
1261+
source_start=source_start,
1262+
source_end=source_end,
1263+
)
12511264

12521265
return PartialDefNode(partial_name, inline, nodelist)
12531266

tests/template_tests/test_partials.py

Lines changed: 158 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -32,33 +32,6 @@ def test_invalid_template_name_raises_template_does_not_exist(self):
3232
):
3333
engine.get_template(template_name)
3434

35-
def test_template_source_is_correct(self):
36-
partial = engine.get_template("partial_examples.html#test-partial")
37-
msg = (
38-
"PartialTemplate.source is only available when "
39-
"template debugging is enabled."
40-
)
41-
with self.assertRaisesMessage(RuntimeWarning, msg):
42-
self.assertEqual(
43-
partial.template.source,
44-
"{% partialdef test-partial %}\n"
45-
"TEST-PARTIAL-CONTENT\n"
46-
"{% endpartialdef %}",
47-
)
48-
49-
def test_template_source_inline_is_correct(self):
50-
partial = engine.get_template("partial_examples.html#inline-partial")
51-
msg = (
52-
"PartialTemplate.source is only available when "
53-
"template debugging is enabled."
54-
)
55-
with self.assertRaisesMessage(RuntimeWarning, msg):
56-
self.assertEqual(
57-
partial.template.source,
58-
"{% partialdef inline-partial inline %}\nINLINE-CONTENT\n"
59-
"{% endpartialdef %}",
60-
)
61-
6235
def test_full_template_from_loader(self):
6336
template = engine.get_template("partial_examples.html")
6437
rendered = template.render({})
@@ -172,12 +145,7 @@ def test_template_source_warning(self):
172145
"PartialTemplate.source is only available when template "
173146
"debugging is enabled.",
174147
):
175-
self.assertEqual(
176-
partial.template.source,
177-
"{% partialdef test-partial %}\n"
178-
"TEST-PARTIAL-CONTENT\n"
179-
"{% endpartialdef %}",
180-
)
148+
self.assertEqual(partial.template.source, "")
181149

182150

183151
class RobustPartialHandlingTests(TestCase):
@@ -287,6 +255,20 @@ def test_find_partial_source_with_inline(self):
287255
{% endpartialdef %}"""
288256
self.assertEqual(partial_proxy.source.strip(), expected.strip())
289257

258+
def test_find_partial_source_fallback_cases(self):
259+
cases = {"None offsets": (None, None), "Out of bounds offsets": (10, 20)}
260+
for name, (source_start, source_end) in cases.items():
261+
with self.subTest(name):
262+
partial = PartialTemplate(
263+
NodeList(),
264+
Origin("test"),
265+
"test",
266+
source_start=source_start,
267+
source_end=source_end,
268+
)
269+
result = partial.find_partial_source("nonexistent-partial")
270+
self.assertEqual(result, "")
271+
290272
@setup(
291273
{
292274
"empty_partial_template": ("{% partialdef empty %}{% endpartialdef %}"),
@@ -297,7 +279,7 @@ def test_find_partial_source_empty_partial(self):
297279
template = self.engine.get_template("empty_partial_template")
298280
partial_proxy = template.extra_data["partials"]["empty"]
299281

300-
result = partial_proxy.find_partial_source(template.source, "empty")
282+
result = partial_proxy.find_partial_source(template.source)
301283
self.assertEqual(result, "{% partialdef empty %}{% endpartialdef %}")
302284

303285
@setup(
@@ -315,10 +297,10 @@ def test_find_partial_source_multiple_consecutive_partials(self):
315297
empty_proxy = template.extra_data["partials"]["empty"]
316298
other_proxy = template.extra_data["partials"]["other"]
317299

318-
empty_result = empty_proxy.find_partial_source(template.source, "empty")
300+
empty_result = empty_proxy.find_partial_source(template.source)
319301
self.assertEqual(empty_result, "{% partialdef empty %}{% endpartialdef %}")
320302

321-
other_result = other_proxy.find_partial_source(template.source, "other")
303+
other_result = other_proxy.find_partial_source(template.source)
322304
self.assertEqual(other_result, "{% partialdef other %}...{% endpartialdef %}")
323305

324306
def test_partials_with_duplicate_names(self):
@@ -368,7 +350,7 @@ def test_find_partial_source_supports_named_end_tag(self):
368350
template = self.engine.get_template("named_end_tag_template")
369351
partial_proxy = template.extra_data["partials"]["thing"]
370352

371-
result = partial_proxy.find_partial_source(template.source, "thing")
353+
result = partial_proxy.find_partial_source(template.source)
372354
self.assertEqual(
373355
result, "{% partialdef thing %}CONTENT{% endpartialdef thing %}"
374356
)
@@ -389,7 +371,7 @@ def test_find_partial_source_supports_nested_partials(self):
389371
empty_proxy = template.extra_data["partials"]["outer"]
390372
other_proxy = template.extra_data["partials"]["inner"]
391373

392-
outer_result = empty_proxy.find_partial_source(template.source, "outer")
374+
outer_result = empty_proxy.find_partial_source(template.source)
393375
self.assertEqual(
394376
outer_result,
395377
(
@@ -398,7 +380,7 @@ def test_find_partial_source_supports_nested_partials(self):
398380
),
399381
)
400382

401-
inner_result = other_proxy.find_partial_source(template.source, "inner")
383+
inner_result = other_proxy.find_partial_source(template.source)
402384
self.assertEqual(inner_result, "{% partialdef inner %}...{% endpartialdef %}")
403385

404386
@setup(
@@ -417,7 +399,7 @@ def test_find_partial_source_supports_nested_partials_and_named_end_tags(self):
417399
empty_proxy = template.extra_data["partials"]["outer"]
418400
other_proxy = template.extra_data["partials"]["inner"]
419401

420-
outer_result = empty_proxy.find_partial_source(template.source, "outer")
402+
outer_result = empty_proxy.find_partial_source(template.source)
421403
self.assertEqual(
422404
outer_result,
423405
(
@@ -426,7 +408,7 @@ def test_find_partial_source_supports_nested_partials_and_named_end_tags(self):
426408
),
427409
)
428410

429-
inner_result = other_proxy.find_partial_source(template.source, "inner")
411+
inner_result = other_proxy.find_partial_source(template.source)
430412
self.assertEqual(
431413
inner_result, "{% partialdef inner %}...{% endpartialdef inner %}"
432414
)
@@ -447,7 +429,7 @@ def test_find_partial_source_supports_nested_partials_and_mixed_end_tags_1(self)
447429
empty_proxy = template.extra_data["partials"]["outer"]
448430
other_proxy = template.extra_data["partials"]["inner"]
449431

450-
outer_result = empty_proxy.find_partial_source(template.source, "outer")
432+
outer_result = empty_proxy.find_partial_source(template.source)
451433
self.assertEqual(
452434
outer_result,
453435
(
@@ -456,7 +438,7 @@ def test_find_partial_source_supports_nested_partials_and_mixed_end_tags_1(self)
456438
),
457439
)
458440

459-
inner_result = other_proxy.find_partial_source(template.source, "inner")
441+
inner_result = other_proxy.find_partial_source(template.source)
460442
self.assertEqual(inner_result, "{% partialdef inner %}...{% endpartialdef %}")
461443

462444
@setup(
@@ -475,7 +457,7 @@ def test_find_partial_source_supports_nested_partials_and_mixed_end_tags_2(self)
475457
empty_proxy = template.extra_data["partials"]["outer"]
476458
other_proxy = template.extra_data["partials"]["inner"]
477459

478-
outer_result = empty_proxy.find_partial_source(template.source, "outer")
460+
outer_result = empty_proxy.find_partial_source(template.source)
479461
self.assertEqual(
480462
outer_result,
481463
(
@@ -484,7 +466,138 @@ def test_find_partial_source_supports_nested_partials_and_mixed_end_tags_2(self)
484466
),
485467
)
486468

487-
inner_result = other_proxy.find_partial_source(template.source, "inner")
469+
inner_result = other_proxy.find_partial_source(template.source)
488470
self.assertEqual(
489471
inner_result, "{% partialdef inner %}...{% endpartialdef inner %}"
490472
)
473+
474+
@setup(
475+
{
476+
"partial_embedded_in_verbatim": (
477+
"{% verbatim %}\n"
478+
"{% partialdef testing-name %}\n"
479+
"<p>Should be ignored</p>"
480+
"{% endpartialdef testing-name %}\n"
481+
"{% endverbatim %}\n"
482+
"{% partialdef testing-name %}\n"
483+
"<p>Content</p>\n"
484+
"{% endpartialdef %}\n"
485+
),
486+
},
487+
debug_only=True,
488+
)
489+
def test_partial_template_embedded_in_verbatim(self):
490+
template = self.engine.get_template("partial_embedded_in_verbatim")
491+
partial_template = template.extra_data["partials"]["testing-name"]
492+
self.assertEqual(
493+
partial_template.source,
494+
"{% partialdef testing-name %}\n<p>Content</p>\n{% endpartialdef %}",
495+
)
496+
497+
@setup(
498+
{
499+
"partial_debug_source": (
500+
"{% partialdef testing-name %}\n"
501+
"<p>Content</p>\n"
502+
"{% endpartialdef %}\n"
503+
),
504+
},
505+
debug_only=True,
506+
)
507+
def test_partial_source_uses_offsets_in_debug(self):
508+
template = self.engine.get_template("partial_debug_source")
509+
partial_template = template.extra_data["partials"]["testing-name"]
510+
511+
self.assertEqual(partial_template._source_start, 0)
512+
self.assertEqual(partial_template._source_end, 64)
513+
expected = template.source[
514+
partial_template._source_start : partial_template._source_end
515+
]
516+
self.assertEqual(partial_template.source, expected)
517+
518+
@setup(
519+
{
520+
"partial_embedded_in_named_verbatim": (
521+
"{% verbatim block1 %}\n"
522+
"{% partialdef testing-name %}\n"
523+
"{% endverbatim block1 %}\n"
524+
"{% partialdef testing-name %}\n"
525+
"<p>Named Content</p>\n"
526+
"{% endpartialdef %}\n"
527+
),
528+
},
529+
debug_only=True,
530+
)
531+
def test_partial_template_embedded_in_named_verbatim(self):
532+
template = self.engine.get_template("partial_embedded_in_named_verbatim")
533+
partial_template = template.extra_data["partials"]["testing-name"]
534+
self.assertEqual(
535+
"{% partialdef testing-name %}\n<p>Named Content</p>\n{% endpartialdef %}",
536+
partial_template.source,
537+
)
538+
539+
@setup(
540+
{
541+
"partial_embedded_in_comment_block": (
542+
"{% comment %}\n"
543+
"{% partialdef testing-name %}\n"
544+
"{% endcomment %}\n"
545+
"{% partialdef testing-name %}\n"
546+
"<p>Comment Content</p>\n"
547+
"{% endpartialdef %}\n"
548+
),
549+
},
550+
debug_only=True,
551+
)
552+
def test_partial_template_embedded_in_comment_block(self):
553+
template = self.engine.get_template("partial_embedded_in_comment_block")
554+
partial_template = template.extra_data["partials"]["testing-name"]
555+
self.assertEqual(
556+
partial_template.source,
557+
"{% partialdef testing-name %}\n"
558+
"<p>Comment Content</p>\n"
559+
"{% endpartialdef %}",
560+
)
561+
562+
@setup(
563+
{
564+
"partial_embedded_in_inline_comment": (
565+
"{# {% partialdef testing-name %} #}\n"
566+
"{% partialdef testing-name %}\n"
567+
"<p>Inline Comment Content</p>\n"
568+
"{% endpartialdef %}\n"
569+
),
570+
},
571+
debug_only=True,
572+
)
573+
def test_partial_template_embedded_in_inline_comment(self):
574+
template = self.engine.get_template("partial_embedded_in_inline_comment")
575+
partial_template = template.extra_data["partials"]["testing-name"]
576+
self.assertEqual(
577+
partial_template.source,
578+
"{% partialdef testing-name %}\n"
579+
"<p>Inline Comment Content</p>\n"
580+
"{% endpartialdef %}",
581+
)
582+
583+
@setup(
584+
{
585+
"partial_contains_fake_end_inside_verbatim": (
586+
"{% partialdef testing-name %}\n"
587+
"{% verbatim %}{% endpartialdef %}{% endverbatim %}\n"
588+
"<p>Body</p>\n"
589+
"{% endpartialdef %}\n"
590+
),
591+
},
592+
debug_only=True,
593+
)
594+
def test_partial_template_contains_fake_end_inside_verbatim(self):
595+
template = self.engine.get_template("partial_contains_fake_end_inside_verbatim")
596+
partial_template = template.extra_data["partials"]["testing-name"]
597+
self.assertEqual(
598+
partial_template.source,
599+
"{% partialdef testing-name %}\n"
600+
"{% verbatim %}{% endpartialdef %}{% endverbatim %}\n"
601+
"<p>Body</p>\n"
602+
"{% endpartialdef %}",
603+
)

0 commit comments

Comments
 (0)