django-components/tests/test_node.py
Juro Oravec 7dfcb447c4
feat: add decorator for writing component tests (#1008)
* feat: add decorator for writing component tests

* refactor: udpate changelog + update deps pins

* refactor: fix deps

* refactor: make cached_ref into generic and fix linter errors

* refactor: fix coverage testing

* refactor: use global var instead of env var for is_testing state
2025-03-02 19:46:12 +01:00

1106 lines
34 KiB
Python

import inspect
import re
import pytest
from django.template import Context, Template
from django.template.exceptions import TemplateSyntaxError
from django_components import types
from django_components.node import BaseNode, template_tag
from django_components.templatetags import component_tags
from django_components.testing import djc_test
from .testutils import setup_test_config
setup_test_config({"autodiscover": False})
@djc_test
class TestNode:
def test_node_class_requires_tag(self):
with pytest.raises(ValueError):
class CaptureNode(BaseNode):
pass
# Test that the template tag can be used within the template under the registered tag
def test_node_class_tags(self):
class TestNode(BaseNode):
tag = "mytag"
end_tag = "endmytag"
def render(self, context: Context, name: str, **kwargs) -> str:
return f"Hello, {name}!"
TestNode.register(component_tags.register)
# Works with end tag and self-closing
template_str: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
{% endmytag %}
Shorthand: {% mytag 'Mary' / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert rendered.strip() == "Hello, John!\n Shorthand: Hello, Mary!"
# But raises if missing end tag
template_str2: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
"""
with pytest.raises(TemplateSyntaxError, match=re.escape("Unclosed tag on line 3: 'mytag'")):
Template(template_str2)
TestNode.unregister(component_tags.register)
def test_node_class_no_end_tag(self):
class TestNode(BaseNode):
tag = "mytag"
def render(self, context: Context, name: str, **kwargs) -> str:
return f"Hello, {name}!"
TestNode.register(component_tags.register)
# Raises with end tag or self-closing
template_str: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
{% endmytag %}
Shorthand: {% mytag 'Mary' / %}
"""
with pytest.raises(TemplateSyntaxError, match=re.escape("Invalid block tag on line 4: 'endmytag'")):
Template(template_str)
# Works when missing end tag
template_str2: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
"""
template2 = Template(template_str2)
rendered2 = template2.render(Context({}))
assert rendered2.strip() == "Hello, John!"
TestNode.unregister(component_tags.register)
def test_node_class_flags(self):
captured = None
class TestNode(BaseNode):
tag = "mytag"
end_tag = "endmytag"
allowed_flags = ["required", "default"]
def render(self, context: Context, name: str, **kwargs) -> str:
nonlocal captured
captured = self.allowed_flags, self.flags, self.active_flags
return f"Hello, {name}!"
TestNode.register(component_tags.register)
template_str = """
{% load component_tags %}
{% mytag 'John' required / %}
"""
template = Template(template_str)
template.render(Context({}))
allowed_flags, flags, active_flags = captured # type: ignore
assert allowed_flags == ["required", "default"]
assert flags == {"required": True, "default": False}
assert active_flags == ["required"]
TestNode.unregister(component_tags.register)
def test_node_render(self):
# Check that the render function is called with the context
captured = None
class TestNode(BaseNode):
tag = "mytag"
def render(self, context: Context) -> str:
nonlocal captured
captured = context.flatten()
return f"Hello, {context['name']}!"
TestNode.register(component_tags.register)
template_str = """
{% load component_tags %}
{% mytag / %}
"""
template = Template(template_str)
rendered = template.render(Context({"name": "John"}))
assert captured == {"False": False, "None": None, "True": True, "name": "John"}
assert rendered.strip() == "Hello, John!"
TestNode.unregister(component_tags.register)
def test_node_render_raises_if_no_context_arg(self):
with pytest.raises(
TypeError,
match=re.escape("`render()` method of TestNode must have at least two parameters"),
):
class TestNode(BaseNode):
tag = "mytag"
def render(self) -> str: # type: ignore
return ""
def test_node_render_accepted_params_set_by_render_signature(self):
captured = None
class TestNode1(BaseNode):
tag = "mytag"
allowed_flags = ["required", "default"]
def render(self, context: Context, name: str, count: int = 1, *, msg: str, mode: str = "default") -> str:
nonlocal captured
captured = name, count, msg, mode
return ""
TestNode1.register(component_tags.register)
# Set only required params
template1 = Template(
"""
{% load component_tags %}
{% mytag 'John' msg='Hello' required %}
"""
)
template1.render(Context({}))
assert captured == ("John", 1, "Hello", "default")
# Set all params
template2 = Template(
"""
{% load component_tags %}
{% mytag 'John2' count=2 msg='Hello' mode='custom' required %}
"""
)
template2.render(Context({}))
assert captured == ("John2", 2, "Hello", "custom")
# Set no params
template3 = Template(
"""
{% load component_tags %}
{% mytag %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
):
template3.render(Context({}))
# Omit required arg
template4 = Template(
"""
{% load component_tags %}
{% mytag msg='Hello' %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
):
template4.render(Context({}))
# Omit required kwarg
template5 = Template(
"""
{% load component_tags %}
{% mytag name='John' %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'msg'"),
):
template5.render(Context({}))
# Extra args
template6 = Template(
"""
{% load component_tags %}
{% mytag 123 count=1 name='John' %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got multiple values for argument 'name'"),
):
template6.render(Context({}))
# Extra args after kwargs
template6 = Template(
"""
{% load component_tags %}
{% mytag count=1 name='John' 123 %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("positional argument follows keyword argument"),
):
template6.render(Context({}))
# Extra kwargs
template7 = Template(
"""
{% load component_tags %}
{% mytag 'John' msg='Hello' mode='custom' var=123 %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'var'"),
):
template7.render(Context({}))
# Extra kwargs - non-identifier or kwargs
template8 = Template(
"""
{% load component_tags %}
{% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'data-id'"),
):
template8.render(Context({}))
# Extra arg after special kwargs
template9 = Template(
"""
{% load component_tags %}
{% mytag data-id=123 'John' msg='Hello' %}
"""
)
with pytest.raises(
SyntaxError,
match=re.escape("positional argument follows keyword argument"),
):
template9.render(Context({}))
TestNode1.unregister(component_tags.register)
def test_node_render_extra_args_and_kwargs(self):
captured = None
class TestNode1(BaseNode):
tag = "mytag"
allowed_flags = ["required", "default"]
def render(self, context: Context, name: str, *args, msg: str, **kwargs) -> str:
nonlocal captured
captured = name, args, msg, kwargs
return ""
TestNode1.register(component_tags.register)
template1 = Template(
"""
{% load component_tags %}
{% mytag 'John'
123 456 789 msg='Hello' a=1 b=2 c=3 required
data-id=123 class="pa-4" @click.once="myVar"
%}
"""
)
template1.render(Context({}))
assert captured == (
"John",
(123, 456, 789),
"Hello",
{"a": 1, "b": 2, "c": 3, "data-id": 123, "class": "pa-4", "@click.once": "myVar"},
)
TestNode1.unregister(component_tags.register)
def test_node_render_kwargs_only(self):
captured = None
class TestNode(BaseNode):
tag = "mytag"
def render(self, context: Context, **kwargs) -> str:
nonlocal captured
captured = kwargs
return ""
TestNode.register(component_tags.register)
# Test with various kwargs including non-identifier keys
template = Template(
"""
{% load component_tags %}
{% mytag
name='John'
age=25
data-id=123
class="header"
@click="handleClick"
v-if="isVisible"
%}
"""
)
template.render(Context({}))
# All kwargs should be accepted since the function accepts **kwargs
assert captured == {
"name": "John",
"age": 25,
"data-id": 123,
"class": "header",
"@click": "handleClick",
"v-if": "isVisible",
}
# Test with positional args (should fail since function only accepts kwargs)
template2 = Template(
"""
{% load component_tags %}
{% mytag "John" name="Mary" %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': takes 0 positional arguments but 1 was given"),
):
template2.render(Context({}))
TestNode.unregister(component_tags.register)
@djc_test
class TestDecorator:
def test_decorator_requires_tag(self):
with pytest.raises(
TypeError,
match=re.escape("template_tag() missing 1 required positional argument: 'tag'"),
):
@template_tag(component_tags.register) # type: ignore
def mytag(node: BaseNode, context: Context) -> str:
return ""
# Test that the template tag can be used within the template under the registered tag
def test_decorator_tags(self):
@template_tag(component_tags.register, tag="mytag", end_tag="endmytag")
def render(node: BaseNode, context: Context, name: str, **kwargs) -> str:
return f"Hello, {name}!"
# Works with end tag and self-closing
template_str: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
{% endmytag %}
Shorthand: {% mytag 'Mary' / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert rendered.strip() == "Hello, John!\n Shorthand: Hello, Mary!"
# But raises if missing end tag
template_str2: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
"""
with pytest.raises(
TemplateSyntaxError,
match=re.escape("Unclosed tag on line 3: 'mytag'"),
):
Template(template_str2)
render._node.unregister(component_tags.register) # type: ignore[attr-defined]
def test_decorator_no_end_tag(self):
@template_tag(component_tags.register, tag="mytag") # type: ignore
def render(node: BaseNode, context: Context, name: str, **kwargs) -> str:
return f"Hello, {name}!"
# Raises with end tag or self-closing
template_str: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
{% endmytag %}
Shorthand: {% mytag 'Mary' / %}
"""
with pytest.raises(
TemplateSyntaxError,
match=re.escape("Invalid block tag on line 4: 'endmytag'"),
):
Template(template_str)
# Works when missing end tag
template_str2: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
"""
template2 = Template(template_str2)
rendered2 = template2.render(Context({}))
assert rendered2.strip() == "Hello, John!"
render._node.unregister(component_tags.register) # type: ignore[attr-defined]
def test_decorator_flags(self):
@template_tag(component_tags.register, tag="mytag", end_tag="endmytag", allowed_flags=["required", "default"])
def render(node: BaseNode, context: Context, name: str, **kwargs) -> str:
return ""
render._node.unregister(component_tags.register) # type: ignore[attr-defined]
def test_decorator_render(self):
# Check that the render function is called with the context
captured = None
@template_tag(component_tags.register, tag="mytag") # type: ignore
def render(node: BaseNode, context: Context) -> str:
nonlocal captured
captured = context.flatten()
return f"Hello, {context['name']}!"
template_str = """
{% load component_tags %}
{% mytag / %}
"""
template = Template(template_str)
rendered = template.render(Context({"name": "John"}))
assert captured == {"False": False, "None": None, "True": True, "name": "John"}
assert rendered.strip() == "Hello, John!"
render._node.unregister(component_tags.register) # type: ignore[attr-defined]
def test_decorator_render_raises_if_no_context_arg(self):
with pytest.raises(
TypeError,
match=re.escape("Failed to create node class in 'template_tag()' for 'render'"),
):
@template_tag(component_tags.register, tag="mytag") # type: ignore
def render(node: BaseNode) -> str: # type: ignore
return ""
def test_decorator_render_accepted_params_set_by_render_signature(self):
captured = None
@template_tag(component_tags.register, tag="mytag", allowed_flags=["required", "default"]) # type: ignore
def render(
node: BaseNode, context: Context, name: str, count: int = 1, *, msg: str, mode: str = "default"
) -> str:
nonlocal captured
captured = name, count, msg, mode
return ""
# Set only required params
template1 = Template(
"""
{% load component_tags %}
{% mytag 'John' msg='Hello' required %}
"""
)
template1.render(Context({}))
assert captured == ("John", 1, "Hello", "default")
# Set all params
template2 = Template(
"""
{% load component_tags %}
{% mytag 'John2' count=2 msg='Hello' mode='custom' required %}
"""
)
template2.render(Context({}))
assert captured == ("John2", 2, "Hello", "custom")
# Set no params
template3 = Template(
"""
{% load component_tags %}
{% mytag %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
):
template3.render(Context({}))
# Omit required arg
template4 = Template(
"""
{% load component_tags %}
{% mytag msg='Hello' %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
):
template4.render(Context({}))
# Omit required kwarg
template5 = Template(
"""
{% load component_tags %}
{% mytag name='John' %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'msg'"),
):
template5.render(Context({}))
# Extra args
template6 = Template(
"""
{% load component_tags %}
{% mytag 123 count=1 name='John' %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got multiple values for argument 'name'"),
):
template6.render(Context({}))
# Extra args after kwargs
template6 = Template(
"""
{% load component_tags %}
{% mytag count=1 name='John' 123 %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("positional argument follows keyword argument"),
):
template6.render(Context({}))
# Extra kwargs
template7 = Template(
"""
{% load component_tags %}
{% mytag 'John' msg='Hello' mode='custom' var=123 %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'var'"),
):
template7.render(Context({}))
# Extra kwargs - non-identifier or kwargs
template8 = Template(
"""
{% load component_tags %}
{% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'data-id'"),
):
template8.render(Context({}))
# Extra arg after special kwargs
template9 = Template(
"""
{% load component_tags %}
{% mytag data-id=123 'John' msg='Hello' %}
"""
)
with pytest.raises(
SyntaxError,
match=re.escape("positional argument follows keyword argument"),
):
template9.render(Context({}))
render._node.unregister(component_tags.register) # type: ignore[attr-defined]
def test_decorator_render_extra_args_and_kwargs(self):
captured = None
@template_tag(component_tags.register, tag="mytag", allowed_flags=["required", "default"]) # type: ignore
def render(node: BaseNode, context: Context, name: str, *args, msg: str, **kwargs) -> str:
nonlocal captured
captured = name, args, msg, kwargs
return ""
template1 = Template(
"""
{% load component_tags %}
{% mytag 'John'
123 456 789 msg='Hello' a=1 b=2 c=3 required
data-id=123 class="pa-4" @click.once="myVar"
%}
"""
)
template1.render(Context({}))
assert captured == (
"John",
(123, 456, 789),
"Hello",
{"a": 1, "b": 2, "c": 3, "data-id": 123, "class": "pa-4", "@click.once": "myVar"},
)
render._node.unregister(component_tags.register) # type: ignore[attr-defined]
def test_decorator_render_kwargs_only(self):
captured = None
@template_tag(component_tags.register, tag="mytag") # type: ignore
def render(node: BaseNode, context: Context, **kwargs) -> str:
nonlocal captured
captured = kwargs
return ""
# Test with various kwargs including non-identifier keys
template = Template(
"""
{% load component_tags %}
{% mytag
name='John'
age=25
data-id=123
class="header"
@click="handleClick"
v-if="isVisible"
%}
"""
)
template.render(Context({}))
# All kwargs should be accepted since the function accepts **kwargs
assert captured == {
"name": "John",
"age": 25,
"data-id": 123,
"class": "header",
"@click": "handleClick",
"v-if": "isVisible",
}
# Test with positional args (should fail since function only accepts kwargs)
template2 = Template(
"""
{% load component_tags %}
{% mytag "John" name="Mary" %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': takes 0 positional arguments but 1 was given"),
):
template2.render(Context({}))
render._node.unregister(component_tags.register) # type: ignore[attr-defined]
def force_signature_validation(fn):
"""
Creates a proxy around a function that makes __code__ inaccessible,
forcing the use of signature-based validation.
"""
class SignatureOnlyFunction:
def __init__(self, fn):
self.__wrapped__ = fn
self.__signature__ = inspect.signature(fn)
def __call__(self, *args, **kwargs):
return self.__wrapped__(*args, **kwargs)
def __getattr__(self, name):
# Return None for __code__ to force signature-based validation
if name == "__code__":
return None
# For all other attributes, delegate to the wrapped function
return getattr(self.__wrapped__, name)
return SignatureOnlyFunction(fn)
@djc_test
class TestSignatureBasedValidation:
# Test that the template tag can be used within the template under the registered tag
def test_node_class_tags(self):
class TestNode(BaseNode):
tag = "mytag"
end_tag = "endmytag"
@force_signature_validation
def render(self, context: Context, name: str, **kwargs) -> str:
return f"Hello, {name}!"
TestNode.register(component_tags.register)
# Works with end tag and self-closing
template_str: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
{% endmytag %}
Shorthand: {% mytag 'Mary' / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert rendered.strip() == "Hello, John!\n Shorthand: Hello, Mary!"
# But raises if missing end tag
template_str2: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
"""
with pytest.raises(
TemplateSyntaxError,
match=re.escape("Unclosed tag on line 3: 'mytag'"),
):
Template(template_str2)
TestNode.unregister(component_tags.register)
def test_node_class_no_end_tag(self):
class TestNode(BaseNode):
tag = "mytag"
@force_signature_validation
def render(self, context: Context, name: str, **kwargs) -> str:
return f"Hello, {name}!"
TestNode.register(component_tags.register)
# Raises with end tag or self-closing
template_str: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
{% endmytag %}
Shorthand: {% mytag 'Mary' / %}
"""
with pytest.raises(
TemplateSyntaxError,
match=re.escape("Invalid block tag on line 4: 'endmytag'"),
):
Template(template_str)
# Works when missing end tag
template_str2: types.django_html = """
{% load component_tags %}
{% mytag 'John' %}
"""
template2 = Template(template_str2)
rendered2 = template2.render(Context({}))
assert rendered2.strip() == "Hello, John!"
TestNode.unregister(component_tags.register)
def test_node_class_flags(self):
captured = None
class TestNode(BaseNode):
tag = "mytag"
end_tag = "endmytag"
allowed_flags = ["required", "default"]
@force_signature_validation
def render(self, context: Context, name: str, **kwargs) -> str:
nonlocal captured
captured = self.allowed_flags, self.flags, self.active_flags
return f"Hello, {name}!"
TestNode.register(component_tags.register)
template_str = """
{% load component_tags %}
{% mytag 'John' required / %}
"""
template = Template(template_str)
template.render(Context({}))
allowed_flags, flags, active_flags = captured # type: ignore
assert allowed_flags == ["required", "default"]
assert flags == {"required": True, "default": False}
assert active_flags == ["required"]
TestNode.unregister(component_tags.register)
def test_node_render(self):
# Check that the render function is called with the context
captured = None
class TestNode(BaseNode):
tag = "mytag"
@force_signature_validation
def render(self, context: Context) -> str:
nonlocal captured
captured = context.flatten()
return f"Hello, {context['name']}!"
TestNode.register(component_tags.register)
template_str = """
{% load component_tags %}
{% mytag / %}
"""
template = Template(template_str)
rendered = template.render(Context({"name": "John"}))
assert captured == {"False": False, "None": None, "True": True, "name": "John"}
assert rendered.strip() == "Hello, John!"
TestNode.unregister(component_tags.register)
def test_node_render_raises_if_no_context_arg(self):
with pytest.raises(
TypeError,
match=re.escape("`render()` method of TestNode must have at least two parameters"),
):
class TestNode(BaseNode):
tag = "mytag"
def render(self) -> str: # type: ignore
return ""
def test_node_render_accepted_params_set_by_render_signature(self):
captured = None
class TestNode1(BaseNode):
tag = "mytag"
allowed_flags = ["required", "default"]
@force_signature_validation
def render(self, context: Context, name: str, count: int = 1, *, msg: str, mode: str = "default") -> str:
nonlocal captured
captured = name, count, msg, mode
return ""
TestNode1.register(component_tags.register)
# Set only required params
template1 = Template(
"""
{% load component_tags %}
{% mytag 'John' msg='Hello' required %}
"""
)
template1.render(Context({}))
assert captured == ("John", 1, "Hello", "default")
# Set all params
template2 = Template(
"""
{% load component_tags %}
{% mytag 'John2' count=2 msg='Hello' mode='custom' required %}
"""
)
template2.render(Context({}))
assert captured == ("John2", 2, "Hello", "custom")
# Set no params
template3 = Template(
"""
{% load component_tags %}
{% mytag %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
):
template3.render(Context({}))
# Omit required arg
template4 = Template(
"""
{% load component_tags %}
{% mytag msg='Hello' %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'name'"),
):
template4.render(Context({}))
# Omit required kwarg
template5 = Template(
"""
{% load component_tags %}
{% mytag name='John' %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': missing a required argument: 'msg'"),
):
template5.render(Context({}))
# Extra args
template6 = Template(
"""
{% load component_tags %}
{% mytag 123 count=1 name='John' %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got multiple values for argument 'name'"),
):
template6.render(Context({}))
# Extra args after kwargs
template6 = Template(
"""
{% load component_tags %}
{% mytag count=1 name='John' 123 %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("positional argument follows keyword argument"),
):
template6.render(Context({}))
# Extra kwargs
template7 = Template(
"""
{% load component_tags %}
{% mytag 'John' msg='Hello' mode='custom' var=123 %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'var'"),
):
template7.render(Context({}))
# Extra kwargs - non-identifier or kwargs
template8 = Template(
"""
{% load component_tags %}
{% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': got an unexpected keyword argument 'data-id'"),
):
template8.render(Context({}))
# Extra arg after special kwargs
template9 = Template(
"""
{% load component_tags %}
{% mytag data-id=123 'John' msg='Hello' %}
"""
)
with pytest.raises(
SyntaxError,
match=re.escape("positional argument follows keyword argument"),
):
template9.render(Context({}))
TestNode1.unregister(component_tags.register)
def test_node_render_extra_args_and_kwargs(self):
captured = None
class TestNode1(BaseNode):
tag = "mytag"
allowed_flags = ["required", "default"]
@force_signature_validation
def render(self, context: Context, name: str, *args, msg: str, **kwargs) -> str:
nonlocal captured
captured = name, args, msg, kwargs
return ""
TestNode1.register(component_tags.register)
template1 = Template(
"""
{% load component_tags %}
{% mytag 'John'
123 456 789 msg='Hello' a=1 b=2 c=3 required
data-id=123 class="pa-4" @click.once="myVar"
%}
"""
)
template1.render(Context({}))
assert captured == (
"John",
(123, 456, 789),
"Hello",
{"a": 1, "b": 2, "c": 3, "data-id": 123, "class": "pa-4", "@click.once": "myVar"},
)
TestNode1.unregister(component_tags.register)
def test_node_render_kwargs_only(self):
captured = None
class TestNode(BaseNode):
tag = "mytag"
@force_signature_validation
def render(self, context: Context, **kwargs) -> str:
nonlocal captured
captured = kwargs
return ""
TestNode.register(component_tags.register)
# Test with various kwargs including non-identifier keys
template = Template(
"""
{% load component_tags %}
{% mytag
name='John'
age=25
data-id=123
class="header"
@click="handleClick"
v-if="isVisible"
%}
"""
)
template.render(Context({}))
# All kwargs should be accepted since the function accepts **kwargs
assert captured == {
"name": "John",
"age": 25,
"data-id": 123,
"class": "header",
"@click": "handleClick",
"v-if": "isVisible",
}
# Test with positional args (should fail since function only accepts kwargs)
template2 = Template(
"""
{% load component_tags %}
{% mytag "John" name="Mary" %}
"""
)
with pytest.raises(
TypeError,
match=re.escape("Invalid parameters for tag 'mytag': takes 0 positional arguments but 1 was given"),
):
template2.render(Context({}))
TestNode.unregister(component_tags.register)