fix: various fixes for inject/provide and html_attrs (#541)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2024-07-08 10:25:38 +02:00 committed by GitHub
parent 23d91218bd
commit 2684b41c07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 106 additions and 30 deletions

View file

@ -15,6 +15,10 @@ HTML_ATTRS_DEFAULTS_KEY = "defaults"
HTML_ATTRS_ATTRS_KEY = "attrs"
def _default(val: Any, default_val: Any) -> Any:
return val if val is not None else default_val
class HtmlAttrsNode(Node):
def __init__(
self,
@ -57,8 +61,9 @@ class HtmlAttrsNode(Node):
attrs_and_defaults_from_kwargs = process_aggregate_kwargs(attrs_and_defaults_from_kwargs)
# NOTE: We want to allow to use `html_attrs` even without `attrs` or `defaults` params
attrs = attrs_and_defaults_from_kwargs.get(HTML_ATTRS_ATTRS_KEY, {})
default_attrs = attrs_and_defaults_from_kwargs.get(HTML_ATTRS_DEFAULTS_KEY, {})
# Or when they are None
attrs = _default(attrs_and_defaults_from_kwargs.get(HTML_ATTRS_ATTRS_KEY, None), {})
default_attrs = _default(attrs_and_defaults_from_kwargs.get(HTML_ATTRS_DEFAULTS_KEY, None), {})
final_attrs = {**default_attrs, **attrs}
final_attrs = append_attributes(*final_attrs.items(), *append_attrs)

View file

@ -10,7 +10,7 @@ from django.template.context import Context
from django.template.exceptions import TemplateSyntaxError
from django.template.loader import get_template
from django.template.loader_tags import BLOCK_CONTEXT_KEY
from django.utils.html import escape
from django.utils.html import conditional_escape
from django.utils.safestring import SafeString, mark_safe
from django.views import View
@ -337,6 +337,7 @@ class Component(View, metaclass=ComponentMeta):
return comp._render(context, args, kwargs, slots, escape_slots_content)
# This is the internal entrypoint for the render function
def _render(
self,
context: Union[Dict[str, Any], Context] = None,
@ -344,6 +345,19 @@ class Component(View, metaclass=ComponentMeta):
kwargs: Optional[Dict[str, Any]] = None,
slots: Optional[Mapping[SlotName, SlotContent]] = None,
escape_slots_content: bool = True,
) -> str:
try:
return self._render_impl(context, args, kwargs, slots, escape_slots_content)
except Exception as err:
raise type(err)(f"An error occured while rendering component '{self.name}':\n{repr(err)}") from err
def _render_impl(
self,
context: Union[Dict[str, Any], Context] = None,
args: Optional[Union[List, Tuple]] = None,
kwargs: Optional[Dict[str, Any]] = None,
slots: Optional[Mapping[SlotName, SlotContent]] = None,
escape_slots_content: bool = True,
) -> str:
# Allow to provide no args/kwargs
args = args or []
@ -441,13 +455,13 @@ class Component(View, metaclass=ComponentMeta):
for slot_name, content in slots_data.items():
if isinstance(content, (str, SafeString)):
content_func = _nodelist_to_slot_render_func(
NodeList([TextNode(escape(content) if escape_content else content)])
NodeList([TextNode(conditional_escape(content) if escape_content else content)])
)
else:
def content_func(ctx: Context, kwargs: Dict[str, Any], slot_ref: SlotRef) -> SlotRenderedContent:
rendered = content(ctx, kwargs, slot_ref)
return escape(rendered) if escape_content else rendered
return conditional_escape(rendered) if escape_content else rendered
slot_fills[slot_name] = FillContent(
content_func=content_func,

View file

@ -100,7 +100,7 @@ def get_injected_context_var(
# Otherwise raise error
raise KeyError(
f"Component '{component_name}' tried to inject a variable '{key}' before it was provided."
f"To fix this, make sure that at least one ancestor of component '{component_name}' has"
f" To fix this, make sure that at least one ancestor of component '{component_name}' has"
f" the variable '{key}' in their 'provide' attribute."
)

View file

@ -11,7 +11,11 @@ from django.template.exceptions import TemplateSyntaxError
from django.utils.safestring import SafeString, mark_safe
from django_components.app_settings import ContextBehavior, app_settings
from django_components.context import _FILLED_SLOTS_CONTENT_CONTEXT_KEY, _ROOT_CTX_CONTEXT_KEY
from django_components.context import (
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
_INJECT_CONTEXT_KEY_PREFIX,
_ROOT_CTX_CONTEXT_KEY,
)
from django_components.expression import resolve_expression_as_identifier, safe_resolve_dict
from django_components.logger import trace_msg
from django_components.node import NodeTraverse, nodelist_has_content, walk_nodelist
@ -144,6 +148,16 @@ class SlotNode(Node):
extra_context: Dict[str, Any] = {}
# Irrespective of which context we use ("root" context or the one passed to this
# render function), pass down the keys used by inject/provide feature. This makes it
# possible to pass the provided values down the slots, e.g.:
# {% provide "abc" val=123 %}
# {% slot "content" %}{% endslot %}
# {% endprovide %}
for key, value in context.flatten().items():
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
extra_context[key] = value
# If slot fill is using `{% fill "myslot" default="abc" %}`, then set the "abc" to
# the context, so users can refer to the default slot from within the fill content.
slot_ref = SlotRef(self, context)

View file

@ -233,6 +233,7 @@ class HtmlAttrsTests(BaseTestCase):
return {"attrs": attrs}
template = Template(self.template_str)
with self.assertRaisesMessage(TemplateSyntaxError, "Received argument 'attrs' both as a regular input"):
template.render(Context({"class_var": "padding-top-8"}))
@ -250,6 +251,7 @@ class HtmlAttrsTests(BaseTestCase):
return {"attrs": attrs}
template = Template(self.template_str)
with self.assertRaisesMessage(
TemplateSyntaxError,
"Received argument 'defaults' both as a regular input",

View file

@ -481,6 +481,45 @@ class ProvideTemplateTagTest(BaseTestCase):
""",
)
@parametrize_context_behavior(["django", "isolated"])
def test_slot_in_provide(self):
@component.register("injectee")
class InjectComponent(component.Component):
template: types.django_html = """
<div> injected: {{ var|safe }} </div>
"""
def get_context_data(self):
var = self.inject("my_provide", "default")
return {"var": var}
@component.register("parent")
class ParentComponent(component.Component):
template: types.django_html = """
{% load component_tags %}
{% provide "my_provide" key="hi" another=123 %}
{% slot "content" default %}{% endslot %}
{% endprovide %}
"""
template_str: types.django_html = """
{% load component_tags %}
{% component "parent" %}
{% component "injectee" %}{% endcomponent %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
self.assertHTMLEqual(
rendered,
"""
<div>
injected: DepInject(key='hi', another=123)
</div>
""",
)
class InjectTest(BaseTestCase):
@parametrize_context_behavior(["django", "isolated"])
@ -530,6 +569,7 @@ class InjectTest(BaseTestCase):
{% endcomponent %}
"""
template = Template(template_str)
with self.assertRaises(KeyError):
template.render(Context({}))
@ -581,6 +621,7 @@ class InjectTest(BaseTestCase):
{% endcomponent %}
"""
template = Template(template_str)
with self.assertRaises(KeyError):
template.render(Context({}))

View file

@ -1,4 +1,3 @@
import textwrap
from typing import Any, Dict, List, Optional
from django.template import Context, Template, TemplateSyntaxError
@ -210,6 +209,7 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
{% endcomponent %}
"""
template = Template(template_str)
with self.assertRaises(TemplateSyntaxError):
template.render(Context({}))
@ -284,6 +284,7 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
{% endcomponent %}
"""
template = Template(template_str)
with self.assertRaises(TemplateSyntaxError):
template.render(Context({}))
@ -381,6 +382,7 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
{% endcomponent %}
"""
template = Template(template_str)
with self.assertRaises(TemplateSyntaxError):
template.render(Context({}))
@ -398,6 +400,7 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
return Template(template_str)
c = BadComponent("name")
with self.assertRaises(TemplateSyntaxError):
c.render(Context({}))
@ -417,20 +420,16 @@ class ComponentSlottedTemplateTagTest(BaseTestCase):
{% endcomponent %}
"""
template = Template(template_str)
with self.assertRaises(TemplateSyntaxError):
try:
template.render(Context({}))
except TemplateSyntaxError as e:
self.assertEqual(
textwrap.dedent(
"""\
Component 'test1' passed fill that refers to undefined slot: 'haeder'.
Unfilled slot names are: ['footer', 'header'].
Did you mean 'header'?"""
),
str(e),
)
raise e
with self.assertRaisesMessage(
TemplateSyntaxError,
(
"Component 'test1' passed fill that refers to undefined slot: 'haeder'.\\n"
"Unfilled slot names are: ['footer', 'header'].\\n"
"Did you mean 'header'?"
),
):
template.render(Context({}))
# NOTE: This is relevant only for the "isolated" mode
@parametrize_context_behavior(["isolated"])
@ -1144,15 +1143,16 @@ class SlotFillTemplateSyntaxErrorTests(BaseTestCase):
{% include 'slotted_template.html' with context=None only %}
"""
template_str: types.django_html = """
{% load component_tags %}
{% component "broken_component" %}
{% fill "header" %}Custom header {% endfill %}
{% fill "main" %}Custom main{% endfill %}
{% fill "footer" %}Custom footer{% endfill %}
{% endcomponent %}
"""
with self.assertRaises(KeyError):
template_str: types.django_html = """
{% load component_tags %}
{% component "broken_component" %}
{% fill "header" %}Custom header {% endfill %}
{% fill "main" %}Custom main{% endfill %}
{% fill "footer" %}Custom footer{% endfill %}
{% endcomponent %}
"""
Template(template_str).render(Context({}))
@parametrize_context_behavior(["django", "isolated"])