mirror of
https://github.com/django-components/django-components.git
synced 2025-08-31 19:27:19 +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
|
||||
|
||||
## 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
|
||||
|
||||
|
|
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:
|
||||
|
||||
```py
|
||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
|
||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, Slot
|
||||
|
||||
from django_components import Component
|
||||
|
||||
|
@ -407,22 +407,19 @@ class ButtonKwargs(TypedDict):
|
|||
another: int
|
||||
maybe_var: NotRequired[int] # May be omitted
|
||||
|
||||
class ButtonData(TypedDict):
|
||||
variable: str
|
||||
|
||||
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
|
||||
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||
|
||||
class Button(ButtonType):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
self.input.args[0] # int
|
||||
self.input.kwargs["variable"] # str
|
||||
self.input.slots["my_slot"] # SlotFunc[MySlotData]
|
||||
|
||||
return {} # Error: Key "variable" is missing
|
||||
self.input.slots["my_slot"] # Slot[MySlotData]
|
||||
```
|
||||
|
||||
When you then call
|
||||
|
|
|
@ -103,7 +103,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
```djc_py
|
||||
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
|
||||
|
||||
|
@ -114,7 +114,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
type MyMenuArgs = Tuple[int, str]
|
||||
|
||||
class MyMenuSlots(TypedDict):
|
||||
default: NotRequired[Optional[SlotFunc[EmptyDict]]]
|
||||
default: NotRequired[Optional[SlotContent[EmptyDict]]]
|
||||
|
||||
class MyMenuProps(TypedDict):
|
||||
vertical: NotRequired[bool]
|
||||
|
@ -124,7 +124,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
# Define the component
|
||||
# NOTE: Don't forget to set the `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(
|
||||
self,
|
||||
*args,
|
||||
|
|
|
@ -169,6 +169,14 @@ class MyExtension(ComponentExtension):
|
|||
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
|
||||
|
||||
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
|
||||
from django_components import Component
|
||||
|
||||
ButtonType = Component[Args, Kwargs, Slots, Data, JsData, CssData]
|
||||
ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData, CssData]
|
||||
|
||||
class Button(ButtonType):
|
||||
template_file = "button.html"
|
||||
|
@ -24,16 +24,34 @@ The generic parameters are:
|
|||
- `Args` - Positional arguments, must be a `Tuple` or `Any`
|
||||
- `Kwargs` - Keyword arguments, 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`
|
||||
- `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
|
||||
from typing import NotRequired, Tuple, TypedDict
|
||||
from pydantic import BaseModel
|
||||
from django_components import Component, SlotContent, SlotFunc
|
||||
from typing_extensions import NotRequired, Tuple, TypedDict
|
||||
from django_components import Component, Slot, SlotContent
|
||||
|
||||
###########################################
|
||||
# 1. Define the types
|
||||
|
@ -54,39 +72,17 @@ class ButtonFooterSlotData(TypedDict):
|
|||
|
||||
# Slots
|
||||
class ButtonSlots(TypedDict):
|
||||
# SlotContent == str or slot func
|
||||
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
||||
header: SlotContent
|
||||
# Use SlotFunc for slot functions.
|
||||
# Use `Slot` for slot functions.
|
||||
# The generic specifies the data available to the slot function
|
||||
footer: NotRequired[SlotFunc[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
|
||||
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
||||
|
||||
###########################################
|
||||
# 2. Define the component with those types
|
||||
###########################################
|
||||
|
||||
ButtonType = Component[
|
||||
ButtonArgs,
|
||||
ButtonKwargs,
|
||||
ButtonSlots,
|
||||
ButtonData,
|
||||
ButtonJsData,
|
||||
ButtonCssData,
|
||||
]
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||
|
||||
class Button(ButtonType):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
|
@ -109,23 +105,81 @@ Button.render(
|
|||
},
|
||||
slots={
|
||||
"header": "...",
|
||||
# ERROR: Expects key "footer"
|
||||
"footer": lambda ctx, slot_data, slot_ref: slot_data.value + 1,
|
||||
# ERROR: Unexpected key "foo"
|
||||
"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
|
||||
ButtonType = Component[
|
||||
ButtonArgs,
|
||||
ButtonKwargs,
|
||||
ButtonSlots,
|
||||
Any,
|
||||
Any,
|
||||
Any,
|
||||
]
|
||||
ButtonType = Component[Any, ButtonKwargs]
|
||||
|
||||
class Button(ButtonType):
|
||||
...
|
||||
```
|
||||
|
||||
## 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):
|
||||
...
|
||||
|
@ -182,6 +236,9 @@ class Button(Component[EmptyTuple, EmptyDict, EmptyDict, EmptyDict, EmptyDict, E
|
|||
|
||||
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:
|
||||
|
||||
```bash
|
||||
|
|
|
@ -47,7 +47,7 @@ Component.render(
|
|||
context: Mapping | django.template.Context | None = None,
|
||||
args: List[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
|
||||
) -> str:
|
||||
```
|
||||
|
@ -60,7 +60,7 @@ Component.render(
|
|||
|
||||
- _`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
|
||||
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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
```py
|
||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
|
||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, Slot
|
||||
|
||||
from django_components import Component
|
||||
|
||||
|
@ -397,22 +397,19 @@ class ButtonKwargs(TypedDict):
|
|||
another: int
|
||||
maybe_var: NotRequired[int] # May be omitted
|
||||
|
||||
class ButtonData(TypedDict):
|
||||
variable: str
|
||||
|
||||
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
|
||||
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||
|
||||
class Button(ButtonType):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
self.input.args[0] # int
|
||||
self.input.kwargs["variable"] # str
|
||||
self.input.slots["my_slot"] # SlotFunc[MySlotData]
|
||||
|
||||
return {} # Error: Key "variable" is missing
|
||||
self.input.slots["my_slot"] # Slot
|
||||
```
|
||||
|
||||
When you then call
|
||||
|
|
|
@ -19,10 +19,6 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentCommand
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentDefaults
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
@ -35,6 +31,10 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentInput
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentMediaInput
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
|
|
@ -462,8 +462,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
|
|||
## `upgradecomponent`
|
||||
|
||||
```txt
|
||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
|
||||
```
|
||||
|
||||
|
@ -507,9 +507,9 @@ Deprecated. Use `components upgrade` instead.
|
|||
## `startcomponent`
|
||||
|
||||
```txt
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run]
|
||||
[--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback]
|
||||
[--no-color] [--force-color] [--skip-checks]
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
||||
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
name
|
||||
|
||||
```
|
||||
|
|
|
@ -109,6 +109,7 @@ name | type | description
|
|||
|
||||
::: django_components.extension.ComponentExtension.on_component_rendered
|
||||
options:
|
||||
heading_level: 3
|
||||
show_root_heading: true
|
||||
show_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 = [
|
||||
'Django>=4.2',
|
||||
'djc-core-html-parser>=1.0.2',
|
||||
'typing-extensions>=4.12.2',
|
||||
]
|
||||
license = {text = "MIT"}
|
||||
|
||||
|
|
|
@ -7,3 +7,4 @@ whitenoise
|
|||
asv
|
||||
pytest-asyncio
|
||||
pytest-django
|
||||
typing-extensions>=4.12.2
|
|
@ -95,6 +95,7 @@ types-requests==2.32.0.20241016
|
|||
# via -r requirements-ci.in
|
||||
typing-extensions==4.12.2
|
||||
# via
|
||||
# -r requirements-ci.in
|
||||
# pyee
|
||||
# tox
|
||||
urllib3==2.2.3
|
||||
|
|
|
@ -18,3 +18,4 @@ whitenoise
|
|||
pygments
|
||||
pygments-djc
|
||||
asv
|
||||
typing-extensions>=4.12.2
|
|
@ -150,6 +150,7 @@ types-requests==2.32.0.20241016
|
|||
# via -r requirements-dev.in
|
||||
typing-extensions==4.12.2
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# asgiref
|
||||
# black
|
||||
# mypy
|
||||
|
|
|
@ -18,7 +18,6 @@ from typing import (
|
|||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
@ -34,6 +33,7 @@ from django.template.loader_tags import BLOCK_CONTEXT_KEY, BlockContext
|
|||
from django.test.signals import template_rendered
|
||||
from django.utils.html import conditional_escape
|
||||
from django.views import View
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
from django_components.app_settings import ContextBehavior, app_settings
|
||||
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"
|
||||
|
||||
# Define TypeVars for args and kwargs
|
||||
ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True)
|
||||
KwargsType = TypeVar("KwargsType", bound=Mapping[str, Any], contravariant=True)
|
||||
SlotsType = TypeVar("SlotsType", bound=Mapping[SlotName, SlotContent])
|
||||
DataType = TypeVar("DataType", bound=Mapping[str, Any], covariant=True)
|
||||
JsDataType = TypeVar("JsDataType", bound=Mapping[str, Any])
|
||||
CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any])
|
||||
ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True, default=Any)
|
||||
KwargsType = TypeVar("KwargsType", bound=Mapping[str, Any], contravariant=True, default=Any)
|
||||
SlotsType = TypeVar("SlotsType", bound=Mapping[SlotName, SlotContent], default=Any)
|
||||
DataType = TypeVar("DataType", bound=Mapping[str, Any], covariant=True, default=Any)
|
||||
JsDataType = TypeVar("JsDataType", bound=Mapping[str, Any], default=Any)
|
||||
CssDataType = TypeVar("CssDataType", bound=Mapping[str, Any], default=Any)
|
||||
|
||||
|
||||
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
from django.template import Context
|
||||
|
@ -661,7 +661,20 @@ class ExtensionManager:
|
|||
# Populate the `urlpatterns` with URLs specified by the extensions
|
||||
# TODO_V3 - Django-specific logic - replace with hook
|
||||
urls: List[URLResolver] = []
|
||||
seen_names: Set[str] = set()
|
||||
|
||||
from django_components import Component
|
||||
|
||||
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
|
||||
curr_ext_url_resolver = django.urls.path(f"{extension.name}/", django.urls.include([]))
|
||||
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_components.cache import get_template_cache
|
||||
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
|
||||
def cached_template(
|
||||
|
|
|
@ -4,7 +4,8 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
|||
"""
|
||||
|
||||
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
|
||||
from django.conf import settings
|
||||
|
@ -16,7 +17,16 @@ from django.test import Client
|
|||
from django.urls import path
|
||||
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.urls import urlpatterns as dc_urlpatterns
|
||||
|
||||
|
@ -375,6 +385,101 @@ class TestComponent:
|
|||
|
||||
assert SimpleComponent.render() == "Hello"
|
||||
|
||||
def test_typing(self):
|
||||
# Types
|
||||
ButtonArgs = Tuple[str, ...]
|
||||
|
||||
class ButtonKwargs(TypedDict):
|
||||
name: str
|
||||
age: int
|
||||
maybe_var: NotRequired[int]
|
||||
|
||||
class ButtonFooterSlotData(TypedDict):
|
||||
value: int
|
||||
|
||||
class ButtonSlots(TypedDict):
|
||||
# Use `SlotContent` when you want to allow either function (`Slot` instance)
|
||||
# or plain string.
|
||||
header: SlotContent
|
||||
# Use `Slot` for slot functions. The generic specifies the data available to the slot function.
|
||||
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
||||
|
||||
# Data returned from `get_context_data`
|
||||
class ButtonData(TypedDict):
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
# Data returned from `get_js_data`
|
||||
class ButtonJsData(TypedDict):
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
# Data returned from `get_css_data`
|
||||
class ButtonCssData(TypedDict):
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
# Tests - We simply check that these don't raise any errors
|
||||
# nor any type errors.
|
||||
ButtonType1 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, ButtonJsData, ButtonCssData]
|
||||
ButtonType2 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, ButtonJsData]
|
||||
ButtonType3 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData]
|
||||
ButtonType4 = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||
ButtonType5 = Component[ButtonArgs, ButtonKwargs]
|
||||
ButtonType6 = Component[ButtonArgs]
|
||||
|
||||
class Button1(ButtonType1):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
class Button2(ButtonType2):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
class Button3(ButtonType3):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
class Button4(ButtonType4):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
class Button5(ButtonType5):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
class Button6(ButtonType6):
|
||||
template = "<button>Click me!</button>"
|
||||
|
||||
Button1.render(
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||
)
|
||||
|
||||
Button2.render(
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||
)
|
||||
|
||||
Button3.render(
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||
)
|
||||
|
||||
Button4.render(
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "name", "age": 123},
|
||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
||||
)
|
||||
|
||||
Button5.render(
|
||||
args=("arg1", "arg2"),
|
||||
kwargs={"name": "name", "age": 123},
|
||||
)
|
||||
|
||||
Button6.render(
|
||||
args=("arg1", "arg2"),
|
||||
)
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestComponentRender:
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import gc
|
||||
from typing import Any, Callable, Dict, List, cast
|
||||
|
||||
import pytest
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.template import Context
|
||||
from django.test import Client
|
||||
|
@ -108,6 +109,10 @@ class DummyNestedExtension(ComponentExtension):
|
|||
]
|
||||
|
||||
|
||||
class RenderExtension(ComponentExtension):
|
||||
name = "render"
|
||||
|
||||
|
||||
def with_component_cls(on_created: Callable):
|
||||
class TempComponent(Component):
|
||||
template = "Hello {{ name }}!"
|
||||
|
@ -151,6 +156,22 @@ class TestExtension:
|
|||
del TestAccessComp
|
||||
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
|
||||
class TestExtensionHooks:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue