mirror of
https://github.com/django-components/django-components.git
synced 2025-11-06 09:39:28 +00:00
refactor: make it optional having to specify parent class of Args, Kwargs, Slots, etc (#1466)
This commit is contained in:
parent
0aeb96fa40
commit
c37628dea0
34 changed files with 661 additions and 299 deletions
55
CHANGELOG.md
55
CHANGELOG.md
|
|
@ -1,5 +1,60 @@
|
||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
|
## v0.142.4
|
||||||
|
|
||||||
|
#### Refactor
|
||||||
|
|
||||||
|
- Simpler syntax for defining component inputs.
|
||||||
|
|
||||||
|
When defining `Args`, `Kwargs`, `Slots`, `JsData`, `CssData`, `TemplateData`, these data classes now don't have to subclass any other class.
|
||||||
|
|
||||||
|
If they are not subclassing (nor `@dataclass`), these data classes will be automatically converted to `NamedTuples`:
|
||||||
|
|
||||||
|
Before - the `Args`, `Kwargs`, and `Slots` (etc..) had to be NamedTuples, dataclasses, or Pydantic models:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import NamedTuple
|
||||||
|
from django_components import Component
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
class Args(NamedTuple):
|
||||||
|
size: int
|
||||||
|
text: str
|
||||||
|
|
||||||
|
class Kwargs(NamedTuple):
|
||||||
|
variable: str
|
||||||
|
maybe_var: Optional[int] = None
|
||||||
|
|
||||||
|
class Slots(NamedTuple):
|
||||||
|
my_slot: Optional[SlotInput] = None
|
||||||
|
|
||||||
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Now these classes are automatically converted to `NamedTuples` if they don't subclass anything else:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Button(Component):
|
||||||
|
class Args: # Same as `Args(NamedTuple)`
|
||||||
|
size: int
|
||||||
|
text: str
|
||||||
|
|
||||||
|
class Kwargs: # Same as `Kwargs(NamedTuple)`
|
||||||
|
variable: str
|
||||||
|
maybe_var: Optional[int] = None
|
||||||
|
|
||||||
|
class Slots: # Same as `Slots(NamedTuple)`
|
||||||
|
my_slot: Optional[SlotInput] = None
|
||||||
|
|
||||||
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- Extension authors: The `ExtensionComponentConfig` can be instantiated with `None` instead of a component instance.
|
||||||
|
|
||||||
|
This allows to call component-level extension methods outside of the normal rendering lifecycle.
|
||||||
|
|
||||||
## v0.142.3
|
## v0.142.3
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
|
||||||
|
|
@ -405,21 +405,21 @@ Avoid needless errors with [type hints and runtime input validation](https://dja
|
||||||
To opt-in to input validation, define types for component's args, kwargs, slots, and more:
|
To opt-in to input validation, define types for component's args, kwargs, slots, and more:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django_components import Component, Slot, SlotInput
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
size: int
|
size: int
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
variable: str
|
variable: str
|
||||||
another: int
|
another: int
|
||||||
maybe_var: Optional[int] = None # May be omitted
|
maybe_var: Optional[int] = None # May be omitted
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
my_slot: Optional[SlotInput] = None
|
my_slot: Optional[SlotInput] = None
|
||||||
another_slot: SlotInput
|
another_slot: SlotInput
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ For live examples, see the [Examples](../../examples/index.md).
|
||||||
It's also a good idea to have a common prefix for your components, so they can be easily distinguished from users' components. In the example below, we use the prefix `my_` / `My`.
|
It's also a good idea to have a common prefix for your components, so they can be easily distinguished from users' components. In the example below, we use the prefix `my_` / `My`.
|
||||||
|
|
||||||
```djc_py
|
```djc_py
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
from django_components import Component, SlotInput, register, types
|
from django_components import Component, SlotInput, register, types
|
||||||
|
|
||||||
from myapp.templatetags.mytags import comp_registry
|
from myapp.templatetags.mytags import comp_registry
|
||||||
|
|
@ -111,16 +111,16 @@ For live examples, see the [Examples](../../examples/index.md).
|
||||||
@register("my_menu", registry=comp_registry)
|
@register("my_menu", registry=comp_registry)
|
||||||
class MyMenu(Component):
|
class MyMenu(Component):
|
||||||
# Define the types
|
# Define the types
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
size: int
|
size: int
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
vertical: Optional[bool] = None
|
vertical: Optional[bool] = None
|
||||||
klass: Optional[str] = None
|
klass: Optional[str] = None
|
||||||
style: Optional[str] = None
|
style: Optional[str] = None
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
default: Optional[SlotInput] = None
|
default: Optional[SlotInput] = None
|
||||||
|
|
||||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
|
|
||||||
|
|
@ -65,12 +65,14 @@ E.g.:
|
||||||
Each method of the nested classes has access to the `component` attribute,
|
Each method of the nested classes has access to the `component` attribute,
|
||||||
which points to the component instance.
|
which points to the component instance.
|
||||||
|
|
||||||
|
This may be `None` if the methods does NOT run during the rendering.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class MyTable(Component):
|
class MyTable(Component):
|
||||||
class MyExtension:
|
class MyExtension:
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
# `self.component` points to the instance of `MyTable` Component.
|
# `self.component` points to the instance of `MyTable` Component.
|
||||||
return self.component.render_to_response(request=request)
|
return self.component.kwargs.some_input
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example: Component as View
|
### Example: Component as View
|
||||||
|
|
|
||||||
|
|
@ -289,14 +289,14 @@ To implement this, we render the fallback slot in [`on_render()`](../../../refer
|
||||||
and return it if an error occured:
|
and return it if an error occured:
|
||||||
|
|
||||||
```djc_py
|
```djc_py
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django_components import Component, OnRenderGenerator, SlotInput, types
|
from django_components import Component, OnRenderGenerator, SlotInput, types
|
||||||
|
|
||||||
class ErrorFallback(Component):
|
class ErrorFallback(Component):
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
default: Optional[SlotInput] = None
|
default: Optional[SlotInput] = None
|
||||||
fallback: Optional[SlotInput] = None
|
fallback: Optional[SlotInput] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ and so `selected_items` will be set to `[1, 2, 3]`.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class ProfileCard(Component):
|
class ProfileCard(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
show_details: bool = True
|
show_details: bool = True
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Each method handles the data independently - you can define different data for t
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class ProfileCard(Component):
|
class ProfileCard(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
user_id: int
|
user_id: int
|
||||||
show_details: bool
|
show_details: bool
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ If [`get_template_data()`](../../../reference/api/#django_components.Component.g
|
||||||
class ProfileCard(Component):
|
class ProfileCard(Component):
|
||||||
template_file = "profile_card.html"
|
template_file = "profile_card.html"
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
user_id: int
|
user_id: int
|
||||||
show_details: bool
|
show_details: bool
|
||||||
|
|
||||||
|
|
@ -182,10 +182,10 @@ class ProfileCard(Component):
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class ProfileCard(Component):
|
class ProfileCard(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
user_id: int
|
user_id: int
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
show_details: bool
|
show_details: bool
|
||||||
|
|
||||||
# Access inputs directly as parameters
|
# Access inputs directly as parameters
|
||||||
|
|
@ -228,10 +228,10 @@ class ProfileCard(Component):
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class ProfileCard(Component):
|
class ProfileCard(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
user_id: int
|
user_id: int
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
show_details: bool
|
show_details: bool
|
||||||
|
|
||||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
|
||||||
|
|
@ -320,7 +320,7 @@ from django_components import Component, Default, register
|
||||||
|
|
||||||
@register("profile_card")
|
@register("profile_card")
|
||||||
class ProfileCard(Component):
|
class ProfileCard(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
show_details: bool
|
show_details: bool
|
||||||
|
|
||||||
class Defaults:
|
class Defaults:
|
||||||
|
|
@ -344,7 +344,7 @@ class ProfileCard(Component):
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class ProfileCard(Component):
|
class ProfileCard(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
show_details: bool = True
|
show_details: bool = True
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -391,18 +391,18 @@ This will also validate the inputs at runtime, as the type classes will be insta
|
||||||
Read more about [Component typing](../../fundamentals/typing_and_validation).
|
Read more about [Component typing](../../fundamentals/typing_and_validation).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
from django_components import Component, SlotInput
|
from django_components import Component, SlotInput
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
surname: str
|
surname: str
|
||||||
maybe_var: Optional[int] = None # May be omitted
|
maybe_var: Optional[int] = None # May be omitted
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
my_slot: Optional[SlotInput] = None
|
my_slot: Optional[SlotInput] = None
|
||||||
footer: SlotInput
|
footer: SlotInput
|
||||||
|
|
||||||
|
|
@ -433,19 +433,18 @@ and [`CssData`](../../../reference/api/#django_components.Component.CssData) cla
|
||||||
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class.
|
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class TemplateData(NamedTuple):
|
class TemplateData(
|
||||||
data1: str
|
data1: str
|
||||||
data2: int
|
data2: int
|
||||||
|
|
||||||
class JsData(NamedTuple):
|
class JsData:
|
||||||
js_data1: str
|
js_data1: str
|
||||||
js_data2: int
|
js_data2: int
|
||||||
|
|
||||||
class CssData(NamedTuple):
|
class CssData:
|
||||||
css_data1: str
|
css_data1: str
|
||||||
css_data2: int
|
css_data2: int
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ With `Args` class:
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
page: int
|
page: int
|
||||||
per_page: int
|
per_page: int
|
||||||
|
|
||||||
|
|
@ -137,7 +137,7 @@ With `Kwargs` class:
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
page: int
|
page: int
|
||||||
per_page: int
|
per_page: int
|
||||||
|
|
||||||
|
|
@ -182,7 +182,7 @@ With `Slots` class:
|
||||||
from django_components import Component, Slot, SlotInput
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
header: SlotInput
|
header: SlotInput
|
||||||
footer: SlotInput
|
footer: SlotInput
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,14 +80,13 @@ For a component to be renderable with the [`{% component %}`](../../../reference
|
||||||
For example, if you register a component under the name `"button"`:
|
For example, if you register a component under the name `"button"`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component, register
|
from django_components import Component, register
|
||||||
|
|
||||||
@register("button")
|
@register("button")
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
template_file = "button.html"
|
template_file = "button.html"
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
name: str
|
name: str
|
||||||
job: str
|
job: str
|
||||||
|
|
||||||
|
|
@ -207,20 +206,20 @@ The [`Component.render()`](../../../reference/api/#django_components.Component.r
|
||||||
This is the equivalent of calling the [`{% component %}`](../template_tags#component) tag.
|
This is the equivalent of calling the [`{% component %}`](../template_tags#component) tag.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
from django_components import Component, SlotInput
|
from django_components import Component, SlotInput
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
template_file = "button.html"
|
template_file = "button.html"
|
||||||
|
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
surname: str
|
surname: str
|
||||||
age: int
|
age: int
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
footer: Optional[SlotInput] = None
|
footer: Optional[SlotInput] = None
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
|
@ -264,20 +263,20 @@ Any extra arguments are passed to the [`HttpResponse`](https://docs.djangoprojec
|
||||||
constructor.
|
constructor.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
from django_components import Component, SlotInput
|
from django_components import Component, SlotInput
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
template_file = "button.html"
|
template_file = "button.html"
|
||||||
|
|
||||||
class Args(NamedTuple):
|
class Args(
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
surname: str
|
surname: str
|
||||||
age: int
|
age: int
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
footer: Optional[SlotInput] = None
|
footer: Optional[SlotInput] = None
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
|
@ -486,19 +485,19 @@ and [`Slots`](../../../reference/api/#django_components.Component.Slots) classes
|
||||||
Read more on [Typing and validation](../../fundamentals/typing_and_validation).
|
Read more on [Typing and validation](../../fundamentals/typing_and_validation).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
from django_components import Component, Slot, SlotInput
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
# Define the component with the types
|
# Define the component with the types
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class Args(NamedTuple):
|
class Args(
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
surname: str
|
surname: str
|
||||||
age: int
|
age: int
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
my_slot: Optional[SlotInput] = None
|
my_slot: Optional[SlotInput] = None
|
||||||
footer: SlotInput
|
footer: SlotInput
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,20 +20,20 @@ that allow you to define the types of args, kwargs, slots, as well as the data r
|
||||||
Use this to add type hints to your components, to validate the inputs at runtime, and to document them.
|
Use this to add type hints to your components, to validate the inputs at runtime, and to document them.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django_components import Component, SlotInput
|
from django_components import Component, SlotInput
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
size: int
|
size: int
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
variable: str
|
variable: str
|
||||||
maybe_var: Optional[int] = None # May be omitted
|
maybe_var: Optional[int] = None # May be omitted
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
my_slot: Optional[SlotInput] = None
|
my_slot: Optional[SlotInput] = None
|
||||||
|
|
||||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
|
@ -59,7 +59,7 @@ You can use [`Component.Args`](../../../reference/api#django_components.Componen
|
||||||
[`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs),
|
[`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs),
|
||||||
and [`Component.Slots`](../../../reference/api#django_components.Component.Slots) to type the component inputs.
|
and [`Component.Slots`](../../../reference/api#django_components.Component.Slots) to type the component inputs.
|
||||||
|
|
||||||
When you set these classes, at render time the `args`, `kwargs`, and `slots` parameters of the data methods
|
When you set these input classes, the `args`, `kwargs`, and `slots` parameters of the data methods
|
||||||
([`get_template_data()`](../../../reference/api#django_components.Component.get_template_data),
|
([`get_template_data()`](../../../reference/api#django_components.Component.get_template_data),
|
||||||
[`get_js_data()`](../../../reference/api#django_components.Component.get_js_data),
|
[`get_js_data()`](../../../reference/api#django_components.Component.get_js_data),
|
||||||
[`get_css_data()`](../../../reference/api#django_components.Component.get_css_data))
|
[`get_css_data()`](../../../reference/api#django_components.Component.get_css_data))
|
||||||
|
|
@ -80,7 +80,7 @@ or set them to `None`, the inputs will be passed as plain lists or dictionaries,
|
||||||
and will not be validated.
|
and will not be validated.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing_extensions import NamedTuple, TypedDict
|
from typing_extensions import TypedDict
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django_components import Component, Slot, SlotInput
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
|
|
@ -90,15 +90,15 @@ class ButtonFooterSlotData(TypedDict):
|
||||||
|
|
||||||
# Define the component with the types
|
# Define the component with the types
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
surname: str
|
surname: str
|
||||||
age: int
|
age: int
|
||||||
maybe_var: Optional[int] = None # May be omitted
|
maybe_var: Optional[int] = None # May be omitted
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
||||||
# plain string, or a function that returns a string.
|
# plain string, or a function that returns a string.
|
||||||
my_slot: Optional[SlotInput] = None
|
my_slot: Optional[SlotInput] = None
|
||||||
|
|
@ -143,7 +143,7 @@ class Button(Component):
|
||||||
Args = None
|
Args = None
|
||||||
Slots = None
|
Slots = None
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
name: str
|
name: str
|
||||||
age: int
|
age: int
|
||||||
|
|
||||||
|
|
@ -195,19 +195,18 @@ If you omit the [`TemplateData`](../../../reference/api#django_components.Compon
|
||||||
or set them to `None`, the validation and instantiation will be skipped.
|
or set them to `None`, the validation and instantiation will be skipped.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class TemplateData(NamedTuple):
|
class TemplateData:
|
||||||
data1: str
|
data1: str
|
||||||
data2: int
|
data2: int
|
||||||
|
|
||||||
class JsData(NamedTuple):
|
class JsData:
|
||||||
js_data1: str
|
js_data1: str
|
||||||
js_data2: int
|
js_data2: int
|
||||||
|
|
||||||
class CssData(NamedTuple):
|
class CssData:
|
||||||
css_data1: str
|
css_data1: str
|
||||||
css_data2: int
|
css_data2: int
|
||||||
|
|
||||||
|
|
@ -233,19 +232,18 @@ class Button(Component):
|
||||||
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class directly.
|
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class directly.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class TemplateData(NamedTuple):
|
class TemplateData:
|
||||||
data1: str
|
data1: str
|
||||||
data2: int
|
data2: int
|
||||||
|
|
||||||
class JsData(NamedTuple):
|
class JsData:
|
||||||
js_data1: str
|
js_data1: str
|
||||||
js_data2: int
|
js_data2: int
|
||||||
|
|
||||||
class CssData(NamedTuple):
|
class CssData:
|
||||||
css_data1: str
|
css_data1: str
|
||||||
css_data2: int
|
css_data2: int
|
||||||
|
|
||||||
|
|
@ -270,34 +268,63 @@ class Button(Component):
|
||||||
|
|
||||||
## Custom types
|
## Custom types
|
||||||
|
|
||||||
We recommend to use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
So far, we've defined the input classes like `Kwargs` as simple classes.
|
||||||
for the `Args` class, and [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple),
|
|
||||||
|
The truth is that when these classes don't subclass anything else,
|
||||||
|
they are converted to `NamedTuples` behind the scenes.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Table(Component):
|
||||||
|
class Kwargs:
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
```
|
||||||
|
|
||||||
|
is the same as:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Table(Component):
|
||||||
|
class Kwargs(NamedTuple):
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
```
|
||||||
|
|
||||||
|
You can actually set these classes to anything you want - whether it's dataclasses,
|
||||||
|
[Pydantic models](https://docs.pydantic.dev/latest/concepts/models/), or custom classes:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
from django_components import Component, Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
class Args(NamedTuple):
|
||||||
|
size: int
|
||||||
|
text: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Kwargs:
|
||||||
|
variable: str
|
||||||
|
maybe_var: Optional[int] = None
|
||||||
|
|
||||||
|
class Slots(BaseModel):
|
||||||
|
my_slot: Optional[SlotInput] = None
|
||||||
|
|
||||||
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
We recommend:
|
||||||
|
|
||||||
|
- [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||||
|
for the `Args` class
|
||||||
|
- [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple),
|
||||||
[dataclasses](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
|
[dataclasses](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
|
||||||
or [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
|
or [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
|
||||||
for `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData` classes.
|
for `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData` classes.
|
||||||
|
|
||||||
However, you can use any class, as long as they meet the conditions below.
|
However, you can use any class, as long as they meet the conditions below.
|
||||||
|
|
||||||
For example, here is how you can use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
|
|
||||||
to validate the inputs at runtime.
|
|
||||||
|
|
||||||
```py
|
|
||||||
from django_components import Component
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
class Table(Component):
|
|
||||||
class Kwargs(BaseModel):
|
|
||||||
name: str
|
|
||||||
age: int
|
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
|
||||||
assert isinstance(kwargs, Table.Kwargs)
|
|
||||||
|
|
||||||
Table.render(
|
|
||||||
kwargs=Table.Kwargs(name="John", age=30),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### `Args` class
|
### `Args` class
|
||||||
|
|
||||||
The [`Args`](../../../reference/api#django_components.Component.Args) class
|
The [`Args`](../../../reference/api#django_components.Component.Args) class
|
||||||
|
|
@ -390,7 +417,7 @@ As a workaround:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
args: List[str]
|
args: List[str]
|
||||||
|
|
||||||
Table.render(
|
Table.render(
|
||||||
|
|
@ -402,7 +429,7 @@ As a workaround:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
variable: str
|
variable: str
|
||||||
another: int
|
another: int
|
||||||
# Pass any extra keys under `extra`
|
# Pass any extra keys under `extra`
|
||||||
|
|
@ -422,17 +449,16 @@ As a workaround:
|
||||||
To declare that a component accepts no args, kwargs, etc, define the types with no attributes using the `pass` keyword:
|
To declare that a component accepts no args, kwargs, etc, define the types with no attributes using the `pass` keyword:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
pass
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -459,14 +485,14 @@ In the example below, `ButtonExtra` inherits `Kwargs` from `Button`, but overrid
|
||||||
from django_components import Component, Empty
|
from django_components import Component, Empty
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
class ButtonExtra(Button):
|
class ButtonExtra(Button):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
name: str
|
name: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
@ -491,10 +517,10 @@ Compare the following:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
# Both `Args` and `Kwargs` are defined on the class
|
# Both `Args` and `Kwargs` are defined on the class
|
||||||
|
|
@ -624,23 +650,23 @@ The steps to migrate are:
|
||||||
Thus, the code above will become:
|
Thus, the code above will become:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django_components import Component, Slot, SlotInput
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
# The Component class does not take any generics
|
# The Component class does not take any generics
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
# Types are now defined inside the component class
|
# Types are now defined inside the component class
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
size: int
|
size: int
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
variable: str
|
variable: str
|
||||||
another: int
|
another: int
|
||||||
maybe_var: Optional[int] = None # May be omitted
|
maybe_var: Optional[int] = None # May be omitted
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
||||||
# plain string, or a function that returns a string.
|
# plain string, or a function that returns a string.
|
||||||
my_slot: Optional[SlotInput] = None
|
my_slot: Optional[SlotInput] = None
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# ruff: noqa: S311
|
# ruff: noqa: S311
|
||||||
import random
|
import random
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django_components import Component, register, types
|
from django_components import Component, register, types
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ DESCRIPTION = "Dynamically render different component versions. Use for A/B test
|
||||||
|
|
||||||
@register("offer_card_old")
|
@register("offer_card_old")
|
||||||
class OfferCardOld(Component):
|
class OfferCardOld(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
savings_percent: int
|
savings_percent: int
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
|
@ -45,7 +45,7 @@ class OfferCardNew(OfferCardOld):
|
||||||
|
|
||||||
@register("offer_card")
|
@register("offer_card")
|
||||||
class OfferCard(Component):
|
class OfferCard(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
savings_percent: int
|
savings_percent: int
|
||||||
use_new_version: Optional[bool] = None
|
use_new_version: Optional[bool] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Dict, List, NamedTuple
|
from typing import Dict, List
|
||||||
|
|
||||||
from django_components import Component, register, types
|
from django_components import Component, register, types
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ error_rate = {
|
||||||
|
|
||||||
@register("api_widget")
|
@register("api_widget")
|
||||||
class ApiWidget(Component):
|
class ApiWidget(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
simulate_error: bool = False
|
simulate_error: bool = False
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
# ruff: noqa: S311
|
# ruff: noqa: S311
|
||||||
import random
|
import random
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
from django_components import Component, register, types
|
from django_components import Component, register, types
|
||||||
|
|
||||||
|
|
@ -9,7 +8,7 @@ DESCRIPTION = "A component that catches errors and displays fallback content, si
|
||||||
|
|
||||||
@register("weather_widget")
|
@register("weather_widget")
|
||||||
class WeatherWidget(Component):
|
class WeatherWidget(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
location: str
|
location: str
|
||||||
simulate_error: bool = False
|
simulate_error: bool = False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
from django_components import Component, Slot, register, types
|
from django_components import Component, Slot, register, types
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ DESCRIPTION = "Form that automatically arranges fields in a grid and generates l
|
||||||
class FormGrid(Component):
|
class FormGrid(Component):
|
||||||
"""Form that automatically arranges fields in a grid and generates labels."""
|
"""Form that automatically arranges fields in a grid and generates labels."""
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
editable: bool = True
|
editable: bool = True
|
||||||
method: str = "post"
|
method: str = "post"
|
||||||
form_content_attrs: Optional[dict] = None
|
form_content_attrs: Optional[dict] = None
|
||||||
|
|
@ -114,7 +114,7 @@ def prepare_form_grid(slots: Dict[str, Slot]):
|
||||||
# Case: Component user didn't explicitly define how to render the label
|
# Case: Component user didn't explicitly define how to render the label
|
||||||
# We will create the label for the field automatically
|
# We will create the label for the field automatically
|
||||||
label = FormGridLabel.render(
|
label = FormGridLabel.render(
|
||||||
kwargs=FormGridLabel.Kwargs(field_name=field_name),
|
kwargs=FormGridLabel.Kwargs(field_name=field_name), # type: ignore[call-arg]
|
||||||
deps_strategy="ignore",
|
deps_strategy="ignore",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -134,7 +134,7 @@ class FormGridLabel(Component):
|
||||||
</label>
|
</label>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
field_name: str
|
field_name: str
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ DESCRIPTION = "Handle the entire form submission flow in a single file and witho
|
||||||
|
|
||||||
@register("thank_you_message")
|
@register("thank_you_message")
|
||||||
class ThankYouMessage(Component):
|
class ThankYouMessage(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
from django_components import Component, register, types
|
from django_components import Component, register, types
|
||||||
|
|
||||||
DESCRIPTION = "Use HTML fragments (partials) with HTMX, AlpineJS, or plain JS."
|
DESCRIPTION = "Use HTML fragments (partials) with HTMX, AlpineJS, or plain JS."
|
||||||
|
|
@ -9,7 +7,7 @@ DESCRIPTION = "Use HTML fragments (partials) with HTMX, AlpineJS, or plain JS."
|
||||||
class SimpleFragment(Component):
|
class SimpleFragment(Component):
|
||||||
"""A simple fragment with JS and CSS."""
|
"""A simple fragment with JS and CSS."""
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
|
|
@ -37,7 +35,7 @@ class SimpleFragment(Component):
|
||||||
class AlpineFragment(Component):
|
class AlpineFragment(Component):
|
||||||
"""A fragment that defines an AlpineJS component."""
|
"""A fragment that defines an AlpineJS component."""
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
type: str
|
type: str
|
||||||
|
|
||||||
# The fragment is wrapped in `<template x-if="false">` so that we prevent
|
# The fragment is wrapped in `<template x-if="false">` so that we prevent
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
from django_components import Component, register, types
|
from django_components import Component, register, types
|
||||||
|
|
||||||
DESCRIPTION = "100 nested components? Not a problem! Handle recursive rendering out of the box."
|
DESCRIPTION = "100 nested components? Not a problem! Handle recursive rendering out of the box."
|
||||||
|
|
@ -7,7 +5,7 @@ DESCRIPTION = "100 nested components? Not a problem! Handle recursive rendering
|
||||||
|
|
||||||
@register("recursion")
|
@register("recursion")
|
||||||
class Recursion(Component):
|
class Recursion(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
current_depth: int = 0
|
current_depth: int = 0
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ class Tablist(Component):
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
id: Optional[str] = None
|
id: Optional[str] = None
|
||||||
name: str = "Tabs"
|
name: str = "Tabs"
|
||||||
selected_tab: Optional[str] = None
|
selected_tab: Optional[str] = None
|
||||||
|
|
@ -341,7 +341,7 @@ class Tab(Component):
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
header: str
|
header: str
|
||||||
disabled: bool = False
|
disabled: bool = False
|
||||||
id: Optional[str] = None
|
id: Optional[str] = None
|
||||||
|
|
|
||||||
|
|
@ -393,21 +393,21 @@ Avoid needless errors with [type hints and runtime input validation](https://dja
|
||||||
To opt-in to input validation, define types for component's args, kwargs, slots:
|
To opt-in to input validation, define types for component's args, kwargs, slots:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django_components import Component, Slot, SlotInput
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
size: int
|
size: int
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
variable: str
|
variable: str
|
||||||
another: int
|
another: int
|
||||||
maybe_var: Optional[int] = None # May be omitted
|
maybe_var: Optional[int] = None # May be omitted
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
my_slot: Optional[SlotInput] = None
|
my_slot: Optional[SlotInput] = None
|
||||||
another_slot: SlotInput
|
another_slot: SlotInput
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
from django_components import Component, register
|
from django_components import Component, register
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,7 +13,7 @@ class Calendar(Component):
|
||||||
js_file = "calendar/calendar.js"
|
js_file = "calendar/calendar.js"
|
||||||
|
|
||||||
# This component takes one parameter, a date string to show in the template
|
# This component takes one parameter, a date string to show in the template
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
date: str
|
date: str
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||||
|
|
@ -46,7 +44,7 @@ class CalendarRelative(Component):
|
||||||
js_file = "calendar.js"
|
js_file = "calendar.js"
|
||||||
|
|
||||||
# This component takes one parameter, a date string to show in the template
|
# This component takes one parameter, a date string to show in the template
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
date: str
|
date: str
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Any, NamedTuple
|
from typing import Any
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ class CalendarNested(Component):
|
||||||
js_file = "calendar.js"
|
js_file = "calendar.js"
|
||||||
|
|
||||||
# This component takes one parameter, a date string to show in the template
|
# This component takes one parameter, a date string to show in the template
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
date: str
|
date: str
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ class CreateCommand(ComponentCommand):
|
||||||
js_file = "{js_filename}"
|
js_file = "{js_filename}"
|
||||||
css_file = "{css_filename}"
|
css_file = "{css_filename}"
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
param: str
|
param: str
|
||||||
|
|
||||||
class Defaults:
|
class Defaults:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# ruff: noqa: ARG002, N804, N805
|
# ruff: noqa: ARG002, N804, N805
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, is_dataclass
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
from types import MethodType
|
from types import MethodType
|
||||||
from typing import (
|
from typing import (
|
||||||
|
|
@ -78,7 +78,14 @@ from django_components.template import cache_component_template_file, prepare_co
|
||||||
from django_components.util.context import gen_context_processors_data, snapshot_context
|
from django_components.util.context import gen_context_processors_data, snapshot_context
|
||||||
from django_components.util.exception import component_error_message
|
from django_components.util.exception import component_error_message
|
||||||
from django_components.util.logger import trace_component_msg
|
from django_components.util.logger import trace_component_msg
|
||||||
from django_components.util.misc import default, gen_id, hash_comp_cls, is_generator, to_dict
|
from django_components.util.misc import (
|
||||||
|
convert_class_to_namedtuple,
|
||||||
|
default,
|
||||||
|
gen_id,
|
||||||
|
hash_comp_cls,
|
||||||
|
is_generator,
|
||||||
|
to_dict,
|
||||||
|
)
|
||||||
from django_components.util.template_tag import TagAttr
|
from django_components.util.template_tag import TagAttr
|
||||||
from django_components.util.weakref import cached_ref
|
from django_components.util.weakref import cached_ref
|
||||||
|
|
||||||
|
|
@ -311,7 +318,7 @@ class ComponentVars(NamedTuple):
|
||||||
|
|
||||||
@register("table")
|
@register("table")
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
page: int
|
page: int
|
||||||
per_page: int
|
per_page: int
|
||||||
|
|
||||||
|
|
@ -363,7 +370,7 @@ class ComponentVars(NamedTuple):
|
||||||
|
|
||||||
@register("table")
|
@register("table")
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
page: int
|
page: int
|
||||||
per_page: int
|
per_page: int
|
||||||
|
|
||||||
|
|
@ -415,7 +422,7 @@ class ComponentVars(NamedTuple):
|
||||||
|
|
||||||
@register("table")
|
@register("table")
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
footer: SlotInput
|
footer: SlotInput
|
||||||
|
|
||||||
template = '''
|
template = '''
|
||||||
|
|
@ -506,6 +513,34 @@ class ComponentMeta(ComponentMediaMeta):
|
||||||
attrs["template_file"] = attrs.pop("template_name")
|
attrs["template_file"] = attrs.pop("template_name")
|
||||||
attrs["template_name"] = ComponentTemplateNameDescriptor()
|
attrs["template_name"] = ComponentTemplateNameDescriptor()
|
||||||
|
|
||||||
|
# Allow to define data classes (`Args`, `Kwargs`, `Slots`, `TemplateData`, `JsData`, `CssData`)
|
||||||
|
# without explicitly subclassing anything. In which case we make them into a subclass of `NamedTuple`.
|
||||||
|
# In other words:
|
||||||
|
# ```py
|
||||||
|
# class MyTable(Component):
|
||||||
|
# class Kwargs(NamedTuple):
|
||||||
|
# ...
|
||||||
|
# ```
|
||||||
|
# Can be simplified to:
|
||||||
|
# ```py
|
||||||
|
# class MyTable(Component):
|
||||||
|
# class Kwargs:
|
||||||
|
# ...
|
||||||
|
# ```
|
||||||
|
for data_class_name in ["Args", "Kwargs", "Slots", "TemplateData", "JsData", "CssData"]:
|
||||||
|
data_class = attrs.get(data_class_name)
|
||||||
|
# Not a class
|
||||||
|
if data_class is None or not isinstance(data_class, type):
|
||||||
|
continue
|
||||||
|
# Is dataclass
|
||||||
|
if is_dataclass(data_class):
|
||||||
|
continue
|
||||||
|
# Has base class(es)
|
||||||
|
has_parents = data_class.__bases__ != (object,)
|
||||||
|
if has_parents:
|
||||||
|
continue
|
||||||
|
attrs[data_class_name] = convert_class_to_namedtuple(data_class)
|
||||||
|
|
||||||
cls = cast("Type[Component]", super().__new__(mcs, name, bases, attrs))
|
cls = cast("Type[Component]", super().__new__(mcs, name, bases, attrs))
|
||||||
|
|
||||||
# If the component defined `template_file`, then associate this Component class
|
# If the component defined `template_file`, then associate this Component class
|
||||||
|
|
@ -598,11 +633,10 @@ class Component(metaclass=ComponentMeta):
|
||||||
will be the instance of this class:
|
will be the instance of this class:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
color: str
|
color: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
@ -615,15 +649,6 @@ class Component(metaclass=ComponentMeta):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The constructor of this class MUST accept positional arguments:
|
|
||||||
|
|
||||||
```py
|
|
||||||
Args(*args)
|
|
||||||
```
|
|
||||||
|
|
||||||
As such, a good starting point is to set this field to a subclass of
|
|
||||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple).
|
|
||||||
|
|
||||||
Use `Args` to:
|
Use `Args` to:
|
||||||
|
|
||||||
- Validate the input at runtime.
|
- Validate the input at runtime.
|
||||||
|
|
@ -640,6 +665,20 @@ class Component(metaclass=ComponentMeta):
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you do not specify any bases, the `Args` class will be automatically
|
||||||
|
converted to a `NamedTuple`:
|
||||||
|
|
||||||
|
`class Args:` -> `class Args(NamedTuple):`
|
||||||
|
|
||||||
|
If you explicitly set bases, the constructor of this class MUST accept positional arguments:
|
||||||
|
|
||||||
|
```py
|
||||||
|
Args(*args)
|
||||||
|
```
|
||||||
|
|
||||||
|
As such, a good starting point is to set this field to a subclass of
|
||||||
|
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple).
|
||||||
|
|
||||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -654,11 +693,10 @@ class Component(metaclass=ComponentMeta):
|
||||||
will be the instance of this class:
|
will be the instance of this class:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
color: str
|
color: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
@ -671,16 +709,6 @@ class Component(metaclass=ComponentMeta):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The constructor of this class MUST accept keyword arguments:
|
|
||||||
|
|
||||||
```py
|
|
||||||
Kwargs(**kwargs)
|
|
||||||
```
|
|
||||||
|
|
||||||
As such, a good starting point is to set this field to a subclass of
|
|
||||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
|
||||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
|
||||||
|
|
||||||
Use `Kwargs` to:
|
Use `Kwargs` to:
|
||||||
|
|
||||||
- Validate the input at runtime.
|
- Validate the input at runtime.
|
||||||
|
|
@ -697,6 +725,21 @@ class Component(metaclass=ComponentMeta):
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you do not specify any bases, the `Kwargs` class will be automatically
|
||||||
|
converted to a `NamedTuple`:
|
||||||
|
|
||||||
|
`class Kwargs:` -> `class Kwargs(NamedTuple):`
|
||||||
|
|
||||||
|
If you explicitly set bases, the constructor of this class MUST accept keyword arguments:
|
||||||
|
|
||||||
|
```py
|
||||||
|
Kwargs(**kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
As such, a good starting point is to set this field to a subclass of
|
||||||
|
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||||
|
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||||
|
|
||||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -711,11 +754,10 @@ class Component(metaclass=ComponentMeta):
|
||||||
will be the instance of this class:
|
will be the instance of this class:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component, Slot, SlotInput
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
header: SlotInput
|
header: SlotInput
|
||||||
footer: Slot
|
footer: Slot
|
||||||
|
|
||||||
|
|
@ -728,16 +770,6 @@ class Component(metaclass=ComponentMeta):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The constructor of this class MUST accept keyword arguments:
|
|
||||||
|
|
||||||
```py
|
|
||||||
Slots(**slots)
|
|
||||||
```
|
|
||||||
|
|
||||||
As such, a good starting point is to set this field to a subclass of
|
|
||||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
|
||||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
|
||||||
|
|
||||||
Use `Slots` to:
|
Use `Slots` to:
|
||||||
|
|
||||||
- Validate the input at runtime.
|
- Validate the input at runtime.
|
||||||
|
|
@ -757,6 +789,21 @@ class Component(metaclass=ComponentMeta):
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you do not specify any bases, the `Slots` class will be automatically
|
||||||
|
converted to a `NamedTuple`:
|
||||||
|
|
||||||
|
`class Slots:` -> `class Slots(NamedTuple):`
|
||||||
|
|
||||||
|
If you explicitly set bases, the constructor of this class MUST accept keyword arguments:
|
||||||
|
|
||||||
|
```py
|
||||||
|
Slots(**slots)
|
||||||
|
```
|
||||||
|
|
||||||
|
As such, a good starting point is to set this field to a subclass of
|
||||||
|
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||||
|
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||||
|
|
||||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
|
|
@ -1081,18 +1128,17 @@ class Component(metaclass=ComponentMeta):
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple
|
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django_components import Component, SlotInput
|
from django_components import Component, SlotInput
|
||||||
|
|
||||||
class MyComponent(Component):
|
class MyComponent(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
footer: SlotInput
|
footer: SlotInput
|
||||||
|
|
||||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
|
@ -1123,7 +1169,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class MyComponent(Component):
|
class MyComponent(Component):
|
||||||
class TemplateData(NamedTuple):
|
class TemplateData:
|
||||||
color: str
|
color: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
@ -1156,22 +1202,22 @@ class Component(metaclass=ComponentMeta):
|
||||||
If set and not `None`, then this class will be instantiated with the dictionary returned from
|
If set and not `None`, then this class will be instantiated with the dictionary returned from
|
||||||
[`get_template_data()`](../api#django_components.Component.get_template_data) to validate the data.
|
[`get_template_data()`](../api#django_components.Component.get_template_data) to validate the data.
|
||||||
|
|
||||||
The constructor of this class MUST accept keyword arguments:
|
Use `TemplateData` to:
|
||||||
|
|
||||||
```py
|
- Validate the data returned from
|
||||||
TemplateData(**template_data)
|
[`get_template_data()`](../api#django_components.Component.get_template_data) at runtime.
|
||||||
```
|
- Set type hints for this data.
|
||||||
|
- Document the component data.
|
||||||
|
|
||||||
You can also return an instance of `TemplateData` directly from
|
You can also return an instance of `TemplateData` directly from
|
||||||
[`get_template_data()`](../api#django_components.Component.get_template_data)
|
[`get_template_data()`](../api#django_components.Component.get_template_data)
|
||||||
to get type hints:
|
to get type hints:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class TemplateData(NamedTuple):
|
class TemplateData:
|
||||||
color: str
|
color: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
@ -1182,17 +1228,16 @@ class Component(metaclass=ComponentMeta):
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The constructor of this class MUST accept keyword arguments:
|
||||||
|
|
||||||
|
```py
|
||||||
|
TemplateData(**template_data)
|
||||||
|
```
|
||||||
|
|
||||||
A good starting point is to set this field to a subclass of
|
A good starting point is to set this field to a subclass of
|
||||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||||
|
|
||||||
Use `TemplateData` to:
|
|
||||||
|
|
||||||
- Validate the data returned from
|
|
||||||
[`get_template_data()`](../api#django_components.Component.get_template_data) at runtime.
|
|
||||||
- Set type hints for this data.
|
|
||||||
- Document the component data.
|
|
||||||
|
|
||||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
|
|
@ -1376,13 +1421,13 @@ class Component(metaclass=ComponentMeta):
|
||||||
from django_components import Component, SlotInput
|
from django_components import Component, SlotInput
|
||||||
|
|
||||||
class MyComponent(Component):
|
class MyComponent(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
footer: SlotInput
|
footer: SlotInput
|
||||||
|
|
||||||
def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
|
@ -1413,7 +1458,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class MyComponent(Component):
|
class MyComponent(Component):
|
||||||
class JsData(NamedTuple):
|
class JsData:
|
||||||
color: str
|
color: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
@ -1439,22 +1484,22 @@ class Component(metaclass=ComponentMeta):
|
||||||
If set and not `None`, then this class will be instantiated with the dictionary returned from
|
If set and not `None`, then this class will be instantiated with the dictionary returned from
|
||||||
[`get_js_data()`](../api#django_components.Component.get_js_data) to validate the data.
|
[`get_js_data()`](../api#django_components.Component.get_js_data) to validate the data.
|
||||||
|
|
||||||
The constructor of this class MUST accept keyword arguments:
|
Use `JsData` to:
|
||||||
|
|
||||||
```py
|
- Validate the data returned from
|
||||||
JsData(**js_data)
|
[`get_js_data()`](../api#django_components.Component.get_js_data) at runtime.
|
||||||
```
|
- Set type hints for this data.
|
||||||
|
- Document the component data.
|
||||||
|
|
||||||
You can also return an instance of `JsData` directly from
|
You can also return an instance of `JsData` directly from
|
||||||
[`get_js_data()`](../api#django_components.Component.get_js_data)
|
[`get_js_data()`](../api#django_components.Component.get_js_data)
|
||||||
to get type hints:
|
to get type hints:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class JsData(NamedTuple):
|
class JsData(
|
||||||
color: str
|
color: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
@ -1465,17 +1510,16 @@ class Component(metaclass=ComponentMeta):
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The constructor of this class MUST accept keyword arguments:
|
||||||
|
|
||||||
|
```py
|
||||||
|
JsData(**js_data)
|
||||||
|
```
|
||||||
|
|
||||||
A good starting point is to set this field to a subclass of
|
A good starting point is to set this field to a subclass of
|
||||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||||
|
|
||||||
Use `JsData` to:
|
|
||||||
|
|
||||||
- Validate the data returned from
|
|
||||||
[`get_js_data()`](../api#django_components.Component.get_js_data) at runtime.
|
|
||||||
- Set type hints for this data.
|
|
||||||
- Document the component data.
|
|
||||||
|
|
||||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
|
|
@ -1664,18 +1708,17 @@ class Component(metaclass=ComponentMeta):
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple
|
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django_components import Component, SlotInput
|
from django_components import Component, SlotInput
|
||||||
|
|
||||||
class MyComponent(Component):
|
class MyComponent(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
footer: SlotInput
|
footer: SlotInput
|
||||||
|
|
||||||
def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
|
@ -1705,7 +1748,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class MyComponent(Component):
|
class MyComponent(Component):
|
||||||
class CssData(NamedTuple):
|
class CssData:
|
||||||
color: str
|
color: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
@ -1731,22 +1774,22 @@ class Component(metaclass=ComponentMeta):
|
||||||
If set and not `None`, then this class will be instantiated with the dictionary returned from
|
If set and not `None`, then this class will be instantiated with the dictionary returned from
|
||||||
[`get_css_data()`](../api#django_components.Component.get_css_data) to validate the data.
|
[`get_css_data()`](../api#django_components.Component.get_css_data) to validate the data.
|
||||||
|
|
||||||
The constructor of this class MUST accept keyword arguments:
|
Use `CssData` to:
|
||||||
|
|
||||||
```py
|
- Validate the data returned from
|
||||||
CssData(**css_data)
|
[`get_css_data()`](../api#django_components.Component.get_css_data) at runtime.
|
||||||
```
|
- Set type hints for this data.
|
||||||
|
- Document the component data.
|
||||||
|
|
||||||
You can also return an instance of `CssData` directly from
|
You can also return an instance of `CssData` directly from
|
||||||
[`get_css_data()`](../api#django_components.Component.get_css_data)
|
[`get_css_data()`](../api#django_components.Component.get_css_data)
|
||||||
to get type hints:
|
to get type hints:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NamedTuple
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class CssData(NamedTuple):
|
class CssData:
|
||||||
color: str
|
color: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
@ -1757,17 +1800,16 @@ class Component(metaclass=ComponentMeta):
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The constructor of this class MUST accept keyword arguments:
|
||||||
|
|
||||||
|
```py
|
||||||
|
CssData(**css_data)
|
||||||
|
```
|
||||||
|
|
||||||
A good starting point is to set this field to a subclass of
|
A good starting point is to set this field to a subclass of
|
||||||
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||||
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
|
||||||
|
|
||||||
Use `CssData` to:
|
|
||||||
|
|
||||||
- Validate the data returned from
|
|
||||||
[`get_css_data()`](../api#django_components.Component.get_css_data) at runtime.
|
|
||||||
- Set type hints for this data.
|
|
||||||
- Document the component data.
|
|
||||||
|
|
||||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
|
|
@ -2567,7 +2609,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
page: int
|
page: int
|
||||||
per_page: int
|
per_page: int
|
||||||
|
|
||||||
|
|
@ -2635,7 +2677,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
page: int
|
page: int
|
||||||
per_page: int
|
per_page: int
|
||||||
|
|
||||||
|
|
@ -2706,7 +2748,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
from django_components import Component, Slot, SlotInput
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
header: SlotInput
|
header: SlotInput
|
||||||
footer: SlotInput
|
footer: SlotInput
|
||||||
|
|
||||||
|
|
@ -3314,19 +3356,19 @@ class Component(metaclass=ComponentMeta):
|
||||||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NamedTuple, Optional
|
from typing import Optional
|
||||||
from django_components import Component, Slot, SlotInput
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
# Define the component with the types
|
# Define the component with the types
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
surname: str
|
surname: str
|
||||||
age: int
|
age: int
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
my_slot: Optional[SlotInput] = None
|
my_slot: Optional[SlotInput] = None
|
||||||
footer: SlotInput
|
footer: SlotInput
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import NamedTuple, Optional, cast
|
from typing import Optional, cast
|
||||||
|
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.template.exceptions import TemplateSyntaxError
|
from django.template.exceptions import TemplateSyntaxError
|
||||||
|
|
@ -115,10 +115,10 @@ class ErrorFallback(Component):
|
||||||
Remember to define the `content` slot as function, so it's evaluated from inside of `ErrorFallback`.
|
Remember to define the `content` slot as function, so it's evaluated from inside of `ErrorFallback`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
fallback: Optional[str] = None
|
fallback: Optional[str] = None
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
default: Optional[SlotInput] = None
|
default: Optional[SlotInput] = None
|
||||||
content: Optional[SlotInput] = None
|
content: Optional[SlotInput] = None
|
||||||
fallback: Optional[SlotInput] = None
|
fallback: Optional[SlotInput] = None
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import sys
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
|
@ -14,7 +15,7 @@ from typing import (
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
from weakref import ref
|
from weakref import ReferenceType, ref
|
||||||
|
|
||||||
import django.urls
|
import django.urls
|
||||||
from django.template import Context, Origin, Template
|
from django.template import Context, Origin, Template
|
||||||
|
|
@ -33,6 +34,13 @@ if TYPE_CHECKING:
|
||||||
from django_components.slots import Slot, SlotNode, SlotResult
|
from django_components.slots import Slot, SlotNode, SlotResult
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
ComponentInstanceRef = ReferenceType["Component"]
|
||||||
|
else:
|
||||||
|
ComponentInstanceRef = ReferenceType
|
||||||
|
|
||||||
|
|
||||||
TCallable = TypeVar("TCallable", bound=Callable)
|
TCallable = TypeVar("TCallable", bound=Callable)
|
||||||
TClass = TypeVar("TClass", bound=Type[Any])
|
TClass = TypeVar("TClass", bound=Type[Any])
|
||||||
|
|
||||||
|
|
@ -265,16 +273,27 @@ class ExtensionComponentConfig:
|
||||||
|
|
||||||
This attribute holds the owner [`Component`](./api.md#django_components.Component) instance
|
This attribute holds the owner [`Component`](./api.md#django_components.Component) instance
|
||||||
that this extension is defined on.
|
that this extension is defined on.
|
||||||
|
|
||||||
|
Some extensions like Storybook run outside of the component lifecycle,
|
||||||
|
so there is no component instance available when running extension's methods.
|
||||||
|
In such cases, this attribute will be `None`.
|
||||||
"""
|
"""
|
||||||
|
component: Optional[Component] = None
|
||||||
|
if self._component_ref is not None:
|
||||||
component = self._component_ref()
|
component = self._component_ref()
|
||||||
if component is None:
|
if component is None:
|
||||||
raise RuntimeError("Component has been garbage collected")
|
raise RuntimeError("Component has been garbage collected")
|
||||||
return component
|
return component
|
||||||
|
|
||||||
def __init__(self, component: "Component") -> None:
|
def __init__(self, component: "Optional[Component]") -> None:
|
||||||
# NOTE: Use weak reference to avoid a circular reference between the component instance
|
# NOTE: Use weak reference to avoid a circular reference between the component instance
|
||||||
# and the extension class.
|
# and the extension class.
|
||||||
self._component_ref = ref(component)
|
if component is not None:
|
||||||
|
self._component_ref: Optional[ComponentInstanceRef] = ref(component)
|
||||||
|
else:
|
||||||
|
# NOTE: Some extensions like Storybook run outside of the component lifecycle,
|
||||||
|
# so there is no component instance available when running extension's methods.
|
||||||
|
self._component_ref = None
|
||||||
|
|
||||||
|
|
||||||
# TODO_v1 - Delete
|
# TODO_v1 - Delete
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ class DependenciesExtension(ComponentExtension):
|
||||||
|
|
||||||
# Cache the component's JS and CSS scripts when the class is created, so that
|
# Cache the component's JS and CSS scripts when the class is created, so that
|
||||||
# components' JS/CSS files are accessible even before having to render the component first.
|
# components' JS/CSS files are accessible even before having to render the component first.
|
||||||
|
#
|
||||||
|
# This is important for the scenario when the web server may restart in a middle of user
|
||||||
|
# session. In which case, if we did not cache the JS/CSS, then user may fail to retrieve
|
||||||
|
# JS/CSS of some component.
|
||||||
|
#
|
||||||
|
# Component JS/CSS is then also cached after each time a component is rendered.
|
||||||
|
# That way, if the component JS/CSS cache is smaller than the total number of
|
||||||
|
# components/assets, we add back the most-recent entries.
|
||||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||||
cache_component_js(ctx.component_cls, force=True)
|
cache_component_js(ctx.component_cls, force=True)
|
||||||
cache_component_css(ctx.component_cls, force=True)
|
cache_component_css(ctx.component_cls, force=True)
|
||||||
|
|
|
||||||
|
|
@ -394,7 +394,6 @@ slot content function.
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NamedTuple
|
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
from django_components import Component, SlotInput
|
from django_components import Component, SlotInput
|
||||||
|
|
||||||
|
|
@ -402,7 +401,7 @@ class TableFooterSlotData(TypedDict):
|
||||||
page_number: int
|
page_number: int
|
||||||
|
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
header: SlotInput
|
header: SlotInput
|
||||||
footer: SlotInput[TableFooterSlotData]
|
footer: SlotInput[TableFooterSlotData]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from collections import namedtuple
|
||||||
from dataclasses import asdict, is_dataclass
|
from dataclasses import asdict, is_dataclass
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from inspect import getmembers
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import (
|
from typing import (
|
||||||
|
|
@ -267,3 +269,56 @@ def format_as_ascii_table(
|
||||||
def is_generator(obj: Any) -> bool:
|
def is_generator(obj: Any) -> bool:
|
||||||
"""Check if an object is a generator with send method."""
|
"""Check if an object is a generator with send method."""
|
||||||
return hasattr(obj, "send")
|
return hasattr(obj, "send")
|
||||||
|
|
||||||
|
|
||||||
|
def convert_class_to_namedtuple(cls: Type[Any]) -> Type[Tuple[Any, ...]]:
|
||||||
|
# Construct fields for a NamedTuple. Unfortunately one can't further subclass the subclass of `NamedTuple`,
|
||||||
|
# so we need to construct a new class with the same fields.
|
||||||
|
# NamedTuple has:
|
||||||
|
# - Required fields, which are defined without values (annotations only)
|
||||||
|
# - Optional fields with defaults
|
||||||
|
# ```py
|
||||||
|
# class Z:
|
||||||
|
# b: str # Required, annotated
|
||||||
|
# a: int = None # Optional, annotated
|
||||||
|
# c = 1 # NOT A FIELD! Class var!
|
||||||
|
# ```
|
||||||
|
# Annotations are stored in `X.__annotations__`, while the defaults are regular class attributes
|
||||||
|
# NOTE: We ignore dunder methods
|
||||||
|
# NOTE 2: All fields with default values must come after fields without defaults.
|
||||||
|
field_names = list(cls.__annotations__.keys())
|
||||||
|
|
||||||
|
# Get default values from the original class and set them on the new NamedTuple class
|
||||||
|
field_names_set = set(field_names)
|
||||||
|
defaults = {}
|
||||||
|
class_attrs = {}
|
||||||
|
for name, value in getmembers(cls):
|
||||||
|
if name.startswith("__"):
|
||||||
|
continue
|
||||||
|
# Field default
|
||||||
|
if name in field_names_set:
|
||||||
|
defaults[name] = value
|
||||||
|
else:
|
||||||
|
# Class attribute
|
||||||
|
class_attrs[name] = value
|
||||||
|
|
||||||
|
# Figure out how many tuple fields have defaults. We need to know this
|
||||||
|
# because NamedTuple functional syntax uses the pattern where defaults
|
||||||
|
# are applied from the end.
|
||||||
|
# Final call then looks like this:
|
||||||
|
# `namedtuple("MyClass", ["a", "b", "c", "d"], defaults=[3, 4])`
|
||||||
|
# with defaults c=3 and d=4
|
||||||
|
num_fields_with_defaults = len(defaults)
|
||||||
|
if num_fields_with_defaults:
|
||||||
|
defaults_list = [defaults[name] for name in field_names[-num_fields_with_defaults:]]
|
||||||
|
else:
|
||||||
|
defaults_list = []
|
||||||
|
tuple_cls = namedtuple(cls.__name__, field_names, defaults=defaults_list) # type: ignore[misc] # noqa: PYI024
|
||||||
|
|
||||||
|
# `collections.namedtuple` doesn't allow to specify annotations, so we pass them afterwards
|
||||||
|
tuple_cls.__annotations__ = cls.__annotations__
|
||||||
|
# Likewise, `collections.namedtuple` doesn't allow to specify class vars
|
||||||
|
for name, value in class_attrs.items():
|
||||||
|
setattr(tuple_cls, name, value)
|
||||||
|
|
||||||
|
return tuple_cls
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import Any, List, Literal, NamedTuple, Optional
|
from typing import Any, List, Literal, Optional
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -591,23 +591,23 @@ class TestComponentRenderAPI:
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
template = ""
|
template = ""
|
||||||
|
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
variable: int
|
variable: int
|
||||||
another: str
|
another: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
variable: str
|
variable: str
|
||||||
another: int
|
another: int
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
my_slot: SlotInput
|
my_slot: SlotInput
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
nonlocal called
|
nonlocal called
|
||||||
called = True
|
called = True
|
||||||
|
|
||||||
assert self.args == TestComponent.Args(123, "str")
|
assert self.args == TestComponent.Args(123, "str") # type: ignore[call-arg]
|
||||||
assert self.kwargs == TestComponent.Kwargs(variable="test", another=1)
|
assert self.kwargs == TestComponent.Kwargs(variable="test", another=1) # type: ignore[call-arg]
|
||||||
assert isinstance(self.slots, TestComponent.Slots)
|
assert isinstance(self.slots, TestComponent.Slots)
|
||||||
assert isinstance(self.slots.my_slot, Slot)
|
assert isinstance(self.slots.my_slot, Slot)
|
||||||
assert self.slots.my_slot() == "MY_SLOT"
|
assert self.slots.my_slot() == "MY_SLOT"
|
||||||
|
|
@ -789,15 +789,15 @@ class TestComponentTemplateVars:
|
||||||
|
|
||||||
def test_args_kwargs_slots__simple_typed(self):
|
def test_args_kwargs_slots__simple_typed(self):
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
variable: int
|
variable: int
|
||||||
another: str
|
another: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
variable: str
|
variable: str
|
||||||
another: int
|
another: int
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
my_slot: SlotInput
|
my_slot: SlotInput
|
||||||
|
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
|
|
@ -898,15 +898,15 @@ class TestComponentTemplateVars:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
variable: int
|
variable: int
|
||||||
another: str
|
another: str
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
variable: str
|
variable: str
|
||||||
another: int
|
another: int
|
||||||
|
|
||||||
class Slots(NamedTuple):
|
class Slots:
|
||||||
my_slot: SlotInput
|
my_slot: SlotInput
|
||||||
|
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import re
|
import re
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
|
|
@ -21,7 +20,7 @@ class TestDynamicComponent:
|
||||||
Variable: <strong>{{ variable }}</strong>
|
Variable: <strong>{{ variable }}</strong>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
variable: str
|
variable: str
|
||||||
variable2: str
|
variable2: str
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.template.exceptions import TemplateSyntaxError
|
from django.template.exceptions import TemplateSyntaxError
|
||||||
|
|
@ -313,7 +311,7 @@ class TestErrorFallbackComponent:
|
||||||
def test_error_fallback_nested_inside_another(self, components_settings):
|
def test_error_fallback_nested_inside_another(self, components_settings):
|
||||||
@register("broken")
|
@register("broken")
|
||||||
class BrokenComponent(Component):
|
class BrokenComponent(Component):
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
msg: str
|
msg: str
|
||||||
|
|
||||||
def on_render(self, context: Context, template: Template):
|
def on_render(self, context: Context, template: Template):
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ setup_test_config()
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestComponentTyping:
|
class TestComponentTyping:
|
||||||
def test_data_methods_input_typed(self):
|
def test_data_methods_input_typed_custom_classes(self):
|
||||||
template_called = False
|
template_called = False
|
||||||
js_called = False
|
js_called = False
|
||||||
css_called = False
|
css_called = False
|
||||||
|
|
@ -84,6 +84,74 @@ class TestComponentTyping:
|
||||||
assert js_called
|
assert js_called
|
||||||
assert css_called
|
assert css_called
|
||||||
|
|
||||||
|
def test_data_methods_input_typed_default_classes(self):
|
||||||
|
template_called = False
|
||||||
|
js_called = False
|
||||||
|
css_called = False
|
||||||
|
|
||||||
|
class ButtonFooterSlotData(TypedDict):
|
||||||
|
value: int
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
class Args:
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
class Kwargs:
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
maybe_var: Optional[int] = None
|
||||||
|
|
||||||
|
class Slots:
|
||||||
|
header: SlotInput
|
||||||
|
footer: Optional[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, Button.Slots)
|
||||||
|
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, Button.Slots)
|
||||||
|
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, Button.Slots)
|
||||||
|
assert isinstance(context, Context)
|
||||||
|
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
assert issubclass(Button.Args, tuple)
|
||||||
|
assert issubclass(Button.Kwargs, tuple)
|
||||||
|
assert issubclass(Button.Slots, tuple)
|
||||||
|
|
||||||
|
Button.render(
|
||||||
|
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||||
|
kwargs=Button.Kwargs(name="name", age=123), # type: ignore[call-arg]
|
||||||
|
slots=Button.Slots( # type: ignore[call-arg]
|
||||||
|
header="HEADER",
|
||||||
|
footer=Slot(lambda _ctx: "FOOTER"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert template_called
|
||||||
|
assert js_called
|
||||||
|
assert css_called
|
||||||
|
|
||||||
def test_data_methods_input_not_typed_by_default(self):
|
def test_data_methods_input_not_typed_by_default(self):
|
||||||
template_called = False
|
template_called = False
|
||||||
js_called = False
|
js_called = False
|
||||||
|
|
@ -132,7 +200,71 @@ class TestComponentTyping:
|
||||||
assert js_called
|
assert js_called
|
||||||
assert css_called
|
assert css_called
|
||||||
|
|
||||||
def test_data_methods_output_typed(self):
|
def test_data_methods_output_typed_default_classes(self):
|
||||||
|
template_called = False
|
||||||
|
js_called = False
|
||||||
|
css_called = False
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
class TemplateData:
|
||||||
|
data1: str
|
||||||
|
data2: int
|
||||||
|
|
||||||
|
class JsData:
|
||||||
|
js_data1: str
|
||||||
|
js_data2: int
|
||||||
|
|
||||||
|
class CssData:
|
||||||
|
css_data1: str
|
||||||
|
css_data2: int
|
||||||
|
|
||||||
|
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>"
|
||||||
|
|
||||||
|
assert issubclass(Button.TemplateData, tuple)
|
||||||
|
assert issubclass(Button.JsData, tuple)
|
||||||
|
assert issubclass(Button.CssData, tuple)
|
||||||
|
|
||||||
|
Button.render(
|
||||||
|
args=["arg1", "arg2"],
|
||||||
|
kwargs={"name": "name", "age": 123},
|
||||||
|
slots={
|
||||||
|
"header": "HEADER",
|
||||||
|
"footer": Slot(lambda _ctx: "FOOTER"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert template_called
|
||||||
|
assert js_called
|
||||||
|
assert css_called
|
||||||
|
|
||||||
|
def test_data_methods_output_typed_custom_classes(self):
|
||||||
template_called = False
|
template_called = False
|
||||||
js_called = False
|
js_called = False
|
||||||
css_called = False
|
css_called = False
|
||||||
|
|
@ -489,43 +621,57 @@ class TestComponentTyping:
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_custom_args_class_raises_on_invalid(self):
|
def test_custom_args_class_raises_on_invalid(self):
|
||||||
|
class Parent:
|
||||||
|
pass
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
template = "Hello"
|
template = "Hello"
|
||||||
|
|
||||||
class Args:
|
class Args(Parent):
|
||||||
arg1: str
|
arg1: str
|
||||||
arg2: str
|
arg2: str
|
||||||
|
|
||||||
|
assert issubclass(Button.Args, Parent)
|
||||||
|
assert not issubclass(Button.Args, tuple)
|
||||||
|
|
||||||
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
|
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
|
||||||
Button.render(
|
Button.render(
|
||||||
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||||
)
|
)
|
||||||
|
|
||||||
class Button2(Component):
|
class Parent2:
|
||||||
template = "Hello"
|
|
||||||
|
|
||||||
class Args:
|
|
||||||
arg1: str
|
|
||||||
arg2: str
|
|
||||||
|
|
||||||
def __init__(self, arg1: str, arg2: str):
|
def __init__(self, arg1: str, arg2: str):
|
||||||
self.arg1 = arg1
|
self.arg1 = arg1
|
||||||
self.arg2 = arg2
|
self.arg2 = arg2
|
||||||
|
|
||||||
|
class Button2(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Args(Parent2):
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
assert not issubclass(Button2.Args, tuple)
|
||||||
|
assert issubclass(Button2.Args, Parent2)
|
||||||
|
|
||||||
with pytest.raises(TypeError, match=re.escape("'Args' object is not iterable")):
|
with pytest.raises(TypeError, match=re.escape("'Args' object is not iterable")):
|
||||||
Button2.render(
|
Button2.render(
|
||||||
args=Button2.Args(arg1="arg1", arg2="arg2"),
|
args=Button2.Args(arg1="arg1", arg2="arg2"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Parent3:
|
||||||
|
def __iter__(self):
|
||||||
|
return iter([self.arg1, self.arg2]) # type: ignore[attr-defined]
|
||||||
|
|
||||||
class Button3(Component):
|
class Button3(Component):
|
||||||
template = "Hello"
|
template = "Hello"
|
||||||
|
|
||||||
class Args:
|
class Args(Parent3):
|
||||||
arg1: str
|
arg1: str
|
||||||
arg2: str
|
arg2: str
|
||||||
|
|
||||||
def __iter__(self):
|
assert not issubclass(Button3.Args, tuple)
|
||||||
return iter([self.arg1, self.arg2])
|
assert issubclass(Button3.Args, Parent3)
|
||||||
|
|
||||||
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
|
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
|
||||||
Button3.render(
|
Button3.render(
|
||||||
|
|
@ -533,13 +679,7 @@ class TestComponentTyping:
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_custom_args_class_custom(self):
|
def test_custom_args_class_custom(self):
|
||||||
class Button(Component):
|
class Parent:
|
||||||
template = "Hello"
|
|
||||||
|
|
||||||
class Args:
|
|
||||||
arg1: str
|
|
||||||
arg2: str
|
|
||||||
|
|
||||||
def __init__(self, arg1: str, arg2: str):
|
def __init__(self, arg1: str, arg2: str):
|
||||||
self.arg1 = arg1
|
self.arg1 = arg1
|
||||||
self.arg2 = arg2
|
self.arg2 = arg2
|
||||||
|
|
@ -547,48 +687,69 @@ class TestComponentTyping:
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter([self.arg1, self.arg2])
|
return iter([self.arg1, self.arg2])
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Args(Parent):
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
Button.render(
|
Button.render(
|
||||||
args=Button.Args(arg1="arg1", arg2="arg2"),
|
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_custom_kwargs_class_raises_on_invalid(self):
|
def test_custom_kwargs_class_raises_on_invalid(self):
|
||||||
|
class Parent:
|
||||||
|
pass
|
||||||
|
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
template = "Hello"
|
template = "Hello"
|
||||||
|
|
||||||
class Kwargs:
|
class Kwargs(Parent):
|
||||||
arg1: str
|
arg1: str
|
||||||
arg2: str
|
arg2: str
|
||||||
|
|
||||||
|
assert not issubclass(Button.Kwargs, tuple)
|
||||||
|
assert issubclass(Button.Kwargs, Parent)
|
||||||
|
|
||||||
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
|
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
|
||||||
Button.render(
|
Button.render(
|
||||||
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||||
)
|
)
|
||||||
|
|
||||||
class Button2(Component):
|
class Parent2:
|
||||||
template = "Hello"
|
|
||||||
|
|
||||||
class Kwargs:
|
|
||||||
arg1: str
|
|
||||||
arg2: str
|
|
||||||
|
|
||||||
def __init__(self, arg1: str, arg2: str):
|
def __init__(self, arg1: str, arg2: str):
|
||||||
self.arg1 = arg1
|
self.arg1 = arg1
|
||||||
self.arg2 = arg2
|
self.arg2 = arg2
|
||||||
|
|
||||||
|
class Button2(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Kwargs(Parent2):
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
assert not issubclass(Button2.Kwargs, tuple)
|
||||||
|
assert issubclass(Button2.Kwargs, Parent2)
|
||||||
|
|
||||||
with pytest.raises(TypeError, match=re.escape("'Kwargs' object is not iterable")):
|
with pytest.raises(TypeError, match=re.escape("'Kwargs' object is not iterable")):
|
||||||
Button2.render(
|
Button2.render(
|
||||||
kwargs=Button2.Kwargs(arg1="arg1", arg2="arg2"),
|
kwargs=Button2.Kwargs(arg1="arg1", arg2="arg2"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class Parent3:
|
||||||
|
def _asdict(self):
|
||||||
|
return {"arg1": self.arg1, "arg2": self.arg2} # type: ignore[attr-defined]
|
||||||
|
|
||||||
class Button3(Component):
|
class Button3(Component):
|
||||||
template = "Hello"
|
template = "Hello"
|
||||||
|
|
||||||
class Kwargs:
|
class Kwargs(Parent3):
|
||||||
arg1: str
|
arg1: str
|
||||||
arg2: str
|
arg2: str
|
||||||
|
|
||||||
def _asdict(self):
|
assert not issubclass(Button3.Kwargs, tuple)
|
||||||
return {"arg1": self.arg1, "arg2": self.arg2}
|
assert issubclass(Button3.Kwargs, Parent3)
|
||||||
|
|
||||||
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
|
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
|
||||||
Button3.render(
|
Button3.render(
|
||||||
|
|
@ -596,13 +757,7 @@ class TestComponentTyping:
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_custom_kwargs_class_custom(self):
|
def test_custom_kwargs_class_custom(self):
|
||||||
class Button(Component):
|
class Parent:
|
||||||
template = "Hello"
|
|
||||||
|
|
||||||
class Kwargs:
|
|
||||||
arg1: str
|
|
||||||
arg2: str
|
|
||||||
|
|
||||||
def __init__(self, arg1: str, arg2: str):
|
def __init__(self, arg1: str, arg2: str):
|
||||||
self.arg1 = arg1
|
self.arg1 = arg1
|
||||||
self.arg2 = arg2
|
self.arg2 = arg2
|
||||||
|
|
@ -610,6 +765,16 @@ class TestComponentTyping:
|
||||||
def _asdict(self):
|
def _asdict(self):
|
||||||
return {"arg1": self.arg1, "arg2": self.arg2}
|
return {"arg1": self.arg1, "arg2": self.arg2}
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Kwargs(Parent):
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
assert not issubclass(Button.Kwargs, tuple)
|
||||||
|
assert issubclass(Button.Kwargs, Parent)
|
||||||
|
|
||||||
Button.render(
|
Button.render(
|
||||||
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"),
|
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"),
|
||||||
)
|
)
|
||||||
|
|
@ -618,14 +783,14 @@ class TestComponentTyping:
|
||||||
class Button(Component):
|
class Button(Component):
|
||||||
template = "Hello"
|
template = "Hello"
|
||||||
|
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
class ButtonExtra(Button):
|
class ButtonExtra(Button):
|
||||||
class Args(NamedTuple):
|
class Args:
|
||||||
name: str
|
name: str
|
||||||
size: int
|
size: int
|
||||||
|
|
||||||
|
|
@ -635,6 +800,6 @@ class TestComponentTyping:
|
||||||
assert ButtonExtra.Kwargs is Button.Kwargs
|
assert ButtonExtra.Kwargs is Button.Kwargs
|
||||||
|
|
||||||
ButtonExtra.render(
|
ButtonExtra.render(
|
||||||
args=ButtonExtra.Args(name="John", size=30),
|
args=ButtonExtra.Args(name="John", size=30), # type: ignore[call-arg]
|
||||||
kwargs=ButtonExtra.Kwargs(color="red"),
|
kwargs=ButtonExtra.Kwargs(color="red"), # type: ignore[call-arg]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,11 @@ class TestExtensions:
|
||||||
del TestAccessComp
|
del TestAccessComp
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
|
@djc_test(components_settings={"extensions": [DummyExtension]})
|
||||||
|
def test_instantiate_ext_component_config_none(self):
|
||||||
|
config = DummyExtension.ComponentConfig(None)
|
||||||
|
assert isinstance(config, DummyExtension.ComponentConfig)
|
||||||
|
|
||||||
def test_raises_on_extension_name_conflict(self):
|
def test_raises_on_extension_name_conflict(self):
|
||||||
@djc_test(components_settings={"extensions": [RenderExtension]})
|
@djc_test(components_settings={"extensions": [RenderExtension]})
|
||||||
def inner():
|
def inner():
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ This file can be deleted after Django 5.2 reached end of life.
|
||||||
See https://github.com/django-components/django-components/issues/1323#issuecomment-3163478287.
|
See https://github.com/django-components/django-components/issues/1323#issuecomment-3163478287.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import NamedTuple
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
@ -53,7 +51,7 @@ class TestTemplatePartialsIntegration:
|
||||||
})()
|
})()
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
|
|
||||||
class Kwargs(NamedTuple):
|
class Kwargs:
|
||||||
date: str
|
date: str
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue