feat: extensions (#1009)

* feat: extensions

* refactor: remove support for passing in extensions as instances
This commit is contained in:
Juro Oravec 2025-03-08 09:41:28 +01:00 committed by GitHub
parent cff252c566
commit 4d35bc97a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1884 additions and 57 deletions

View file

@ -1,6 +1,7 @@
import re
from dataclasses import dataclass
from enum import Enum
from importlib import import_module
from os import PathLike
from pathlib import Path
from typing import (
@ -15,6 +16,7 @@ from typing import (
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
cast,
@ -25,6 +27,7 @@ from django.conf import settings
from django_components.util.misc import default
if TYPE_CHECKING:
from django_components.extension import ComponentExtension
from django_components.tag_formatter import TagFormatterABC
@ -146,6 +149,25 @@ class ComponentsSettings(NamedTuple):
```
"""
extensions: Optional[Sequence[Union[Type["ComponentExtension"], str]]] = None
"""
List of [extensions](../../concepts/advanced/extensions) to be loaded.
The extensions can be specified as:
- Python import path, e.g. `"path.to.my_extension.MyExtension"`.
- Extension class, e.g. `my_extension.MyExtension`.
```python
COMPONENTS = ComponentsSettings(
extensions=[
"path.to.my_extension.MyExtension",
StorybookExtension,
],
)
```
"""
autodiscover: Optional[bool] = None
"""
Toggle whether to run [autodiscovery](../../concepts/fundamentals/autodiscovery) at the Django server startup.
@ -647,6 +669,7 @@ defaults = ComponentsSettings(
debug_highlight_components=False,
debug_highlight_slots=False,
dynamic_component_name="dynamic",
extensions=[],
libraries=[], # E.g. ["mysite.components.forms", ...]
multiline_tags=True,
reload_on_file_change=False,
@ -709,6 +732,9 @@ class InternalSettings:
components_settings.dynamic_component_name, defaults.dynamic_component_name
),
libraries=default(components_settings.libraries, defaults.libraries),
# NOTE: Internally we store the extensions as a list of instances, but the user
# can pass in either a list of classes or a list of import strings.
extensions=self._prepare_extensions(components_settings), # type: ignore[arg-type]
multiline_tags=default(components_settings.multiline_tags, defaults.multiline_tags),
reload_on_file_change=self._prepare_reload_on_file_change(components_settings),
template_cache_size=default(components_settings.template_cache_size, defaults.template_cache_size),
@ -718,6 +744,33 @@ class InternalSettings:
tag_formatter=default(components_settings.tag_formatter, defaults.tag_formatter), # type: ignore[arg-type]
)
def _prepare_extensions(self, new_settings: ComponentsSettings) -> List["ComponentExtension"]:
extensions: Sequence[Union[Type["ComponentExtension"], str]] = default(
new_settings.extensions, cast(List[str], defaults.extensions)
)
# Prepend built-in extensions
from django_components.extensions.view import ViewExtension
extensions = [ViewExtension] + list(extensions)
# Extensions may be passed in either as classes or import strings.
extension_instances: List["ComponentExtension"] = []
for extension in extensions:
if isinstance(extension, str):
import_path, class_name = extension.rsplit(".", 1)
extension_module = import_module(import_path)
extension = cast(Type["ComponentExtension"], getattr(extension_module, class_name))
if isinstance(extension, type):
extension_instance = extension()
else:
extension_instances.append(extension)
extension_instances.append(extension_instance)
return extension_instances
def _prepare_reload_on_file_change(self, new_settings: ComponentsSettings) -> bool:
val = new_settings.reload_on_file_change
# TODO_REMOVE_IN_V1
@ -780,6 +833,10 @@ class InternalSettings:
def LIBRARIES(self) -> List[str]:
return self._settings.libraries # type: ignore[return-value]
@property
def EXTENSIONS(self) -> List["ComponentExtension"]:
return self._settings.extensions # type: ignore[return-value]
@property
def MULTILINE_TAGS(self) -> bool:
return self._settings.multiline_tags # type: ignore[return-value]