mirror of
https://github.com/django-components/django-components.git
synced 2025-11-04 08:48:22 +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
|
||||
|
||||
## 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
|
||||
|
||||
#### 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:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django.template import Context
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
size: int
|
||||
text: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
my_slot: Optional[SlotInput] = None
|
||||
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`.
|
||||
|
||||
```djc_py
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django_components import Component, SlotInput, register, types
|
||||
|
||||
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)
|
||||
class MyMenu(Component):
|
||||
# Define the types
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
size: int
|
||||
text: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
vertical: Optional[bool] = None
|
||||
klass: Optional[str] = None
|
||||
style: Optional[str] = None
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
default: Optional[SlotInput] = None
|
||||
|
||||
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,
|
||||
which points to the component instance.
|
||||
|
||||
This may be `None` if the methods does NOT run during the rendering.
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class MyExtension:
|
||||
def get(self, request):
|
||||
# `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
|
||||
|
|
|
|||
|
|
@ -289,14 +289,14 @@ To implement this, we render the fallback slot in [`on_render()`](../../../refer
|
|||
and return it if an error occured:
|
||||
|
||||
```djc_py
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
|
||||
from django.template import Context, Template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_components import Component, OnRenderGenerator, SlotInput, types
|
||||
|
||||
class ErrorFallback(Component):
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
default: Optional[SlotInput] = None
|
||||
fallback: Optional[SlotInput] = None
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ and so `selected_items` will be set to `[1, 2, 3]`.
|
|||
|
||||
```py
|
||||
class ProfileCard(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
show_details: bool = True
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Each method handles the data independently - you can define different data for t
|
|||
|
||||
```python
|
||||
class ProfileCard(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
user_id: int
|
||||
show_details: bool
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ If [`get_template_data()`](../../../reference/api/#django_components.Component.g
|
|||
class ProfileCard(Component):
|
||||
template_file = "profile_card.html"
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
user_id: int
|
||||
show_details: bool
|
||||
|
||||
|
|
@ -182,10 +182,10 @@ class ProfileCard(Component):
|
|||
|
||||
```py
|
||||
class ProfileCard(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
user_id: int
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
show_details: bool
|
||||
|
||||
# Access inputs directly as parameters
|
||||
|
|
@ -228,10 +228,10 @@ class ProfileCard(Component):
|
|||
|
||||
```py
|
||||
class ProfileCard(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
user_id: int
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
show_details: bool
|
||||
|
||||
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")
|
||||
class ProfileCard(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
show_details: bool
|
||||
|
||||
class Defaults:
|
||||
|
|
@ -344,7 +344,7 @@ class ProfileCard(Component):
|
|||
|
||||
```py
|
||||
class ProfileCard(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
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).
|
||||
|
||||
```python
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
surname: str
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
my_slot: Optional[SlotInput] = None
|
||||
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.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Button(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
class TemplateData(
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
class JsData(NamedTuple):
|
||||
class JsData:
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
class CssData(NamedTuple):
|
||||
class CssData:
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ With `Args` class:
|
|||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ With `Kwargs` class:
|
|||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
|
@ -182,7 +182,7 @@ With `Slots` class:
|
|||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
class Table(Component):
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
header: 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"`:
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
from django_components import Component, register
|
||||
|
||||
@register("button")
|
||||
class Button(Component):
|
||||
template_file = "button.html"
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
name: 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.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class Button(Component):
|
||||
template_file = "button.html"
|
||||
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
surname: str
|
||||
age: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
footer: Optional[SlotInput] = None
|
||||
|
||||
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.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class Button(Component):
|
||||
template_file = "button.html"
|
||||
|
||||
class Args(NamedTuple):
|
||||
class Args(
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
surname: str
|
||||
age: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
footer: Optional[SlotInput] = None
|
||||
|
||||
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).
|
||||
|
||||
```python
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
# Define the component with the types
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args(
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
surname: str
|
||||
age: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
my_slot: Optional[SlotInput] = None
|
||||
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.
|
||||
|
||||
```py
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django.template import Context
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
size: int
|
||||
text: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
variable: str
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
my_slot: Optional[SlotInput] = None
|
||||
|
||||
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),
|
||||
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_js_data()`](../../../reference/api#django_components.Component.get_js_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.
|
||||
|
||||
```python
|
||||
from typing_extensions import NamedTuple, TypedDict
|
||||
from typing_extensions import TypedDict
|
||||
from django.template import Context
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
|
|
@ -90,15 +90,15 @@ class ButtonFooterSlotData(TypedDict):
|
|||
|
||||
# Define the component with the types
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
surname: str
|
||||
age: int
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
||||
# plain string, or a function that returns a string.
|
||||
my_slot: Optional[SlotInput] = None
|
||||
|
|
@ -143,7 +143,7 @@ class Button(Component):
|
|||
Args = None
|
||||
Slots = None
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
name: str
|
||||
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.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Button(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
class TemplateData:
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
class JsData(NamedTuple):
|
||||
class JsData:
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
class CssData(NamedTuple):
|
||||
class CssData:
|
||||
css_data1: str
|
||||
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.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Button(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
class TemplateData:
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
class JsData(NamedTuple):
|
||||
class JsData:
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
class CssData(NamedTuple):
|
||||
class CssData:
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
|
|
@ -270,34 +268,63 @@ class Button(Component):
|
|||
|
||||
## Custom types
|
||||
|
||||
We recommend to use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
for the `Args` class, and [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple),
|
||||
So far, we've defined the input classes like `Kwargs` as simple classes.
|
||||
|
||||
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),
|
||||
or [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
|
||||
for `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData` classes.
|
||||
|
||||
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
|
||||
|
||||
The [`Args`](../../../reference/api#django_components.Component.Args) class
|
||||
|
|
@ -390,7 +417,7 @@ As a workaround:
|
|||
|
||||
```py
|
||||
class Table(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
args: List[str]
|
||||
|
||||
Table.render(
|
||||
|
|
@ -402,7 +429,7 @@ As a workaround:
|
|||
|
||||
```py
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
variable: str
|
||||
another: int
|
||||
# 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:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
pass
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
pass
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
pass
|
||||
```
|
||||
|
||||
|
|
@ -459,14 +485,14 @@ In the example below, `ButtonExtra` inherits `Kwargs` from `Button`, but overrid
|
|||
from django_components import Component, Empty
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
size: int
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
color: str
|
||||
|
||||
class ButtonExtra(Button):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
name: str
|
||||
size: int
|
||||
|
||||
|
|
@ -491,10 +517,10 @@ Compare the following:
|
|||
|
||||
```py
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
size: int
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
color: str
|
||||
|
||||
# Both `Args` and `Kwargs` are defined on the class
|
||||
|
|
@ -624,23 +650,23 @@ The steps to migrate are:
|
|||
Thus, the code above will become:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django.template import Context
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
# The Component class does not take any generics
|
||||
class Button(Component):
|
||||
# Types are now defined inside the component class
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
size: int
|
||||
text: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
||||
# plain string, or a function that returns a string.
|
||||
my_slot: Optional[SlotInput] = None
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# ruff: noqa: S311
|
||||
import random
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
|
||||
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")
|
||||
class OfferCardOld(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
savings_percent: int
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
|
|
@ -45,7 +45,7 @@ class OfferCardNew(OfferCardOld):
|
|||
|
||||
@register("offer_card")
|
||||
class OfferCard(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
savings_percent: int
|
||||
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
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ error_rate = {
|
|||
|
||||
@register("api_widget")
|
||||
class ApiWidget(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
simulate_error: bool = False
|
||||
|
||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# ruff: noqa: S311
|
||||
import random
|
||||
from typing import NamedTuple
|
||||
|
||||
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")
|
||||
class WeatherWidget(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
location: str
|
||||
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
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ DESCRIPTION = "Form that automatically arranges fields in a grid and generates l
|
|||
class FormGrid(Component):
|
||||
"""Form that automatically arranges fields in a grid and generates labels."""
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
editable: bool = True
|
||||
method: str = "post"
|
||||
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
|
||||
# We will create the label for the field automatically
|
||||
label = FormGridLabel.render(
|
||||
kwargs=FormGridLabel.Kwargs(field_name=field_name),
|
||||
kwargs=FormGridLabel.Kwargs(field_name=field_name), # type: ignore[call-arg]
|
||||
deps_strategy="ignore",
|
||||
)
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ class FormGridLabel(Component):
|
|||
</label>
|
||||
"""
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
field_name: str
|
||||
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")
|
||||
class ThankYouMessage(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
name: str
|
||||
|
||||
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
|
||||
|
||||
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):
|
||||
"""A simple fragment with JS and CSS."""
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
type: str
|
||||
|
||||
template: types.django_html = """
|
||||
|
|
@ -37,7 +35,7 @@ class SimpleFragment(Component):
|
|||
class AlpineFragment(Component):
|
||||
"""A fragment that defines an AlpineJS component."""
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
type: str
|
||||
|
||||
# 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
|
||||
|
||||
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")
|
||||
class Recursion(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
current_depth: int = 0
|
||||
|
||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ class Tablist(Component):
|
|||
{% endprovide %}
|
||||
"""
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
id: Optional[str] = None
|
||||
name: str = "Tabs"
|
||||
selected_tab: Optional[str] = None
|
||||
|
|
@ -341,7 +341,7 @@ class Tab(Component):
|
|||
{% endprovide %}
|
||||
"""
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
header: str
|
||||
disabled: bool = False
|
||||
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:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django.template import Context
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
size: int
|
||||
text: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
my_slot: Optional[SlotInput] = None
|
||||
another_slot: SlotInput
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from typing import NamedTuple
|
||||
|
||||
from django_components import Component, register
|
||||
|
||||
|
||||
|
|
@ -15,7 +13,7 @@ class Calendar(Component):
|
|||
js_file = "calendar/calendar.js"
|
||||
|
||||
# This component takes one parameter, a date string to show in the template
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
date: str
|
||||
|
||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||
|
|
@ -46,7 +44,7 @@ class CalendarRelative(Component):
|
|||
js_file = "calendar.js"
|
||||
|
||||
# This component takes one parameter, a date string to show in the template
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
date: str
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ class CalendarNested(Component):
|
|||
js_file = "calendar.js"
|
||||
|
||||
# This component takes one parameter, a date string to show in the template
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
date: str
|
||||
|
||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ class CreateCommand(ComponentCommand):
|
|||
js_file = "{js_filename}"
|
||||
css_file = "{css_filename}"
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
param: str
|
||||
|
||||
class Defaults:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# ruff: noqa: ARG002, N804, N805
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from inspect import signature
|
||||
from types import MethodType
|
||||
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.exception import component_error_message
|
||||
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.weakref import cached_ref
|
||||
|
||||
|
|
@ -311,7 +318,7 @@ class ComponentVars(NamedTuple):
|
|||
|
||||
@register("table")
|
||||
class Table(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
|
@ -363,7 +370,7 @@ class ComponentVars(NamedTuple):
|
|||
|
||||
@register("table")
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
|
@ -415,7 +422,7 @@ class ComponentVars(NamedTuple):
|
|||
|
||||
@register("table")
|
||||
class Table(Component):
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
footer: SlotInput
|
||||
|
||||
template = '''
|
||||
|
|
@ -506,6 +513,34 @@ class ComponentMeta(ComponentMediaMeta):
|
|||
attrs["template_file"] = attrs.pop("template_name")
|
||||
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))
|
||||
|
||||
# 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:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
color: str
|
||||
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:
|
||||
|
||||
- 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).
|
||||
"""
|
||||
|
||||
|
|
@ -654,11 +693,10 @@ class Component(metaclass=ComponentMeta):
|
|||
will be the instance of this class:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
color: str
|
||||
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:
|
||||
|
||||
- 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).
|
||||
"""
|
||||
|
||||
|
|
@ -711,11 +754,10 @@ class Component(metaclass=ComponentMeta):
|
|||
will be the instance of this class:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
class Table(Component):
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
header: SlotInput
|
||||
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:
|
||||
|
||||
- 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).
|
||||
|
||||
!!! info
|
||||
|
|
@ -1081,18 +1128,17 @@ class Component(metaclass=ComponentMeta):
|
|||
**Example:**
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django.template import Context
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class MyComponent(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
color: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
size: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
footer: SlotInput
|
||||
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
|
|
@ -1123,7 +1169,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
class TemplateData:
|
||||
color: str
|
||||
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
|
||||
[`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
|
||||
TemplateData(**template_data)
|
||||
```
|
||||
- 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.
|
||||
|
||||
You can also return an instance of `TemplateData` directly from
|
||||
[`get_template_data()`](../api#django_components.Component.get_template_data)
|
||||
to get type hints:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
class TemplateData:
|
||||
color: str
|
||||
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
|
||||
[`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 `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).
|
||||
|
||||
!!! info
|
||||
|
|
@ -1376,13 +1421,13 @@ class Component(metaclass=ComponentMeta):
|
|||
from django_components import Component, SlotInput
|
||||
|
||||
class MyComponent(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
color: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
size: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
footer: SlotInput
|
||||
|
||||
def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
|
|
@ -1413,7 +1458,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class JsData(NamedTuple):
|
||||
class JsData:
|
||||
color: str
|
||||
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
|
||||
[`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
|
||||
JsData(**js_data)
|
||||
```
|
||||
- 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.
|
||||
|
||||
You can also return an instance of `JsData` directly from
|
||||
[`get_js_data()`](../api#django_components.Component.get_js_data)
|
||||
to get type hints:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class JsData(NamedTuple):
|
||||
class JsData(
|
||||
color: str
|
||||
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
|
||||
[`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 `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).
|
||||
|
||||
!!! info
|
||||
|
|
@ -1664,18 +1708,17 @@ class Component(metaclass=ComponentMeta):
|
|||
**Example:**
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django.template import Context
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class MyComponent(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
color: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
size: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
footer: SlotInput
|
||||
|
||||
def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
|
|
@ -1705,7 +1748,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class CssData(NamedTuple):
|
||||
class CssData:
|
||||
color: str
|
||||
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
|
||||
[`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
|
||||
CssData(**css_data)
|
||||
```
|
||||
- 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.
|
||||
|
||||
You can also return an instance of `CssData` directly from
|
||||
[`get_css_data()`](../api#django_components.Component.get_css_data)
|
||||
to get type hints:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class CssData(NamedTuple):
|
||||
class CssData:
|
||||
color: str
|
||||
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
|
||||
[`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 `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).
|
||||
|
||||
!!! info
|
||||
|
|
@ -2567,7 +2609,7 @@ class Component(metaclass=ComponentMeta):
|
|||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
|
@ -2635,7 +2677,7 @@ class Component(metaclass=ComponentMeta):
|
|||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
|
|
@ -2706,7 +2748,7 @@ class Component(metaclass=ComponentMeta):
|
|||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
class Table(Component):
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
header: SlotInput
|
||||
footer: SlotInput
|
||||
|
||||
|
|
@ -3314,19 +3356,19 @@ class Component(metaclass=ComponentMeta):
|
|||
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||
|
||||
```python
|
||||
from typing import NamedTuple, Optional
|
||||
from typing import Optional
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
# Define the component with the types
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
surname: str
|
||||
age: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
my_slot: Optional[SlotInput] = None
|
||||
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.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`.
|
||||
"""
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
fallback: Optional[str] = None
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
default: Optional[SlotInput] = None
|
||||
content: Optional[SlotInput] = None
|
||||
fallback: Optional[SlotInput] = None
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import sys
|
||||
from functools import wraps
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
|
|
@ -14,7 +15,7 @@ from typing import (
|
|||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
from weakref import ref
|
||||
from weakref import ReferenceType, ref
|
||||
|
||||
import django.urls
|
||||
from django.template import Context, Origin, Template
|
||||
|
|
@ -33,6 +34,13 @@ if TYPE_CHECKING:
|
|||
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)
|
||||
TClass = TypeVar("TClass", bound=Type[Any])
|
||||
|
||||
|
|
@ -265,16 +273,27 @@ class ExtensionComponentConfig:
|
|||
|
||||
This attribute holds the owner [`Component`](./api.md#django_components.Component) instance
|
||||
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 = self._component_ref()
|
||||
component: Optional[Component] = None
|
||||
if self._component_ref is not None:
|
||||
component = self._component_ref()
|
||||
if component is None:
|
||||
raise RuntimeError("Component has been garbage collected")
|
||||
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
|
||||
# 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
|
||||
|
|
|
|||
|
|
@ -16,6 +16,14 @@ class DependenciesExtension(ComponentExtension):
|
|||
|
||||
# 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.
|
||||
#
|
||||
# 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:
|
||||
cache_component_js(ctx.component_cls, force=True)
|
||||
cache_component_css(ctx.component_cls, force=True)
|
||||
|
|
|
|||
|
|
@ -394,7 +394,6 @@ slot content function.
|
|||
**Example:**
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
from typing_extensions import TypedDict
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
|
|
@ -402,7 +401,7 @@ class TableFooterSlotData(TypedDict):
|
|||
page_number: int
|
||||
|
||||
class Table(Component):
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
header: SlotInput
|
||||
footer: SlotInput[TableFooterSlotData]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import re
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from dataclasses import asdict, is_dataclass
|
||||
from hashlib import md5
|
||||
from importlib import import_module
|
||||
from inspect import getmembers
|
||||
from itertools import chain
|
||||
from types import ModuleType
|
||||
from typing import (
|
||||
|
|
@ -267,3 +269,56 @@ def format_as_ascii_table(
|
|||
def is_generator(obj: Any) -> bool:
|
||||
"""Check if an object is a generator with send method."""
|
||||
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 re
|
||||
from typing import Any, List, Literal, NamedTuple, Optional
|
||||
from typing import Any, List, Literal, Optional
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
|
|
@ -591,23 +591,23 @@ class TestComponentRenderAPI:
|
|||
class TestComponent(Component):
|
||||
template = ""
|
||||
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
variable: int
|
||||
another: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
variable: str
|
||||
another: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
my_slot: SlotInput
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
nonlocal called
|
||||
called = True
|
||||
|
||||
assert self.args == TestComponent.Args(123, "str")
|
||||
assert self.kwargs == TestComponent.Kwargs(variable="test", another=1)
|
||||
assert self.args == TestComponent.Args(123, "str") # type: ignore[call-arg]
|
||||
assert self.kwargs == TestComponent.Kwargs(variable="test", another=1) # type: ignore[call-arg]
|
||||
assert isinstance(self.slots, TestComponent.Slots)
|
||||
assert isinstance(self.slots.my_slot, Slot)
|
||||
assert self.slots.my_slot() == "MY_SLOT"
|
||||
|
|
@ -789,15 +789,15 @@ class TestComponentTemplateVars:
|
|||
|
||||
def test_args_kwargs_slots__simple_typed(self):
|
||||
class TestComponent(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
variable: int
|
||||
another: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
variable: str
|
||||
another: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
my_slot: SlotInput
|
||||
|
||||
template: types.django_html = """
|
||||
|
|
@ -898,15 +898,15 @@ class TestComponentTemplateVars:
|
|||
"""
|
||||
|
||||
class TestComponent(Component):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
variable: int
|
||||
another: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
variable: str
|
||||
another: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
class Slots:
|
||||
my_slot: SlotInput
|
||||
|
||||
template: types.django_html = """
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import re
|
||||
from typing import NamedTuple
|
||||
|
||||
import pytest
|
||||
from django.template import Context, Template
|
||||
|
|
@ -21,7 +20,7 @@ class TestDynamicComponent:
|
|||
Variable: <strong>{{ variable }}</strong>
|
||||
"""
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
variable: str
|
||||
variable2: str
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from typing import NamedTuple
|
||||
|
||||
import pytest
|
||||
from django.template import Context, Template
|
||||
from django.template.exceptions import TemplateSyntaxError
|
||||
|
|
@ -313,7 +311,7 @@ class TestErrorFallbackComponent:
|
|||
def test_error_fallback_nested_inside_another(self, components_settings):
|
||||
@register("broken")
|
||||
class BrokenComponent(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
msg: str
|
||||
|
||||
def on_render(self, context: Context, template: Template):
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ setup_test_config()
|
|||
|
||||
@djc_test
|
||||
class TestComponentTyping:
|
||||
def test_data_methods_input_typed(self):
|
||||
def test_data_methods_input_typed_custom_classes(self):
|
||||
template_called = False
|
||||
js_called = False
|
||||
css_called = False
|
||||
|
|
@ -84,6 +84,74 @@ class TestComponentTyping:
|
|||
assert js_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):
|
||||
template_called = False
|
||||
js_called = False
|
||||
|
|
@ -132,7 +200,71 @@ class TestComponentTyping:
|
|||
assert js_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
|
||||
js_called = False
|
||||
css_called = False
|
||||
|
|
@ -489,43 +621,57 @@ class TestComponentTyping:
|
|||
)
|
||||
|
||||
def test_custom_args_class_raises_on_invalid(self):
|
||||
class Parent:
|
||||
pass
|
||||
|
||||
class Button(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Args:
|
||||
class Args(Parent):
|
||||
arg1: 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")):
|
||||
Button.render(
|
||||
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
class Parent2:
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
class Button2(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Args:
|
||||
class Args(Parent2):
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
assert not issubclass(Button2.Args, tuple)
|
||||
assert issubclass(Button2.Args, Parent2)
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("'Args' object is not iterable")):
|
||||
Button2.render(
|
||||
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):
|
||||
template = "Hello"
|
||||
|
||||
class Args:
|
||||
class Args(Parent3):
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def __iter__(self):
|
||||
return iter([self.arg1, self.arg2])
|
||||
assert not issubclass(Button3.Args, tuple)
|
||||
assert issubclass(Button3.Args, Parent3)
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
|
||||
Button3.render(
|
||||
|
|
@ -533,62 +679,77 @@ class TestComponentTyping:
|
|||
)
|
||||
|
||||
def test_custom_args_class_custom(self):
|
||||
class Parent:
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
def __iter__(self):
|
||||
return iter([self.arg1, self.arg2])
|
||||
|
||||
class Button(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Args:
|
||||
class Args(Parent):
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
def __iter__(self):
|
||||
return iter([self.arg1, self.arg2])
|
||||
|
||||
Button.render(
|
||||
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||
)
|
||||
|
||||
def test_custom_kwargs_class_raises_on_invalid(self):
|
||||
class Parent:
|
||||
pass
|
||||
|
||||
class Button(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Kwargs:
|
||||
class Kwargs(Parent):
|
||||
arg1: 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")):
|
||||
Button.render(
|
||||
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
class Parent2:
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
class Button2(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Kwargs:
|
||||
class Kwargs(Parent2):
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
assert not issubclass(Button2.Kwargs, tuple)
|
||||
assert issubclass(Button2.Kwargs, Parent2)
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("'Kwargs' object is not iterable")):
|
||||
Button2.render(
|
||||
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):
|
||||
template = "Hello"
|
||||
|
||||
class Kwargs:
|
||||
class Kwargs(Parent3):
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def _asdict(self):
|
||||
return {"arg1": self.arg1, "arg2": self.arg2}
|
||||
assert not issubclass(Button3.Kwargs, tuple)
|
||||
assert issubclass(Button3.Kwargs, Parent3)
|
||||
|
||||
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
|
||||
Button3.render(
|
||||
|
|
@ -596,19 +757,23 @@ class TestComponentTyping:
|
|||
)
|
||||
|
||||
def test_custom_kwargs_class_custom(self):
|
||||
class Parent:
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
def _asdict(self):
|
||||
return {"arg1": self.arg1, "arg2": self.arg2}
|
||||
|
||||
class Button(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Kwargs:
|
||||
class Kwargs(Parent):
|
||||
arg1: str
|
||||
arg2: str
|
||||
|
||||
def __init__(self, arg1: str, arg2: str):
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
def _asdict(self):
|
||||
return {"arg1": self.arg1, "arg2": self.arg2}
|
||||
assert not issubclass(Button.Kwargs, tuple)
|
||||
assert issubclass(Button.Kwargs, Parent)
|
||||
|
||||
Button.render(
|
||||
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"),
|
||||
|
|
@ -618,14 +783,14 @@ class TestComponentTyping:
|
|||
class Button(Component):
|
||||
template = "Hello"
|
||||
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
size: int
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
color: str
|
||||
|
||||
class ButtonExtra(Button):
|
||||
class Args(NamedTuple):
|
||||
class Args:
|
||||
name: str
|
||||
size: int
|
||||
|
||||
|
|
@ -635,6 +800,6 @@ class TestComponentTyping:
|
|||
assert ButtonExtra.Kwargs is Button.Kwargs
|
||||
|
||||
ButtonExtra.render(
|
||||
args=ButtonExtra.Args(name="John", size=30),
|
||||
kwargs=ButtonExtra.Kwargs(color="red"),
|
||||
args=ButtonExtra.Args(name="John", size=30), # type: ignore[call-arg]
|
||||
kwargs=ButtonExtra.Kwargs(color="red"), # type: ignore[call-arg]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -242,6 +242,11 @@ class TestExtensions:
|
|||
del TestAccessComp
|
||||
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):
|
||||
@djc_test(components_settings={"extensions": [RenderExtension]})
|
||||
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.
|
||||
"""
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
import pytest
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import render
|
||||
|
|
@ -53,7 +51,7 @@ class TestTemplatePartialsIntegration:
|
|||
})()
|
||||
""" # noqa: E501
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
class Kwargs:
|
||||
date: str
|
||||
|
||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue