diff --git a/render_block/django.py b/render_block/django.py index 977ac7d..0aa2844 100644 --- a/render_block/django.py +++ b/render_block/django.py @@ -1,4 +1,5 @@ from copy import copy +import hashlib from django.template import Context, RequestContext from django.template.base import TextNode @@ -12,6 +13,8 @@ from render_block.exceptions import BlockNotFound +_NODES_CACHE = {} + def django_render_block(template, block_name, context, request=None): # Create a Django Context if needed @@ -30,24 +33,47 @@ def django_render_block(template, block_name, context, request=None): # Get the underlying django.template.base.Template object. template = template.template + cache_key = _make_node_cache_key(template, block_name) # Bind the template to the context. with context_instance.bind_template(template): - # Before trying to render the template, we need to traverse the tree of - # parent templates and find all blocks in them. - parent_template = _build_block_context(template, context_instance) - try: - return _render_template_block(template, block_name, context_instance) - except BlockNotFound: - # The block wasn't found in the current template. + node, render_context = _NODES_CACHE[cache_key] + except KeyError: + # Before trying to render the template, we need to traverse the tree of + # parent templates and find all blocks in them. + parent_template = _build_block_context(template, context_instance) - # If there's no parent template (i.e. no ExtendsNode), re-raise. - if not parent_template: - raise + try: + node, render_context = _find_template_block( + template, block_name, context_instance + ) + except BlockNotFound: + # The block wasn't found in the current template. + + # If there's no parent template (i.e. no ExtendsNode), re-raise. + if not parent_template: + raise - # Check the parent template for this block. - return _render_template_block(parent_template, block_name, context_instance) + # Check the parent template for this block. + node, render_context = _find_template_block( + parent_template, block_name, context_instance + ) + + if cache_key and not template.engine.debug: + _NODES_CACHE[cache_key] = node, render_context + + context_instance.render_context = render_context + return node.render(context_instance) + + +def _make_node_cache_key(template, block_name): + if template.name: + key = template.name + else: + source = template.source.encode() + key = hashlib.md5(source).hexdigest() + return f"{key}@{block_name}" def _build_block_context(template, context): @@ -82,12 +108,12 @@ def _build_block_context(template, context): break -def _render_template_block(template, block_name, context): - """Renders a single block from a template.""" - return _render_template_block_nodelist(template.nodelist, block_name, context) +def _find_template_block(template, block_name, context): + """Finds a single block from a template.""" + return _find_template_block_nodelist(template.nodelist, block_name, context) -def _render_template_block_nodelist(nodelist, block_name, context): +def _find_template_block_nodelist(nodelist, block_name, context): """Recursively iterate over a node to find the wanted block.""" # Attempt to find the wanted block in the current template. @@ -99,7 +125,7 @@ def _render_template_block_nodelist(nodelist, block_name, context): # If the name matches, you're all set and we found the block! if node.name == block_name: - return node.render(context) + return node, context.render_context # If a node has children, recurse into them. Based on # django.template.base.Node.get_nodes_by_type. @@ -111,9 +137,7 @@ def _render_template_block_nodelist(nodelist, block_name, context): # Try to find the block recursively. try: - return _render_template_block_nodelist( - new_nodelist, block_name, context - ) + return _find_template_block_nodelist(new_nodelist, block_name, context) except BlockNotFound: continue diff --git a/tests/tests.py b/tests/tests.py index fc0c2c5..9c2d958 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,9 +1,11 @@ +from collections import namedtuple from unittest import skip -from django.template import Context +from django.template import Context, Template from django.test import RequestFactory, TestCase, modify_settings, override_settings from render_block import BlockNotFound, UnsupportedEngine, render_block_to_string +from render_block.django import _NODES_CACHE, django_render_block class TestDjango(TestCase): @@ -142,6 +144,29 @@ def test_request_context(self): self.assertEqual(result, "/dummy-url") + def test_node_cache(self): + """Test rendering from cache.""" + render_block_to_string("test1.html", "block1") + _NODES_CACHE["test1.html@fakeblock"] = _NODES_CACHE["test1.html@block1"] + result = render_block_to_string("test1.html", "fakeblock") + self.assertEqual(result, "block1 from test1") + + def test_node_cache_anonymous_template(self): + """ + Test rendering from cache for anonymous templates. + + Cache key must be created from template source instead of template name + to avoid clashes. + """ + T = namedtuple("T", ["template"]) + template_1 = Template("{% block b %}foo {{ foo }}{% endblock %}") + template_2 = Template("{% block b %}bar {{ foo }}{% endblock %}") + + result = django_render_block(T(template_1), "b", {"foo": "1"}) + self.assertEqual(result, "foo 1") + result = django_render_block(T(template_2), "b", {"foo": "2"}) + self.assertEqual(result, "bar 2") + @override_settings( TEMPLATES=[