mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 14:28:18 +00:00
refactor: change component typing from generics to class attributes (#1138)
This commit is contained in:
parent
912d8e8074
commit
b49002b545
25 changed files with 2451 additions and 610 deletions
|
@ -85,10 +85,10 @@ class TestComponentMediaCache:
|
|||
<div>Template only component</div>
|
||||
"""
|
||||
|
||||
def get_js_data(self):
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
def get_css_data(self):
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
@register("test_media_no_vars")
|
||||
|
@ -100,10 +100,10 @@ class TestComponentMediaCache:
|
|||
js = "console.log('Hello from JS');"
|
||||
css = ".novars-component { color: blue; }"
|
||||
|
||||
def get_js_data(self):
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
def get_css_data(self):
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class TestMediaAndVarsComponent(Component):
|
||||
|
@ -114,10 +114,10 @@ class TestComponentMediaCache:
|
|||
js = "console.log('Hello from full component');"
|
||||
css = ".full-component { color: blue; }"
|
||||
|
||||
def get_js_data(self):
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {"message": "Hello"}
|
||||
|
||||
def get_css_data(self):
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {"color": "blue"}
|
||||
|
||||
# Register our test cache
|
||||
|
|
|
@ -4,8 +4,7 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
|||
"""
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, Tuple, no_type_check
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
from typing import Any, Dict, no_type_check
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
|
@ -20,8 +19,6 @@ from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
|||
from django_components import (
|
||||
Component,
|
||||
ComponentView,
|
||||
SlotContent,
|
||||
Slot,
|
||||
all_components,
|
||||
get_component_by_class_id,
|
||||
register,
|
||||
|
@ -385,100 +382,6 @@ class TestComponent:
|
|||
|
||||
assert SimpleComponent.render() == "Hello"
|
||||
|
||||
def test_typing(self):
|
||||
# Types
|
||||
ButtonArgs = Tuple[str, ...]
|
||||
|
||||
class ButtonKwargs(TypedDict):
|
||||
name: str
|
||||
age: int
|
||||
maybe_var: NotRequired[int]
|
||||
|
||||
class ButtonFooterSlotData(TypedDict):
|
||||
value: int
|
||||
|
||||
class ButtonSlots(TypedDict):
|
||||
# Use `SlotContent` when you want to allow either function (`Slot` instance)
|
||||
# or plain string.
|
||||
header: SlotContent
|
||||
# Use `Slot` for slot functions. The generic specifies the data available to the slot function.
|
||||
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
||||
|
||||
# Data returned from `get_context_data`
|
||||
class ButtonData(TypedDict):
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
# Data returned from `get_js_data`
|
||||
class ButtonJsData(TypedDict):
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
# Data returned from `get_css_data`
|
||||
class ButtonCssData(TypedDict):
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
# Tests - We simply check that these don't raise any errors
|
||||
# nor any type errors.
|
||||
ButtonType1 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, ButtonJsData, ButtonCssData]
|
||||
ButtonType2 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, ButtonJsData]
|
||||
ButtonType3 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData]
|
||||
ButtonType4 = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||
ButtonType5 = Component[ButtonArgs, ButtonKwargs]
|
||||
ButtonType6 = Component[ButtonArgs]
|
||||
|
||||
class Button1(ButtonType1):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
class Button2(ButtonType2):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
class Button3(ButtonType3):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
class Button4(ButtonType4):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
class Button5(ButtonType5):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
class Button6(ButtonType6):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
Button1.render(
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||
)
|
||||
|
||||
Button2.render(
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||
)
|
||||
|
||||
Button3.render(
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||
)
|
||||
|
||||
Button4.render(
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||
)
|
||||
|
||||
Button5.render(
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "name", "age": 123},
|
||||
)
|
||||
|
||||
Button6.render(
|
||||
args=("arg1", "arg2"),
|
||||
)
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestComponentRender:
|
||||
|
|
|
@ -32,7 +32,7 @@ class TestComponentDefaults:
|
|||
# Check that args and slots are NOT affected by the defaults
|
||||
assert self.input.args == [123]
|
||||
assert [*self.input.slots.keys()] == ["my_slot"]
|
||||
assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT"
|
||||
assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT" # type: ignore[arg-type]
|
||||
|
||||
assert self.input.kwargs == {
|
||||
"variable": "test", # User-given
|
||||
|
|
615
tests/test_component_typing.py
Normal file
615
tests/test_component_typing.py
Normal file
|
@ -0,0 +1,615 @@
|
|||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import NamedTuple, Optional
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
import pytest
|
||||
from django.template import Context
|
||||
|
||||
from django_components import Component, Empty, Slot, SlotInput
|
||||
|
||||
from django_components.testing import djc_test
|
||||
from .testutils import setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestComponentTyping:
|
||||
def test_data_methods_input_typed(self):
|
||||
template_called = False
|
||||
js_called = False
|
||||
css_called = False
|
||||
|
||||
class ButtonFooterSlotData(TypedDict):
|
||||
value: int
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
@dataclass
|
||||
class Kwargs:
|
||||
name: str
|
||||
age: int
|
||||
maybe_var: Optional[int] = None
|
||||
|
||||
class Slots(TypedDict):
|
||||
# Use `SlotInput` when you want to pass slot as string
|
||||
header: SlotInput
|
||||
# Use `Slot` for slot functions.
|
||||
# The generic specifies the data available to the slot function
|
||||
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
||||
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
nonlocal template_called
|
||||
template_called = True
|
||||
|
||||
assert isinstance(args, Button.Args)
|
||||
assert isinstance(kwargs, Button.Kwargs)
|
||||
assert isinstance(slots, dict)
|
||||
assert isinstance(context, Context)
|
||||
|
||||
def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
nonlocal js_called
|
||||
js_called = True
|
||||
|
||||
assert isinstance(args, Button.Args)
|
||||
assert isinstance(kwargs, Button.Kwargs)
|
||||
assert isinstance(slots, dict)
|
||||
assert isinstance(context, Context)
|
||||
|
||||
def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
nonlocal css_called
|
||||
css_called = True
|
||||
|
||||
assert isinstance(args, Button.Args)
|
||||
assert isinstance(kwargs, Button.Kwargs)
|
||||
assert isinstance(slots, dict)
|
||||
assert isinstance(context, Context)
|
||||
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
Button.render(
|
||||
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||
kwargs=Button.Kwargs(name="name", age=123),
|
||||
slots=Button.Slots(
|
||||
header="HEADER",
|
||||
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||
),
|
||||
)
|
||||
|
||||
assert template_called
|
||||
assert js_called
|
||||
assert css_called
|
||||
|
||||
def test_data_methods_input_not_typed_by_default(self):
|
||||
template_called = False
|
||||
js_called = False
|
||||
css_called = False
|
||||
|
||||
class Button(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal template_called
|
||||
template_called = True
|
||||
|
||||
assert isinstance(args, list)
|
||||
assert isinstance(kwargs, dict)
|
||||
assert isinstance(slots, dict)
|
||||
assert isinstance(context, Context)
|
||||
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
nonlocal js_called
|
||||
js_called = True
|
||||
|
||||
assert isinstance(args, list)
|
||||
assert isinstance(kwargs, dict)
|
||||
assert isinstance(slots, dict)
|
||||
assert isinstance(context, Context)
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
nonlocal css_called
|
||||
css_called = True
|
||||
|
||||
assert isinstance(args, list)
|
||||
assert isinstance(kwargs, dict)
|
||||
assert isinstance(slots, dict)
|
||||
assert isinstance(context, Context)
|
||||
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
Button.render(
|
||||
args=["arg1", "arg2"],
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={
|
||||
"header": "HEADER",
|
||||
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||
},
|
||||
)
|
||||
|
||||
assert template_called
|
||||
assert js_called
|
||||
assert css_called
|
||||
|
||||
def test_data_methods_output_typed(self):
|
||||
template_called = False
|
||||
js_called = False
|
||||
css_called = False
|
||||
|
||||
template_data_instance = None
|
||||
js_data_instance = None
|
||||
css_data_instance = None
|
||||
|
||||
class Button(Component):
|
||||
# Data returned from `get_context_data`
|
||||
@dataclass
|
||||
class TemplateData:
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
def __post_init__(self):
|
||||
nonlocal template_data_instance
|
||||
template_data_instance = self
|
||||
|
||||
# Data returned from `get_js_data`
|
||||
@dataclass
|
||||
class JsData:
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
def __post_init__(self):
|
||||
nonlocal js_data_instance
|
||||
js_data_instance = self
|
||||
|
||||
# Data returned from `get_css_data`
|
||||
@dataclass
|
||||
class CssData:
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
def __post_init__(self):
|
||||
nonlocal css_data_instance
|
||||
css_data_instance = self
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal template_called
|
||||
template_called = True
|
||||
|
||||
return {
|
||||
"data1": "data1",
|
||||
"data2": 123,
|
||||
}
|
||||
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
nonlocal js_called
|
||||
js_called = True
|
||||
|
||||
return {
|
||||
"js_data1": "js_data1",
|
||||
"js_data2": 123,
|
||||
}
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
nonlocal css_called
|
||||
css_called = True
|
||||
|
||||
return {
|
||||
"css_data1": "css_data1",
|
||||
"css_data2": 123,
|
||||
}
|
||||
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
Button.render(
|
||||
args=["arg1", "arg2"],
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={
|
||||
"header": "HEADER",
|
||||
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||
},
|
||||
)
|
||||
|
||||
assert template_called
|
||||
assert js_called
|
||||
assert css_called
|
||||
|
||||
assert isinstance(template_data_instance, Button.TemplateData)
|
||||
assert isinstance(js_data_instance, Button.JsData)
|
||||
assert isinstance(css_data_instance, Button.CssData)
|
||||
|
||||
assert template_data_instance == Button.TemplateData(data1="data1", data2=123)
|
||||
assert js_data_instance == Button.JsData(js_data1="js_data1", js_data2=123)
|
||||
assert css_data_instance == Button.CssData(css_data1="css_data1", css_data2=123)
|
||||
|
||||
def test_data_methods_output_typed_reuses_instances(self):
|
||||
template_called = False
|
||||
js_called = False
|
||||
css_called = False
|
||||
|
||||
template_data_instance = None
|
||||
js_data_instance = None
|
||||
css_data_instance = None
|
||||
|
||||
class Button(Component):
|
||||
# Data returned from `get_context_data`
|
||||
@dataclass
|
||||
class TemplateData:
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
def __post_init__(self):
|
||||
nonlocal template_data_instance
|
||||
template_data_instance = self
|
||||
|
||||
# Data returned from `get_js_data`
|
||||
@dataclass
|
||||
class JsData:
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
def __post_init__(self):
|
||||
nonlocal js_data_instance
|
||||
js_data_instance = self
|
||||
|
||||
# Data returned from `get_css_data`
|
||||
@dataclass
|
||||
class CssData:
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
def __post_init__(self):
|
||||
nonlocal css_data_instance
|
||||
css_data_instance = self
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal template_called
|
||||
template_called = True
|
||||
|
||||
data = Button.TemplateData(
|
||||
data1="data1",
|
||||
data2=123,
|
||||
)
|
||||
|
||||
# Reset the instance to None to check if the instance is reused
|
||||
nonlocal template_data_instance
|
||||
template_data_instance = None
|
||||
|
||||
return data
|
||||
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
nonlocal js_called
|
||||
js_called = True
|
||||
|
||||
data = Button.JsData(
|
||||
js_data1="js_data1",
|
||||
js_data2=123,
|
||||
)
|
||||
|
||||
# Reset the instance to None to check if the instance is reused
|
||||
nonlocal js_data_instance
|
||||
js_data_instance = None
|
||||
|
||||
return data
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
nonlocal css_called
|
||||
css_called = True
|
||||
|
||||
data = Button.CssData(
|
||||
css_data1="css_data1",
|
||||
css_data2=123,
|
||||
)
|
||||
|
||||
# Reset the instance to None to check if the instance is reused
|
||||
nonlocal css_data_instance
|
||||
css_data_instance = None
|
||||
|
||||
return data
|
||||
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
Button.render(
|
||||
args=["arg1", "arg2"],
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={
|
||||
"header": "HEADER",
|
||||
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||
},
|
||||
)
|
||||
|
||||
assert template_called
|
||||
assert js_called
|
||||
assert css_called
|
||||
|
||||
assert template_data_instance is not None
|
||||
assert js_data_instance is not None
|
||||
assert css_data_instance is not None
|
||||
|
||||
def test_builtin_classes(self):
|
||||
class ButtonFooterSlotData(TypedDict):
|
||||
value: int
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
@dataclass
|
||||
class Kwargs:
|
||||
name: str
|
||||
age: int
|
||||
maybe_var: Optional[int] = None
|
||||
|
||||
class Slots(TypedDict):
|
||||
# Use `SlotInput` when you want to pass slot as string
|
||||
header: SlotInput
|
||||
# Use `Slot` for slot functions.
|
||||
# The generic specifies the data available to the slot function
|
||||
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
||||
|
||||
# Data returned from `get_context_data`
|
||||
class TemplateData(NamedTuple):
|
||||
data1: str
|
||||
data2: int
|
||||
data3: str
|
||||
|
||||
# Data returned from `get_js_data`
|
||||
@dataclass
|
||||
class JsData:
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
js_data3: str
|
||||
|
||||
# Data returned from `get_css_data`
|
||||
@dataclass
|
||||
class CssData:
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
css_data3: str
|
||||
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
return self.TemplateData(
|
||||
data1=kwargs.name,
|
||||
data2=kwargs.age,
|
||||
data3=args.arg1,
|
||||
)
|
||||
|
||||
def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
return self.JsData(
|
||||
js_data1=kwargs.name,
|
||||
js_data2=kwargs.age,
|
||||
js_data3=args.arg1,
|
||||
)
|
||||
|
||||
def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
return self.CssData(
|
||||
css_data1=kwargs.name,
|
||||
css_data2=kwargs.age,
|
||||
css_data3=args.arg1,
|
||||
)
|
||||
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
# Success case
|
||||
Button.render(
|
||||
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||
kwargs=Button.Kwargs(name="name", age=123),
|
||||
slots=Button.Slots(
|
||||
header="HEADER",
|
||||
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||
),
|
||||
)
|
||||
|
||||
# Failure case 1: NamedTuple raises error when a required argument is missing
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match=re.escape("missing 1 required positional argument: 'arg2'"),
|
||||
):
|
||||
Button.render(
|
||||
# Missing arg2
|
||||
args=Button.Args(arg1="arg1"), # type: ignore[call-arg]
|
||||
kwargs=Button.Kwargs(name="name", age=123),
|
||||
slots=Button.Slots(
|
||||
header="HEADER",
|
||||
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||
),
|
||||
)
|
||||
|
||||
# Failure case 2: Dataclass raises error when a required argument is missing
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match=re.escape("missing 1 required positional argument: 'name'"),
|
||||
):
|
||||
Button.render(
|
||||
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||
# Name missing
|
||||
kwargs=Button.Kwargs(age=123), # type: ignore[call-arg]
|
||||
slots=Button.Slots(
|
||||
header="HEADER",
|
||||
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||
),
|
||||
)
|
||||
|
||||
# Failure case 3
|
||||
# NOTE: While we would expect this to raise, seems that TypedDict (`Slots`)
|
||||
# does NOT raise an error when a required key is missing.
|
||||
Button.render(
|
||||
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||
kwargs=Button.Kwargs(name="name", age=123),
|
||||
slots=Button.Slots( # type: ignore[typeddict-item]
|
||||
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"), # Missing header
|
||||
),
|
||||
)
|
||||
|
||||
# Failure case 4: Data object is not of the expected type
|
||||
class ButtonBad(Button):
|
||||
class TemplateData(NamedTuple):
|
||||
data1: str
|
||||
data2: int # Removed data3
|
||||
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match=re.escape("got an unexpected keyword argument 'data3'"),
|
||||
):
|
||||
ButtonBad.render(
|
||||
args=ButtonBad.Args(arg1="arg1", arg2="arg2"),
|
||||
kwargs=ButtonBad.Kwargs(name="name", age=123),
|
||||
)
|
||||
|
||||
def test_empty_type(self):
|
||||
template_called = False
|
||||
|
||||
class Button(Component):
|
||||
template = "Hello"
|
||||
|
||||
Args = Empty
|
||||
Kwargs = Empty
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal template_called
|
||||
template_called = True
|
||||
|
||||
assert isinstance(args, Empty)
|
||||
assert isinstance(kwargs, Empty)
|
||||
|
||||
# Success case
|
||||
Button.render()
|
||||
assert template_called
|
||||
|
||||
# Failure cases
|
||||
with pytest.raises(TypeError, match=re.escape("got an unexpected keyword argument 'arg1'")):
|
||||
Button.render(
|
||||
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("got an unexpected keyword argument 'arg1'")):
|
||||
Button.render(
|
||||
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
def test_custom_args_class_raises_on_invalid(self):
|
||||
class Button(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Args:
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
|
||||
Button.render(
|
||||
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
class Button2(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Args:
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("'Args' object is not iterable")):
|
||||
Button2.render(
|
||||
args=Button2.Args(arg1="arg1", arg2="arg2"),
|
||||
)
|
||||
|
||||
class Button3(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Args:
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def __iter__(self):
|
||||
return iter([self.arg1, self.arg2])
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
|
||||
Button3.render(
|
||||
args=Button3.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
def test_custom_args_class_custom(self):
|
||||
class Button(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Args:
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
def __iter__(self):
|
||||
return iter([self.arg1, self.arg2])
|
||||
|
||||
Button.render(
|
||||
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||
)
|
||||
|
||||
def test_custom_kwargs_class_raises_on_invalid(self):
|
||||
class Button(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Kwargs:
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
|
||||
Button.render(
|
||||
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
class Button2(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Kwargs:
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("'Kwargs' object is not iterable")):
|
||||
Button2.render(
|
||||
kwargs=Button2.Kwargs(arg1="arg1", arg2="arg2"),
|
||||
)
|
||||
|
||||
class Button3(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Kwargs:
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def _asdict(self):
|
||||
return {"arg1": self.arg1, "arg2": self.arg2}
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
|
||||
Button3.render(
|
||||
kwargs=Button3.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
def test_custom_kwargs_class_custom(self):
|
||||
class Button(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Kwargs:
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
def _asdict(self):
|
||||
return {"arg1": self.arg1, "arg2": self.arg2}
|
||||
|
||||
Button.render(
|
||||
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"),
|
||||
)
|
|
@ -198,7 +198,10 @@ class TestComponentAsView(SimpleTestCase):
|
|||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||
return self.render_to_response({"name": "Bob"}, {"second_slot": "Nice to meet you, Bob"})
|
||||
return self.render_to_response(
|
||||
context={"name": "Bob"},
|
||||
slots={"second_slot": "Nice to meet you, Bob"},
|
||||
)
|
||||
|
||||
client = CustomClient(urlpatterns=[path("test_slot/", MockComponentSlot.as_view())])
|
||||
response = client.get("/test_slot/")
|
||||
|
@ -223,7 +226,10 @@ class TestComponentAsView(SimpleTestCase):
|
|||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||
return self.render_to_response({}, {"test_slot": "<script>alert(1);</script>"})
|
||||
return self.render_to_response(
|
||||
context={},
|
||||
slots={"test_slot": "<script>alert(1);</script>"},
|
||||
)
|
||||
|
||||
client = CustomClient(urlpatterns=[path("test_slot_insecure/", MockInsecureComponentSlot.as_view())])
|
||||
response = client.get("/test_slot_insecure/")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue