mirror of
https://github.com/django-components/django-components.git
synced 2025-09-04 05:00:32 +00:00
refactor: use typevar defaults + raise on conflicting extensions (#1125)
* refactor: use typevar defaults + raise on conflicting extensions * refactor: fix linter errors
This commit is contained in:
parent
61528ef0ad
commit
06cad2ec64
21 changed files with 321 additions and 93 deletions
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -1,6 +1,32 @@
|
||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
## v0.139
|
## v0.139.1
|
||||||
|
|
||||||
|
#### Refactor
|
||||||
|
|
||||||
|
- When typing a Component, you can now specify as few or as many parameters as you want.
|
||||||
|
|
||||||
|
```py
|
||||||
|
Component[Args]
|
||||||
|
Component[Args, Kwargs]
|
||||||
|
Component[Args, Kwargs, Slots]
|
||||||
|
Component[Args, Kwargs, Slots, Data]
|
||||||
|
Component[Args, Kwargs, Slots, Data, JsData]
|
||||||
|
Component[Args, Kwargs, Slots, Data, JsData, CssData]
|
||||||
|
```
|
||||||
|
|
||||||
|
All omitted parameters will default to `Any`.
|
||||||
|
|
||||||
|
- Added `typing_extensions` to the project as a dependency
|
||||||
|
|
||||||
|
- Multiple extensions with the same name (case-insensitive) now raise an error
|
||||||
|
|
||||||
|
- Extension names (case-insensitive) also MUST NOT conflict with existing Component class API.
|
||||||
|
|
||||||
|
So if you name an extension `render`, it will conflict with the `render()` method of the `Component` class,
|
||||||
|
and thus raise an error.
|
||||||
|
|
||||||
|
## v0.139.0
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
||||||
|
|
15
README.md
15
README.md
|
@ -396,7 +396,7 @@ Components API is fully typed, and supports [static type hints](https://django-c
|
||||||
To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
|
To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
|
from typing import NotRequired, Tuple, TypedDict, SlotContent, Slot
|
||||||
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
|
@ -407,22 +407,19 @@ class ButtonKwargs(TypedDict):
|
||||||
another: int
|
another: int
|
||||||
maybe_var: NotRequired[int] # May be omitted
|
maybe_var: NotRequired[int] # May be omitted
|
||||||
|
|
||||||
class ButtonData(TypedDict):
|
|
||||||
variable: str
|
|
||||||
|
|
||||||
class ButtonSlots(TypedDict):
|
class ButtonSlots(TypedDict):
|
||||||
my_slot: NotRequired[SlotFunc]
|
# Use `Slot` for slot functions.
|
||||||
|
my_slot: NotRequired[Slot]
|
||||||
|
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
||||||
another_slot: SlotContent
|
another_slot: SlotContent
|
||||||
|
|
||||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
|
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||||
|
|
||||||
class Button(ButtonType):
|
class Button(ButtonType):
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
self.input.args[0] # int
|
self.input.args[0] # int
|
||||||
self.input.kwargs["variable"] # str
|
self.input.kwargs["variable"] # str
|
||||||
self.input.slots["my_slot"] # SlotFunc[MySlotData]
|
self.input.slots["my_slot"] # Slot[MySlotData]
|
||||||
|
|
||||||
return {} # Error: Key "variable" is missing
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When you then call
|
When you then call
|
||||||
|
|
|
@ -103,7 +103,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
||||||
```djc_py
|
```djc_py
|
||||||
from typing import Dict, NotRequired, Optional, Tuple, TypedDict
|
from typing import Dict, NotRequired, Optional, Tuple, TypedDict
|
||||||
|
|
||||||
from django_components import Component, SlotFunc, register, types
|
from django_components import Component, SlotContent, register, types
|
||||||
|
|
||||||
from myapp.templatetags.mytags import comp_registry
|
from myapp.templatetags.mytags import comp_registry
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
||||||
type MyMenuArgs = Tuple[int, str]
|
type MyMenuArgs = Tuple[int, str]
|
||||||
|
|
||||||
class MyMenuSlots(TypedDict):
|
class MyMenuSlots(TypedDict):
|
||||||
default: NotRequired[Optional[SlotFunc[EmptyDict]]]
|
default: NotRequired[Optional[SlotContent[EmptyDict]]]
|
||||||
|
|
||||||
class MyMenuProps(TypedDict):
|
class MyMenuProps(TypedDict):
|
||||||
vertical: NotRequired[bool]
|
vertical: NotRequired[bool]
|
||||||
|
@ -124,7 +124,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
||||||
# Define the component
|
# Define the component
|
||||||
# NOTE: Don't forget to set the `registry`!
|
# NOTE: Don't forget to set the `registry`!
|
||||||
@register("my_menu", registry=comp_registry)
|
@register("my_menu", registry=comp_registry)
|
||||||
class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots, Any, Any, Any]):
|
class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots]):
|
||||||
def get_context_data(
|
def get_context_data(
|
||||||
self,
|
self,
|
||||||
*args,
|
*args,
|
||||||
|
|
|
@ -169,6 +169,14 @@ class MyExtension(ComponentExtension):
|
||||||
ctx.component_cls.my_attr = "my_value"
|
ctx.component_cls.my_attr = "my_value"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
The `name` attribute MUST be unique across all extensions.
|
||||||
|
|
||||||
|
Moreover, the `name` attribute MUST NOT conflict with existing Component class API.
|
||||||
|
|
||||||
|
So if you name an extension `render`, it will conflict with the [`render()`](../../../reference/api/#django_components.Component.render) method of the `Component` class.
|
||||||
|
|
||||||
### Defining the extension class
|
### Defining the extension class
|
||||||
|
|
||||||
In previous sections we've seen the `View` and `Storybook` extensions classes that were nested within the `Component` class:
|
In previous sections we've seen the `View` and `Storybook` extensions classes that were nested within the `Component` class:
|
||||||
|
|
|
@ -10,7 +10,7 @@ Use this to add type hints to your components, or to validate component inputs.
|
||||||
```py
|
```py
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
ButtonType = Component[Args, Kwargs, Slots, Data, JsData, CssData]
|
ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData, CssData]
|
||||||
|
|
||||||
class Button(ButtonType):
|
class Button(ButtonType):
|
||||||
template_file = "button.html"
|
template_file = "button.html"
|
||||||
|
@ -24,16 +24,34 @@ The generic parameters are:
|
||||||
- `Args` - Positional arguments, must be a `Tuple` or `Any`
|
- `Args` - Positional arguments, must be a `Tuple` or `Any`
|
||||||
- `Kwargs` - Keyword arguments, must be a `TypedDict` or `Any`
|
- `Kwargs` - Keyword arguments, must be a `TypedDict` or `Any`
|
||||||
- `Slots` - Slots, must be a `TypedDict` or `Any`
|
- `Slots` - Slots, must be a `TypedDict` or `Any`
|
||||||
- `Data` - Data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), must be a `TypedDict` or `Any`
|
- `TemplateData` - Data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), must be a `TypedDict` or `Any`
|
||||||
- `JsData` - Data returned from [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), must be a `TypedDict` or `Any`
|
- `JsData` - Data returned from [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), must be a `TypedDict` or `Any`
|
||||||
- `CssData` - Data returned from [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data), must be a `TypedDict` or `Any`
|
- `CssData` - Data returned from [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data), must be a `TypedDict` or `Any`
|
||||||
|
|
||||||
## Example
|
You can specify as many or as few of the parameters as you want. The rest will default to `Any`.
|
||||||
|
|
||||||
|
All of these are valid:
|
||||||
|
|
||||||
|
```py
|
||||||
|
ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData, CssData]
|
||||||
|
ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData]
|
||||||
|
ButtonType = Component[Args, Kwargs, Slots, TemplateData]
|
||||||
|
ButtonType = Component[Args, Kwargs, Slots]
|
||||||
|
ButtonType = Component[Args, Kwargs]
|
||||||
|
ButtonType = Component[Args]
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
Setting a type parameter to `Any` will disable typing and validation for that part.
|
||||||
|
|
||||||
|
## Typing inputs
|
||||||
|
|
||||||
|
You can use the first 3 parameters to type inputs.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import NotRequired, Tuple, TypedDict
|
from typing_extensions import NotRequired, Tuple, TypedDict
|
||||||
from pydantic import BaseModel
|
from django_components import Component, Slot, SlotContent
|
||||||
from django_components import Component, SlotContent, SlotFunc
|
|
||||||
|
|
||||||
###########################################
|
###########################################
|
||||||
# 1. Define the types
|
# 1. Define the types
|
||||||
|
@ -54,39 +72,17 @@ class ButtonFooterSlotData(TypedDict):
|
||||||
|
|
||||||
# Slots
|
# Slots
|
||||||
class ButtonSlots(TypedDict):
|
class ButtonSlots(TypedDict):
|
||||||
# SlotContent == str or slot func
|
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
||||||
header: SlotContent
|
header: SlotContent
|
||||||
# Use SlotFunc for slot functions.
|
# Use `Slot` for slot functions.
|
||||||
# The generic specifies the data available to the slot function
|
# The generic specifies the data available to the slot function
|
||||||
footer: NotRequired[SlotFunc[ButtonFooterSlotData]]
|
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
||||||
|
|
||||||
# Data returned from `get_context_data`
|
|
||||||
class ButtonData(BaseModel):
|
|
||||||
data1: str
|
|
||||||
data2: int
|
|
||||||
|
|
||||||
# Data returned from `get_js_data`
|
|
||||||
class ButtonJsData(BaseModel):
|
|
||||||
js_data1: str
|
|
||||||
js_data2: int
|
|
||||||
|
|
||||||
# Data returned from `get_css_data`
|
|
||||||
class ButtonCssData(BaseModel):
|
|
||||||
css_data1: str
|
|
||||||
css_data2: int
|
|
||||||
|
|
||||||
###########################################
|
###########################################
|
||||||
# 2. Define the component with those types
|
# 2. Define the component with those types
|
||||||
###########################################
|
###########################################
|
||||||
|
|
||||||
ButtonType = Component[
|
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||||
ButtonArgs,
|
|
||||||
ButtonKwargs,
|
|
||||||
ButtonSlots,
|
|
||||||
ButtonData,
|
|
||||||
ButtonJsData,
|
|
||||||
ButtonCssData,
|
|
||||||
]
|
|
||||||
|
|
||||||
class Button(ButtonType):
|
class Button(ButtonType):
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
@ -109,23 +105,81 @@ Button.render(
|
||||||
},
|
},
|
||||||
slots={
|
slots={
|
||||||
"header": "...",
|
"header": "...",
|
||||||
# ERROR: Expects key "footer"
|
"footer": lambda ctx, slot_data, slot_ref: slot_data.value + 1,
|
||||||
|
# ERROR: Unexpected key "foo"
|
||||||
"foo": "invalid",
|
"foo": "invalid",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
If you don't want to validate some parts, set them to [`Any`](https://docs.python.org/3/library/typing.html#typing.Any).
|
If you don't want to validate some parts, set them to [`Any`](https://docs.python.org/3/library/typing.html#typing.Any) or omit them.
|
||||||
|
|
||||||
|
The following will validate only the keyword inputs:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
ButtonType = Component[
|
ButtonType = Component[Any, ButtonKwargs]
|
||||||
ButtonArgs,
|
|
||||||
ButtonKwargs,
|
class Button(ButtonType):
|
||||||
ButtonSlots,
|
...
|
||||||
Any,
|
```
|
||||||
Any,
|
|
||||||
Any,
|
## Typing data
|
||||||
]
|
|
||||||
|
You can use the last 3 parameters to type the data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), and [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
|
||||||
|
|
||||||
|
Use this together with the [Pydantic integration](#runtime-input-validation-with-types) to have runtime validation of the data.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing_extensions import NotRequired, Tuple, TypedDict
|
||||||
|
from django_components import Component, Slot, SlotContent
|
||||||
|
|
||||||
|
###########################################
|
||||||
|
# 1. Define the types
|
||||||
|
###########################################
|
||||||
|
|
||||||
|
# Data returned from `get_context_data`
|
||||||
|
class ButtonData(TypedDict):
|
||||||
|
data1: str
|
||||||
|
data2: int
|
||||||
|
|
||||||
|
# Data returned from `get_js_data`
|
||||||
|
class ButtonJsData(TypedDict):
|
||||||
|
js_data1: str
|
||||||
|
js_data2: int
|
||||||
|
|
||||||
|
# Data returned from `get_css_data`
|
||||||
|
class ButtonCssData(TypedDict):
|
||||||
|
css_data1: str
|
||||||
|
css_data2: int
|
||||||
|
|
||||||
|
###########################################
|
||||||
|
# 2. Define the component with those types
|
||||||
|
###########################################
|
||||||
|
|
||||||
|
ButtonType = Component[Any, Any, Any, ButtonData, ButtonJsData, ButtonCssData]
|
||||||
|
|
||||||
|
class Button(ButtonType):
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_js_data(self, *args, **kwargs):
|
||||||
|
...
|
||||||
|
|
||||||
|
def get_css_data(self, *args, **kwargs):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
When you then call
|
||||||
|
[`Component.render`](../../../reference/api#django_components.Component.render)
|
||||||
|
or [`Component.render_to_response`](../../../reference/api#django_components.Component.render_to_response),
|
||||||
|
the component will raise an error if the data is not valid.
|
||||||
|
|
||||||
|
If you don't want to validate some parts, set them to [`Any`](https://docs.python.org/3/library/typing.html#typing.Any).
|
||||||
|
|
||||||
|
The following will validate only the data returned from `get_js_data`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ButtonType = Component[Any, Any, Any, Any, ButtonJsData]
|
||||||
|
|
||||||
class Button(ButtonType):
|
class Button(ButtonType):
|
||||||
...
|
...
|
||||||
|
@ -182,6 +236,9 @@ class Button(Component[EmptyTuple, EmptyDict, EmptyDict, EmptyDict, EmptyDict, E
|
||||||
|
|
||||||
Since v0.136, input validation is available as a separate extension.
|
Since v0.136, input validation is available as a separate extension.
|
||||||
|
|
||||||
|
By defualt, when you add types to your component, this will only set up static type hints,
|
||||||
|
but it will not validate the inputs.
|
||||||
|
|
||||||
To enable input validation, you need to install the [`djc-ext-pydantic`](https://github.com/django-components/djc-ext-pydantic) extension:
|
To enable input validation, you need to install the [`djc-ext-pydantic`](https://github.com/django-components/djc-ext-pydantic) extension:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
@ -47,7 +47,7 @@ Component.render(
|
||||||
context: Mapping | django.template.Context | None = None,
|
context: Mapping | django.template.Context | None = None,
|
||||||
args: List[Any] | None = None,
|
args: List[Any] | None = None,
|
||||||
kwargs: Dict[str, Any] | None = None,
|
kwargs: Dict[str, Any] | None = None,
|
||||||
slots: Dict[str, str | SafeString | SlotFunc] | None = None,
|
slots: Dict[str, str | SafeString | SlotContent] | None = None,
|
||||||
escape_slots_content: bool = True
|
escape_slots_content: bool = True
|
||||||
) -> str:
|
) -> str:
|
||||||
```
|
```
|
||||||
|
@ -60,7 +60,7 @@ Component.render(
|
||||||
|
|
||||||
- _`slots`_ - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
|
- _`slots`_ - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
|
||||||
Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
|
Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
|
||||||
or [`SlotFunc`](#slotfunc).
|
or `Slot`.
|
||||||
|
|
||||||
- _`escape_slots_content`_ - Whether the content from `slots` should be escaped. `True` by default to prevent XSS attacks. If you disable escaping, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
|
- _`escape_slots_content`_ - Whether the content from `slots` should be escaped. `True` by default to prevent XSS attacks. If you disable escaping, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
|
||||||
|
|
||||||
|
|
|
@ -386,7 +386,7 @@ Components API is fully typed, and supports [static type hints](https://django-c
|
||||||
To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
|
To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
|
from typing import NotRequired, Tuple, TypedDict, SlotContent, Slot
|
||||||
|
|
||||||
from django_components import Component
|
from django_components import Component
|
||||||
|
|
||||||
|
@ -397,22 +397,19 @@ class ButtonKwargs(TypedDict):
|
||||||
another: int
|
another: int
|
||||||
maybe_var: NotRequired[int] # May be omitted
|
maybe_var: NotRequired[int] # May be omitted
|
||||||
|
|
||||||
class ButtonData(TypedDict):
|
|
||||||
variable: str
|
|
||||||
|
|
||||||
class ButtonSlots(TypedDict):
|
class ButtonSlots(TypedDict):
|
||||||
my_slot: NotRequired[SlotFunc]
|
# Use `Slot` for slot functions.
|
||||||
|
my_slot: NotRequired[Slot]
|
||||||
|
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
||||||
another_slot: SlotContent
|
another_slot: SlotContent
|
||||||
|
|
||||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
|
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||||
|
|
||||||
class Button(ButtonType):
|
class Button(ButtonType):
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
self.input.args[0] # int
|
self.input.args[0] # int
|
||||||
self.input.kwargs["variable"] # str
|
self.input.kwargs["variable"] # str
|
||||||
self.input.slots["my_slot"] # SlotFunc[MySlotData]
|
self.input.slots["my_slot"] # Slot
|
||||||
|
|
||||||
return {} # Error: Key "variable" is missing
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When you then call
|
When you then call
|
||||||
|
|
|
@ -19,10 +19,6 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.ComponentCommand
|
|
||||||
options:
|
|
||||||
show_if_no_docstring: true
|
|
||||||
|
|
||||||
::: django_components.ComponentDefaults
|
::: django_components.ComponentDefaults
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
@ -35,6 +31,10 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.ComponentInput
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.ComponentMediaInput
|
::: django_components.ComponentMediaInput
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
|
@ -462,8 +462,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
|
||||||
## `upgradecomponent`
|
## `upgradecomponent`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -507,9 +507,9 @@ Deprecated. Use `components upgrade` instead.
|
||||||
## `startcomponent`
|
## `startcomponent`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run]
|
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
||||||
[--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback]
|
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||||
[--no-color] [--force-color] [--skip-checks]
|
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||||
name
|
name
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -109,6 +109,7 @@ name | type | description
|
||||||
|
|
||||||
::: django_components.extension.ComponentExtension.on_component_rendered
|
::: django_components.extension.ComponentExtension.on_component_rendered
|
||||||
options:
|
options:
|
||||||
|
heading_level: 3
|
||||||
show_root_heading: true
|
show_root_heading: true
|
||||||
show_signature: true
|
show_signature: true
|
||||||
separate_signature: true
|
separate_signature: true
|
||||||
|
|
|
@ -20,7 +20,7 @@ Import as
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1037" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1064" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ If you insert this tag multiple times, ALL CSS links will be duplicately inserte
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1059" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1086" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1564" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1669" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ classifiers = [
|
||||||
dependencies = [
|
dependencies = [
|
||||||
'Django>=4.2',
|
'Django>=4.2',
|
||||||
'djc-core-html-parser>=1.0.2',
|
'djc-core-html-parser>=1.0.2',
|
||||||
|
'typing-extensions>=4.12.2',
|
||||||
]
|
]
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|
||||||
|
|
|
@ -7,3 +7,4 @@ whitenoise
|
||||||
asv
|
asv
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
pytest-django
|
pytest-django
|
||||||
|
typing-extensions>=4.12.2
|
|
@ -95,6 +95,7 @@ types-requests==2.32.0.20241016
|
||||||
# via -r requirements-ci.in
|
# via -r requirements-ci.in
|
||||||
typing-extensions==4.12.2
|
typing-extensions==4.12.2
|
||||||
# via
|
# via
|
||||||
|
# -r requirements-ci.in
|
||||||
# pyee
|
# pyee
|
||||||
# tox
|
# tox
|
||||||
urllib3==2.2.3
|
urllib3==2.2.3
|
||||||
|
|
|
@ -18,3 +18,4 @@ whitenoise
|
||||||
pygments
|
pygments
|
||||||
pygments-djc
|
pygments-djc
|
||||||
asv
|
asv
|
||||||
|
typing-extensions>=4.12.2
|
|
@ -150,6 +150,7 @@ types-requests==2.32.0.20241016
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
typing-extensions==4.12.2
|
typing-extensions==4.12.2
|
||||||
# via
|
# via
|
||||||
|
# -r requirements-dev.in
|
||||||
# asgiref
|
# asgiref
|
||||||
# black
|
# black
|
||||||
# mypy
|
# mypy
|
||||||
|
|
|
@ -18,7 +18,6 @@ from typing import (
|
||||||
Optional,
|
Optional,
|
||||||
Tuple,
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
TypeVar,
|
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
|
@ -34,6 +33,7 @@ from django.template.loader_tags import BLOCK_CONTEXT_KEY, BlockContext
|
||||||
from django.test.signals import template_rendered
|
from django.test.signals import template_rendered
|
||||||
from django.utils.html import conditional_escape
|
from django.utils.html import conditional_escape
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from typing_extensions import TypeVar
|
||||||
|
|
||||||
from django_components.app_settings import ContextBehavior, app_settings
|
from django_components.app_settings import ContextBehavior, app_settings
|
||||||
from django_components.component_media import ComponentMediaInput, ComponentMediaMeta
|
from django_components.component_media import ComponentMediaInput, ComponentMediaMeta
|
||||||
|
@ -103,12 +103,12 @@ from django_components.component_registry import registry as registry # NOQA
|
||||||
COMP_ONLY_FLAG = "only"
|
COMP_ONLY_FLAG = "only"
|
||||||
|
|
||||||
# Define TypeVars for args and kwargs
|
# Define TypeVars for args and kwargs
|
||||||
ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True)
|
ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True, default=Any)
|
||||||
KwargsType = TypeVar("KwargsType", bound=Mapping[str, Any], contravariant=True)
|
KwargsType = TypeVar("KwargsType", bound=Mapping[str, Any], contravariant=True, default=Any)
|
||||||
SlotsType = TypeVar("SlotsType", bound=Mapping[SlotName, SlotContent])
|
SlotsType = TypeVar("SlotsType", bound=Mapping[SlotName, SlotContent], default=Any)
|
||||||
DataType = TypeVar("DataType", bound=Mapping[str, Any], covariant=True)
|
DataType = TypeVar("DataType", bound=Mapping[str, Any], covariant=True, default=Any)
|
||||||
JsDataType = TypeVar("JsDataType", bound=Mapping[str, Any])
|
JsDataType = TypeVar("JsDataType", bound=Mapping[str, Any], default=Any)
|
||||||
CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any])
|
CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any], default=Any)
|
||||||
|
|
||||||
|
|
||||||
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Tuple, Type, TypeVar, Union
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Set, Tuple, Type, TypeVar, Union
|
||||||
|
|
||||||
import django.urls
|
import django.urls
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
|
@ -661,7 +661,20 @@ class ExtensionManager:
|
||||||
# Populate the `urlpatterns` with URLs specified by the extensions
|
# Populate the `urlpatterns` with URLs specified by the extensions
|
||||||
# TODO_V3 - Django-specific logic - replace with hook
|
# TODO_V3 - Django-specific logic - replace with hook
|
||||||
urls: List[URLResolver] = []
|
urls: List[URLResolver] = []
|
||||||
|
seen_names: Set[str] = set()
|
||||||
|
|
||||||
|
from django_components import Component
|
||||||
|
|
||||||
for extension in self.extensions:
|
for extension in self.extensions:
|
||||||
|
# Ensure that the extension name won't conflict with existing Component class API
|
||||||
|
if hasattr(Component, extension.name) or hasattr(Component, extension.class_name):
|
||||||
|
raise ValueError(f"Extension name '{extension.name}' conflicts with existing Component class API")
|
||||||
|
|
||||||
|
if extension.name.lower() in seen_names:
|
||||||
|
raise ValueError(f"Multiple extensions cannot have the same name '{extension.name}'")
|
||||||
|
|
||||||
|
seen_names.add(extension.name.lower())
|
||||||
|
|
||||||
# NOTE: The empty list is a placeholder for the URLs that will be added later
|
# NOTE: The empty list is a placeholder for the URLs that will be added later
|
||||||
curr_ext_url_resolver = django.urls.path(f"{extension.name}/", django.urls.include([]))
|
curr_ext_url_resolver = django.urls.path(f"{extension.name}/", django.urls.include([]))
|
||||||
urls.append(curr_ext_url_resolver)
|
urls.append(curr_ext_url_resolver)
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
from typing import Any, Optional, Type, TypeVar
|
from typing import Any, Optional, Type
|
||||||
|
|
||||||
from django.template import Origin, Template
|
from django.template import Origin, Template
|
||||||
|
|
||||||
from django_components.cache import get_template_cache
|
from django_components.cache import get_template_cache
|
||||||
from django_components.util.misc import get_import_path
|
from django_components.util.misc import get_import_path
|
||||||
|
|
||||||
TTemplate = TypeVar("TTemplate", bound=Template)
|
|
||||||
|
|
||||||
|
|
||||||
# Central logic for creating Templates from string, so we can cache the results
|
# Central logic for creating Templates from string, so we can cache the results
|
||||||
def cached_template(
|
def cached_template(
|
||||||
|
|
|
@ -4,7 +4,8 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, no_type_check
|
from typing import Any, Dict, Tuple, no_type_check
|
||||||
|
from typing_extensions import NotRequired, TypedDict
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -16,7 +17,16 @@ from django.test import Client
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
||||||
|
|
||||||
from django_components import Component, ComponentView, all_components, get_component_by_class_id, register, types
|
from django_components import (
|
||||||
|
Component,
|
||||||
|
ComponentView,
|
||||||
|
SlotContent,
|
||||||
|
Slot,
|
||||||
|
all_components,
|
||||||
|
get_component_by_class_id,
|
||||||
|
register,
|
||||||
|
types,
|
||||||
|
)
|
||||||
from django_components.slots import SlotRef
|
from django_components.slots import SlotRef
|
||||||
from django_components.urls import urlpatterns as dc_urlpatterns
|
from django_components.urls import urlpatterns as dc_urlpatterns
|
||||||
|
|
||||||
|
@ -375,6 +385,101 @@ class TestComponent:
|
||||||
|
|
||||||
assert SimpleComponent.render() == "Hello"
|
assert SimpleComponent.render() == "Hello"
|
||||||
|
|
||||||
|
def test_typing(self):
|
||||||
|
# Types
|
||||||
|
ButtonArgs = Tuple[str, ...]
|
||||||
|
|
||||||
|
class ButtonKwargs(TypedDict):
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
maybe_var: NotRequired[int]
|
||||||
|
|
||||||
|
class ButtonFooterSlotData(TypedDict):
|
||||||
|
value: int
|
||||||
|
|
||||||
|
class ButtonSlots(TypedDict):
|
||||||
|
# Use `SlotContent` when you want to allow either function (`Slot` instance)
|
||||||
|
# or plain string.
|
||||||
|
header: SlotContent
|
||||||
|
# Use `Slot` for slot functions. The generic specifies the data available to the slot function.
|
||||||
|
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
||||||
|
|
||||||
|
# Data returned from `get_context_data`
|
||||||
|
class ButtonData(TypedDict):
|
||||||
|
data1: str
|
||||||
|
data2: int
|
||||||
|
|
||||||
|
# Data returned from `get_js_data`
|
||||||
|
class ButtonJsData(TypedDict):
|
||||||
|
js_data1: str
|
||||||
|
js_data2: int
|
||||||
|
|
||||||
|
# Data returned from `get_css_data`
|
||||||
|
class ButtonCssData(TypedDict):
|
||||||
|
css_data1: str
|
||||||
|
css_data2: int
|
||||||
|
|
||||||
|
# Tests - We simply check that these don't raise any errors
|
||||||
|
# nor any type errors.
|
||||||
|
ButtonType1 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, ButtonJsData, ButtonCssData]
|
||||||
|
ButtonType2 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, ButtonJsData]
|
||||||
|
ButtonType3 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData]
|
||||||
|
ButtonType4 = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||||
|
ButtonType5 = Component[ButtonArgs, ButtonKwargs]
|
||||||
|
ButtonType6 = Component[ButtonArgs]
|
||||||
|
|
||||||
|
class Button1(ButtonType1):
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
class Button2(ButtonType2):
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
class Button3(ButtonType3):
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
class Button4(ButtonType4):
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
class Button5(ButtonType5):
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
class Button6(ButtonType6):
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
Button1.render(
|
||||||
|
args=("arg1", "arg2"),
|
||||||
|
kwargs={"name": "name", "age": 123},
|
||||||
|
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||||
|
)
|
||||||
|
|
||||||
|
Button2.render(
|
||||||
|
args=("arg1", "arg2"),
|
||||||
|
kwargs={"name": "name", "age": 123},
|
||||||
|
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||||
|
)
|
||||||
|
|
||||||
|
Button3.render(
|
||||||
|
args=("arg1", "arg2"),
|
||||||
|
kwargs={"name": "name", "age": 123},
|
||||||
|
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||||
|
)
|
||||||
|
|
||||||
|
Button4.render(
|
||||||
|
args=("arg1", "arg2"),
|
||||||
|
kwargs={"name": "name", "age": 123},
|
||||||
|
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||||
|
)
|
||||||
|
|
||||||
|
Button5.render(
|
||||||
|
args=("arg1", "arg2"),
|
||||||
|
kwargs={"name": "name", "age": 123},
|
||||||
|
)
|
||||||
|
|
||||||
|
Button6.render(
|
||||||
|
args=("arg1", "arg2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestComponentRender:
|
class TestComponentRender:
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import gc
|
import gc
|
||||||
from typing import Any, Callable, Dict, List, cast
|
from typing import Any, Callable, Dict, List, cast
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
|
@ -108,6 +109,10 @@ class DummyNestedExtension(ComponentExtension):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RenderExtension(ComponentExtension):
|
||||||
|
name = "render"
|
||||||
|
|
||||||
|
|
||||||
def with_component_cls(on_created: Callable):
|
def with_component_cls(on_created: Callable):
|
||||||
class TempComponent(Component):
|
class TempComponent(Component):
|
||||||
template = "Hello {{ name }}!"
|
template = "Hello {{ name }}!"
|
||||||
|
@ -151,6 +156,22 @@ class TestExtension:
|
||||||
del TestAccessComp
|
del TestAccessComp
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
|
||||||
|
def test_raises_on_extension_name_conflict(self):
|
||||||
|
@djc_test(components_settings={"extensions": [RenderExtension]})
|
||||||
|
def inner():
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Extension name 'render' conflicts with existing Component class API"):
|
||||||
|
inner()
|
||||||
|
|
||||||
|
def test_raises_on_multiple_extensions_with_same_name(self):
|
||||||
|
@djc_test(components_settings={"extensions": [DummyExtension, DummyExtension]})
|
||||||
|
def inner():
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="Multiple extensions cannot have the same name 'test_extension'"):
|
||||||
|
inner()
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestExtensionHooks:
|
class TestExtensionHooks:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue