Rework of context handling (#18)

Co-authored-by: rbeard0330 <@dul2k3BKW6m>
This commit is contained in:
rbeard0330 2020-12-28 04:40:35 -05:00 committed by GitHub
parent 88fe2fc3b7
commit 93b8a7404a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 305 additions and 59 deletions

View file

@ -1,5 +1,4 @@
from django.forms.widgets import MediaDefiningClass
from django.template import Context
from django.template.base import NodeList, TextNode
from django.template.loader import get_template
from django.utils.safestring import mark_safe
@ -8,12 +7,6 @@ from six import with_metaclass
# Allow "component.AlreadyRegistered" instead of having to import these everywhere
from django_components.component_registry import AlreadyRegistered, ComponentRegistry, NotRegistered # noqa
# Python 2 compatibility
try:
from inspect import getfullargspec
except ImportError:
from inspect import getargspec as getfullargspec
# Django < 2.1 compatibility
try:
from django.template.base import TokenType
@ -49,52 +42,40 @@ class Component(with_metaclass(MediaDefiningClass)):
return mark_safe("\n".join(self.media.render_js()))
def slots_in_template(self, template):
nodelist = NodeList()
for node in template.template.nodelist:
if (
node.token.token_type == TokenType.BLOCK
and node.token.split_contents()[0] == "slot"
):
nodelist.append(node)
return NodeList(node for node in template.template.nodelist if is_slot_node(node))
return nodelist
def render(self, slots_filled=None, *args, **kwargs):
def render(self, context, slots_filled=None):
slots_filled = slots_filled or []
context_args_variables = getfullargspec(self.context).args[1:]
context_args = {
key: kwargs[key] for key in context_args_variables if key in kwargs
}
context = self.context(**context_args)
template = get_template(self.template(context))
slots_in_template = self.slots_in_template(template)
if slots_in_template:
valid_slot_names = set([slot.name for slot in slots_in_template])
# If there are no slots, then we can simply render the template
if not slots_in_template:
return template.template.render(context)
# Otherwise, we need to assemble and render a nodelist containing the nodes from the template, slots that were
# provided when the component was called (previously rendered by the component's render method) and the
# unrendered default slots
nodelist = NodeList()
for node in template.template.nodelist:
if (
node.token.token_type == TokenType.BLOCK
and node.token.split_contents()[0] == "slot"
):
if node.name in valid_slot_names and node.name in slots_filled:
if is_slot_node(node):
if node.name in slots_filled:
nodelist.append(TextNode(slots_filled[node.name]))
else:
for node in node.nodelist:
nodelist.append(node)
nodelist.extend(node.nodelist)
else:
nodelist.append(node)
render_context = Context(context)
with render_context.bind_template(template.template):
return nodelist.render(render_context)
return template.render(context)
return nodelist.render(context)
class Media:
css = {}
js = []
def is_slot_node(node):
return node.token.token_type == TokenType.BLOCK and node.token.split_contents()[0] == "slot"
# This variable represents the global component registry
registry = ComponentRegistry()

View file

@ -88,7 +88,8 @@ def component_js_dependencies_tag():
def component_tag(name, *args, **kwargs):
component_class = registry.get(name)
component = component_class()
return component.render(*args, **kwargs)
context = template.Context(component.context(*args, **kwargs))
return component.render(context)
class SlotNode(Node):
@ -129,27 +130,30 @@ def do_slot(parser, token, component=None):
class ComponentNode(Node):
def __init__(self, component, extra_context, slots):
extra_context = extra_context or {}
self.component, self.extra_context, self.slots = component, extra_context, slots
def __init__(self, component, context_kwargs, slots):
self.context_kwargs = context_kwargs or {}
self.component, self.slots = component, slots
def __repr__(self):
return "<Component Node: %s. Contents: %r>" % (self.component, self.slots)
def render(self, context):
extra_context = {
# Resolve FilterExpressions and Variables that were passed as args to the component, then call component's
# context method to get values to insert into the context
resolved_context_kwargs = {
key: context_item.resolve(context) if hasattr(context_item, 'resolve') else context_item
for key, context_item in self.extra_context.items()
for key, context_item in self.context_kwargs.items()
}
context.update(extra_context)
extra_context = self.component.context(**resolved_context_kwargs)
with context.update(extra_context):
self.slots.render(context)
if COMPONENT_CONTEXT_KEY in context.render_context:
slots_filled = context.render_context[COMPONENT_CONTEXT_KEY][self.component]
return self.component.render(slots_filled=slots_filled, **context.flatten())
else:
slots_filled = []
return self.component.render(**extra_context)
return self.component.render(context, slots_filled=slots_filled)
@register.tag("component_block")
@ -187,9 +191,9 @@ def do_component(parser, token):
component_class = registry.get(component_name)
component = component_class()
extra_context = {}
context_kwargs = {}
if len(bits) > 2:
extra_context = component.context(**token_kwargs(bits[2:], parser))
context_kwargs = token_kwargs(bits[2:], parser)
slots_filled = NodeList()
tag_name = bits[0]
@ -205,4 +209,4 @@ def do_component(parser, token):
elif tag_name == "endcomponent_block":
break
return ComponentNode(component, extra_context, slots_filled)
return ComponentNode(component, context_kwargs, slots_filled)

View file

View file

@ -0,0 +1,2 @@
{% load component_tags %}<p class="incrementer">value={{ value }};calls={{ calls }}</p>
{% slot 'content' %}{% endslot %}

View file

@ -0,0 +1,12 @@
{% load component_tags %}
<div>
<h1>Parent content</h1>
{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %}
</div>
<div>
{% slot 'content' %}
<h2>Slot content</h2>
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
{% endslot %}
</div>

View file

@ -0,0 +1,12 @@
{% load component_tags %}
<div>
<h1>Parent content</h1>
{% component name="variable_display" shadowing_variable=inner_parent_value new_variable='unique_val' %}
</div>
<div>
{% slot 'content' %}
<h2>Slot content</h2>
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable=inner_parent_value %}
{% endslot %}
</div>

View file

@ -0,0 +1,4 @@
{% load component_tags %}
<h1>Shadowing variable = {{ shadowing_variable }}</h1>
<h1>Uniquely named variable = {{ unique_variable }}</h1>

View file

@ -1,5 +1,7 @@
from textwrap import dedent
from django.template import Context
from django_components import component
from .django_test_setup import * # NOQA
@ -29,13 +31,14 @@ class ComponentRegistryTest(SimpleTestCase):
js = ["script.js"]
comp = SimpleComponent()
context = Context(comp.context(variable="test"))
self.assertHTMLEqual(comp.render_dependencies(), dedent("""
<link href="style.css" type="text/css" media="all" rel="stylesheet">
<script src="script.js"></script>
""").strip())
self.assertHTMLEqual(comp.render(variable="test"), dedent("""
self.assertHTMLEqual(comp.render(context), dedent("""
Variable: <strong>test</strong>
""").lstrip())
@ -66,8 +69,9 @@ class ComponentRegistryTest(SimpleTestCase):
return "filtered_template.html"
comp = FilteredComponent()
context = Context(comp.context(var1="test1", var2="test2"))
self.assertHTMLEqual(comp.render(var1="test1", var2="test2"), dedent("""
self.assertHTMLEqual(comp.render(context), dedent("""
Var1: <strong>test1</strong>
Var2 (uppercased): <strong>TEST2</strong>
""").lstrip())

227
tests/test_context.py Normal file
View file

@ -0,0 +1,227 @@
from django.template import Context, Template
from django_components import component
from .django_test_setup import * # NOQA
from .testutils import Django111CompatibleSimpleTestCase as SimpleTestCase
class ParentComponent(component.Component):
def context(self):
return {
"shadowing_variable": 'NOT SHADOWED'
}
def template(self, context):
return "parent_template.html"
class ParentComponentWithArgs(component.Component):
def context(self, parent_value):
return {
"inner_parent_value": parent_value
}
def template(self, context):
return "parent_with_args_template.html"
class VariableDisplay(component.Component):
def context(self, shadowing_variable, new_variable):
return {
"shadowing_variable": shadowing_variable,
"unique_variable": new_variable
}
def template(self, context):
return "variable_display.html"
class IncrementerComponent(component.Component):
def context(self, value=0):
value = int(value)
if hasattr(self, 'call_count'):
self.call_count += 1
else:
self.call_count = 1
return {
"value": value + 1,
"calls": self.call_count
}
def template(self, context):
return "incrementer.html"
component.registry.register(name='parent_component', component=ParentComponent)
component.registry.register(name='parent_with_args', component=ParentComponentWithArgs)
component.registry.register(name='variable_display', component=VariableDisplay)
component.registry.register(name='incrementer', component=IncrementerComponent)
class ContextTests(SimpleTestCase):
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component 'parent_component' %}")
rendered = template.render(Context())
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
self.assertIn('<h1>Shadowing variable = slot_default_override</h1>', rendered, rendered)
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component name='parent_component' %}")
rendered = template.render(Context())
self.assertIn('<h1>Uniquely named variable = unique_val</h1>', rendered, rendered)
self.assertIn('<h1>Uniquely named variable = slot_default_unique</h1>', rendered, rendered)
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_block_tag(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}{% endcomponent_block %}")
rendered = template.render(Context())
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
self.assertIn('<h1>Shadowing variable = slot_default_override</h1>', rendered, rendered)
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_block_tag(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}{% endcomponent_block %}")
rendered = template.render(Context())
self.assertIn('<h1>Uniquely named variable = unique_val</h1>', rendered, rendered)
self.assertIn('<h1>Uniquely named variable = slot_default_unique</h1>', rendered, rendered)
def test_nested_component_context_shadows_parent_with_filled_slots(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}"
"{% slot 'content' %}{% component name='variable_display' "
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endslot %}"
"{% endcomponent_block %}")
rendered = template.render(Context())
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
self.assertIn('<h1>Shadowing variable = shadow_from_slot</h1>', rendered, rendered)
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
def test_nested_component_instances_have_unique_context_with_filled_slots(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}"
"{% slot 'content' %}{% component name='variable_display' "
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endslot %}"
"{% endcomponent_block %}")
rendered = template.render(Context())
self.assertIn('<h1>Uniquely named variable = unique_val</h1>', rendered, rendered)
self.assertIn('<h1>Uniquely named variable = unique_from_slot</h1>', rendered, rendered)
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component name='parent_component' %}")
rendered = template.render(Context({'shadowing_variable': 'NOT SHADOWED'}))
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
self.assertIn('<h1>Shadowing variable = slot_default_override</h1>', rendered, rendered)
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_block_tag(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}{% endcomponent_block %}")
rendered = template.render(Context({'shadowing_variable': 'NOT SHADOWED'}))
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
self.assertIn('<h1>Shadowing variable = slot_default_override</h1>', rendered, rendered)
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
def test_nested_component_context_shadows_outer_context_with_filled_slots(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}"
"{% slot 'content' %}{% component name='variable_display' "
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endslot %}"
"{% endcomponent_block %}")
rendered = template.render(Context({'shadowing_variable': 'NOT SHADOWED'}))
self.assertIn('<h1>Shadowing variable = override</h1>', rendered, rendered)
self.assertIn('<h1>Shadowing variable = shadow_from_slot</h1>', rendered, rendered)
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
class ParentArgsTests(SimpleTestCase):
def test_parent_args_can_be_drawn_from_context(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_with_args' parent_value=parent_value %}"
"{% endcomponent_block %}")
rendered = template.render(Context({'parent_value': 'passed_in'}))
self.assertIn('<h1>Shadowing variable = passed_in</h1>', rendered, rendered)
self.assertIn('<h1>Uniquely named variable = passed_in</h1>', rendered, rendered)
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
def test_parent_args_available_outside_slots(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_with_args' parent_value='passed_in' %}{%endcomponent_block %}")
rendered = template.render(Context())
self.assertIn('<h1>Shadowing variable = passed_in</h1>', rendered, rendered)
self.assertIn('<h1>Uniquely named variable = passed_in</h1>', rendered, rendered)
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
def test_parent_args_available_in_slots(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_with_args' parent_value='passed_in' %}"
"{% slot 'content' %}{% component name='variable_display' "
"shadowing_variable='value_from_slot' new_variable=inner_parent_value %}{% endslot %}"
"{%endcomponent_block %}")
rendered = template.render(Context())
self.assertIn('<h1>Shadowing variable = value_from_slot</h1>', rendered, rendered)
self.assertIn('<h1>Uniquely named variable = passed_in</h1>', rendered, rendered)
self.assertNotIn('<h1>Shadowing variable = NOT SHADOWED</h1>', rendered, rendered)
class ContextCalledOnceTests(SimpleTestCase):
def test_one_context_call_with_simple_component(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component name='incrementer' %}")
rendered = template.render(Context()).strip()
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
def test_one_context_call_with_simple_component_and_arg(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component name='incrementer' value='2' %}")
rendered = template.render(Context()).strip()
self.assertEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered)
def test_one_context_call_with_component_block(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'incrementer' %}{% endcomponent_block %}")
rendered = template.render(Context()).strip()
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
def test_one_context_call_with_component_block_and_arg(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'incrementer' value='3' %}{% endcomponent_block %}")
rendered = template.render(Context()).strip()
self.assertEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered)
def test_one_context_call_with_slot(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'incrementer' %}{% slot 'content' %}"
"<p>slot</p>{% endslot %}{% endcomponent_block %}")
rendered = template.render(Context()).strip()
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>\n<p>slot</p>', rendered)
def test_one_context_call_with_slot_and_arg(self):
template = Template("{% load component_tags %}{% component_dependencies %}"
"{% component_block 'incrementer' value='3' %}{% slot 'content' %}"
"<p>slot</p>{% endslot %}{% endcomponent_block %}")
rendered = template.render(Context()).strip()
self.assertEqual(rendered, '<p class="incrementer">value=4;calls=1</p>\n<p>slot</p>', rendered)