mirror of
https://github.com/django-components/django-components.git
synced 2025-08-08 16:27:59 +00:00
feat: allow different template settings for ComponentRegistries (#615)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
ee5c92ba00
commit
3b1f6088a0
10 changed files with 478 additions and 44 deletions
|
@ -5,6 +5,7 @@ import django
|
|||
|
||||
# Public API
|
||||
# isort: off
|
||||
from django_components.app_settings import ContextBehavior as ContextBehavior
|
||||
from django_components.autodiscover import (
|
||||
autodiscover as autodiscover,
|
||||
import_libraries as import_libraries,
|
||||
|
@ -17,6 +18,7 @@ from django_components.component_registry import (
|
|||
AlreadyRegistered as AlreadyRegistered,
|
||||
ComponentRegistry as ComponentRegistry,
|
||||
NotRegistered as NotRegistered,
|
||||
RegistrySettings as RegistrySettings,
|
||||
register as register,
|
||||
registry as registry,
|
||||
)
|
||||
|
|
|
@ -31,11 +31,14 @@ from django.utils.html import conditional_escape
|
|||
from django.utils.safestring import SafeString, mark_safe
|
||||
from django.views import View
|
||||
|
||||
from django_components.app_settings import ContextBehavior
|
||||
from django_components.component_media import ComponentMediaInput, MediaMeta
|
||||
from django_components.component_registry import registry
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
from django_components.component_registry import registry as registry_
|
||||
from django_components.context import (
|
||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
|
||||
_PARENT_COMP_CONTEXT_KEY,
|
||||
_REGISTRY_CONTEXT_KEY,
|
||||
_ROOT_CTX_CONTEXT_KEY,
|
||||
get_injected_context_var,
|
||||
make_isolated_context_copy,
|
||||
|
@ -70,6 +73,7 @@ from django_components.component_registry import registry as registry # NOQA
|
|||
# isort: on
|
||||
|
||||
RENDERED_COMMENT_TEMPLATE = "<!-- _RENDERED {name} -->"
|
||||
COMP_ONLY_FLAG = "only"
|
||||
|
||||
# Define TypeVars for args and kwargs
|
||||
ArgsType = TypeVar("ArgsType", bound=tuple, contravariant=True)
|
||||
|
@ -170,6 +174,7 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
component_id: Optional[str] = None,
|
||||
outer_context: Optional[Context] = None,
|
||||
fill_content: Optional[Dict[str, FillContent]] = None,
|
||||
registry: Optional[ComponentRegistry] = None, # noqa F811
|
||||
):
|
||||
# When user first instantiates the component class before calling
|
||||
# `render` or `render_to_response`, then we want to allow the render
|
||||
|
@ -189,6 +194,7 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
self.outer_context: Context = outer_context or Context()
|
||||
self.fill_content = fill_content or {}
|
||||
self.component_id = component_id or gen_id()
|
||||
self.registry = registry or registry_
|
||||
self._render_stack: Deque[RenderInput[ArgsType, KwargsType, SlotsType]] = deque()
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
|
@ -535,8 +541,10 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
|
||||
with context.update(
|
||||
{
|
||||
# Private context fields
|
||||
_ROOT_CTX_CONTEXT_KEY: self.outer_context,
|
||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY: updated_slots,
|
||||
_REGISTRY_CONTEXT_KEY: self.registry,
|
||||
# NOTE: Public API for variables accessible from within a component's template
|
||||
# See https://github.com/EmilStenstrom/django-components/issues/280#issuecomment-2081180940
|
||||
"component_vars": {
|
||||
|
@ -595,6 +603,7 @@ class ComponentNode(BaseNode):
|
|||
name: str,
|
||||
args: List[Expression],
|
||||
kwargs: RuntimeKwargs,
|
||||
registry: ComponentRegistry, # noqa F811
|
||||
isolated_context: bool = False,
|
||||
fill_nodes: Optional[List[FillNode]] = None,
|
||||
node_id: Optional[str] = None,
|
||||
|
@ -604,6 +613,7 @@ class ComponentNode(BaseNode):
|
|||
self.name = name
|
||||
self.isolated_context = isolated_context
|
||||
self.fill_nodes = fill_nodes or []
|
||||
self.registry = registry
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<ComponentNode: {}. Contents: {!r}>".format(
|
||||
|
@ -614,7 +624,7 @@ class ComponentNode(BaseNode):
|
|||
def render(self, context: Context) -> str:
|
||||
trace_msg("RENDR", "COMP", self.name, self.node_id)
|
||||
|
||||
component_cls: Type[Component] = registry.get(self.name)
|
||||
component_cls: Type[Component] = self.registry.get(self.name)
|
||||
|
||||
# Resolve FilterExpressions and Variables that were passed as args to the
|
||||
# component, then call component's context method
|
||||
|
@ -639,10 +649,11 @@ class ComponentNode(BaseNode):
|
|||
outer_context=context,
|
||||
fill_content=fill_content,
|
||||
component_id=self.node_id,
|
||||
registry=self.registry,
|
||||
)
|
||||
|
||||
# Prevent outer context from leaking into the template of the component
|
||||
if self.isolated_context:
|
||||
if self.isolated_context or self.registry.settings.CONTEXT_BEHAVIOR == ContextBehavior.ISOLATED:
|
||||
context = make_isolated_context_copy(context)
|
||||
|
||||
output = component._render(
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from typing import TYPE_CHECKING, Callable, Dict, NamedTuple, Optional, Set, Type, TypeVar
|
||||
from typing import TYPE_CHECKING, Callable, Dict, NamedTuple, Optional, Set, Type, TypeVar, Union
|
||||
|
||||
from django.template import Library
|
||||
|
||||
from django_components.app_settings import ContextBehavior, app_settings
|
||||
from django_components.library import is_tag_protected, mark_protected_tags, register_tag_from_formatter
|
||||
from django_components.tag_formatter import get_tag_formatter
|
||||
from django_components.tag_formatter import TagFormatterABC, get_tag_formatter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component
|
||||
|
@ -33,6 +34,16 @@ class ComponentRegistryEntry(NamedTuple):
|
|||
tag: str
|
||||
|
||||
|
||||
class RegistrySettings(NamedTuple):
|
||||
CONTEXT_BEHAVIOR: Optional[ContextBehavior] = None
|
||||
TAG_FORMATTER: Optional[Union["TagFormatterABC", str]] = None
|
||||
|
||||
|
||||
class InternalRegistrySettings(NamedTuple):
|
||||
CONTEXT_BEHAVIOR: ContextBehavior
|
||||
TAG_FORMATTER: Union["TagFormatterABC", str]
|
||||
|
||||
|
||||
class ComponentRegistry:
|
||||
"""
|
||||
Manages which components can be used in the template tags.
|
||||
|
@ -66,10 +77,16 @@ class ComponentRegistry:
|
|||
```
|
||||
"""
|
||||
|
||||
def __init__(self, library: Optional[Library] = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
library: Optional[Library] = None,
|
||||
settings: Optional[Union[RegistrySettings, Callable[["ComponentRegistry"], RegistrySettings]]] = None,
|
||||
) -> None:
|
||||
self._registry: Dict[str, ComponentRegistryEntry] = {} # component name -> component_entry mapping
|
||||
self._tags: Dict[str, Set[str]] = {} # tag -> list[component names]
|
||||
self._library = library
|
||||
self._settings_input = settings
|
||||
self._settings: Optional[Callable[[], InternalRegistrySettings]] = None
|
||||
|
||||
@property
|
||||
def library(self) -> Library:
|
||||
|
@ -91,6 +108,37 @@ class ComponentRegistry:
|
|||
lib = self._library = tag_library
|
||||
return lib
|
||||
|
||||
@property
|
||||
def settings(self) -> InternalRegistrySettings:
|
||||
# This is run on subsequent calls
|
||||
if self._settings is not None:
|
||||
# NOTE: Registry's settings can be a function, so we always take
|
||||
# the latest value from Django's settings.
|
||||
settings = self._settings()
|
||||
|
||||
# First-time initialization
|
||||
# NOTE: We allow the settings to be given as a getter function
|
||||
# so the settings can respond to changes.
|
||||
# So we wrapp that in our getter, which assigns default values from the settings.
|
||||
else:
|
||||
|
||||
def get_settings() -> InternalRegistrySettings:
|
||||
if callable(self._settings_input):
|
||||
settings_input: Optional[RegistrySettings] = self._settings_input(self)
|
||||
else:
|
||||
settings_input = self._settings_input
|
||||
|
||||
return InternalRegistrySettings(
|
||||
CONTEXT_BEHAVIOR=(settings_input and settings_input.CONTEXT_BEHAVIOR)
|
||||
or app_settings.CONTEXT_BEHAVIOR,
|
||||
TAG_FORMATTER=(settings_input and settings_input.TAG_FORMATTER) or app_settings.TAG_FORMATTER,
|
||||
)
|
||||
|
||||
self._settings = get_settings
|
||||
settings = self._settings()
|
||||
|
||||
return settings
|
||||
|
||||
def register(self, name: str, component: Type["Component"]) -> None:
|
||||
"""
|
||||
Register a component with this registry under the given name.
|
||||
|
@ -243,8 +291,8 @@ class ComponentRegistry:
|
|||
# Lazily import to avoid circular dependencies
|
||||
from django_components.templatetags.component_tags import component as do_component
|
||||
|
||||
formatter = get_tag_formatter()
|
||||
tag = register_tag_from_formatter(self.library, do_component, formatter, comp_name)
|
||||
formatter = get_tag_formatter(self)
|
||||
tag = register_tag_from_formatter(self, do_component, formatter, comp_name)
|
||||
|
||||
return ComponentRegistryEntry(cls=component, tag=tag)
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ from django_components.utils import find_last_index
|
|||
|
||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY = "_DJANGO_COMPONENTS_FILLED_SLOTS"
|
||||
_ROOT_CTX_CONTEXT_KEY = "_DJANGO_COMPONENTS_ROOT_CTX"
|
||||
_REGISTRY_CONTEXT_KEY = "_DJANGO_COMPONENTS_REGISTRY"
|
||||
_PARENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_PARENT_COMP"
|
||||
_CURRENT_COMP_CONTEXT_KEY = "_DJANGO_COMPONENTS_CURRENT_COMP"
|
||||
_INJECT_CONTEXT_KEY_PREFIX = "_DJANGO_COMPONENTS_INJECT__"
|
||||
|
@ -38,6 +39,7 @@ def make_isolated_context_copy(context: Context) -> Context:
|
|||
|
||||
# Pass through our internal keys
|
||||
context_copy[_FILLED_SLOTS_CONTENT_CONTEXT_KEY] = context.get(_FILLED_SLOTS_CONTENT_CONTEXT_KEY, {})
|
||||
context_copy[_REGISTRY_CONTEXT_KEY] = context.get(_REGISTRY_CONTEXT_KEY, None)
|
||||
if _ROOT_CTX_CONTEXT_KEY in context:
|
||||
context_copy[_ROOT_CTX_CONTEXT_KEY] = context[_ROOT_CTX_CONTEXT_KEY]
|
||||
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
"""Module for interfacing with Django's Library (`django.template.library`)"""
|
||||
|
||||
from typing import Callable, List, Optional
|
||||
from typing import TYPE_CHECKING, Callable, List, Optional
|
||||
|
||||
from django.template.base import Node, Parser, Token
|
||||
from django.template.library import Library
|
||||
|
||||
from django_components.tag_formatter import InternalTagFormatter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
|
||||
|
||||
class TagProtectedError(Exception):
|
||||
pass
|
||||
|
@ -28,25 +31,25 @@ as they would conflict with other tags in the Library.
|
|||
|
||||
|
||||
def register_tag(
|
||||
library: Library,
|
||||
registry: "ComponentRegistry",
|
||||
tag: str,
|
||||
tag_fn: Callable[[Parser, Token, str], Node],
|
||||
tag_fn: Callable[[Parser, Token, "ComponentRegistry", str], Node],
|
||||
) -> None:
|
||||
# Register inline tag
|
||||
if is_tag_protected(library, tag):
|
||||
if is_tag_protected(registry.library, tag):
|
||||
raise TagProtectedError('Cannot register tag "%s", this tag name is protected' % tag)
|
||||
else:
|
||||
library.tag(tag, lambda parser, token: tag_fn(parser, token, tag))
|
||||
registry.library.tag(tag, lambda parser, token: tag_fn(parser, token, registry, tag))
|
||||
|
||||
|
||||
def register_tag_from_formatter(
|
||||
library: Library,
|
||||
tag_fn: Callable[[Parser, Token, str], Node],
|
||||
registry: "ComponentRegistry",
|
||||
tag_fn: Callable[[Parser, Token, "ComponentRegistry", str], Node],
|
||||
formatter: InternalTagFormatter,
|
||||
component_name: str,
|
||||
) -> str:
|
||||
tag = formatter.start_tag(component_name)
|
||||
register_tag(library, tag, tag_fn)
|
||||
register_tag(registry, tag, tag_fn)
|
||||
return tag
|
||||
|
||||
|
||||
|
|
|
@ -3,7 +3,22 @@ import json
|
|||
import re
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Generic, List, Mapping, NamedTuple, Optional, Protocol, Set, Tuple, Type, TypeVar, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Generic,
|
||||
List,
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Protocol,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from django.template import Context, Template
|
||||
from django.template.base import FilterExpression, Node, NodeList, Parser, TextNode
|
||||
|
@ -11,16 +26,20 @@ from django.template.defaulttags import CommentNode
|
|||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.utils.safestring import SafeString, mark_safe
|
||||
|
||||
from django_components.app_settings import ContextBehavior, app_settings
|
||||
from django_components.app_settings import ContextBehavior
|
||||
from django_components.context import (
|
||||
_FILLED_SLOTS_CONTENT_CONTEXT_KEY,
|
||||
_INJECT_CONTEXT_KEY_PREFIX,
|
||||
_REGISTRY_CONTEXT_KEY,
|
||||
_ROOT_CTX_CONTEXT_KEY,
|
||||
)
|
||||
from django_components.expression import RuntimeKwargs, is_identifier
|
||||
from django_components.logger import trace_msg
|
||||
from django_components.node import BaseNode, NodeTraverse, nodelist_has_content, walk_nodelist
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
|
||||
TSlotData = TypeVar("TSlotData", bound=Mapping, contravariant=True)
|
||||
|
||||
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
||||
|
@ -215,12 +234,13 @@ class SlotNode(BaseNode):
|
|||
if not slot_fill.is_filled:
|
||||
return context
|
||||
|
||||
if app_settings.CONTEXT_BEHAVIOR == ContextBehavior.DJANGO:
|
||||
registry: "ComponentRegistry" = context[_REGISTRY_CONTEXT_KEY]
|
||||
if registry.settings.CONTEXT_BEHAVIOR == ContextBehavior.DJANGO:
|
||||
return context
|
||||
elif app_settings.CONTEXT_BEHAVIOR == ContextBehavior.ISOLATED:
|
||||
elif registry.settings.CONTEXT_BEHAVIOR == ContextBehavior.ISOLATED:
|
||||
return context[_ROOT_CTX_CONTEXT_KEY]
|
||||
else:
|
||||
raise ValueError(f"Unknown value for CONTEXT_BEHAVIOR: '{app_settings.CONTEXT_BEHAVIOR}'")
|
||||
raise ValueError(f"Unknown value for CONTEXT_BEHAVIOR: '{registry.settings.CONTEXT_BEHAVIOR}'")
|
||||
|
||||
def resolve_kwargs(
|
||||
self,
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import abc
|
||||
import re
|
||||
from typing import List, NamedTuple
|
||||
from typing import TYPE_CHECKING, List, NamedTuple
|
||||
|
||||
from django.template import TemplateSyntaxError
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from django_components.app_settings import app_settings
|
||||
from django_components.expression import resolve_string
|
||||
from django_components.template_parser import VAR_CHARS
|
||||
from django_components.utils import is_str_wrapped_in_quotes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
|
||||
|
||||
TAG_RE = re.compile(r"^[{chars}]+$".format(chars=VAR_CHARS))
|
||||
|
||||
|
||||
|
@ -201,10 +204,10 @@ class ShorthandComponentFormatter(TagFormatterABC):
|
|||
return TagResult(name, tokens)
|
||||
|
||||
|
||||
def get_tag_formatter() -> InternalTagFormatter:
|
||||
def get_tag_formatter(registry: "ComponentRegistry") -> InternalTagFormatter:
|
||||
"""Returns an instance of the currently configured component tag formatter."""
|
||||
# Allow users to configure the component TagFormatter
|
||||
formatter_cls_or_str = app_settings.TAG_FORMATTER
|
||||
formatter_cls_or_str = registry.settings.TAG_FORMATTER
|
||||
|
||||
if isinstance(formatter_cls_or_str, str):
|
||||
tag_formatter: TagFormatterABC = import_string(formatter_cls_or_str)
|
||||
|
|
|
@ -6,9 +6,8 @@ from django.template.exceptions import TemplateSyntaxError
|
|||
from django.utils.safestring import SafeString, mark_safe
|
||||
from django.utils.text import smart_split
|
||||
|
||||
from django_components.app_settings import ContextBehavior, app_settings
|
||||
from django_components.attributes import HTML_ATTRS_ATTRS_KEY, HTML_ATTRS_DEFAULTS_KEY, HtmlAttrsNode
|
||||
from django_components.component import RENDERED_COMMENT_TEMPLATE, ComponentNode
|
||||
from django_components.component import COMP_ONLY_FLAG, RENDERED_COMMENT_TEMPLATE, ComponentNode
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
from django_components.component_registry import registry as component_registry
|
||||
from django_components.expression import (
|
||||
|
@ -204,7 +203,7 @@ def fill(parser: Parser, token: Token) -> FillNode:
|
|||
return fill_node
|
||||
|
||||
|
||||
def component(parser: Parser, token: Token, tag_name: str) -> ComponentNode:
|
||||
def component(parser: Parser, token: Token, registry: ComponentRegistry, tag_name: str) -> ComponentNode:
|
||||
"""
|
||||
To give the component access to the template context:
|
||||
```#!htmldjango {% component "name" positional_arg keyword_arg=value ... %}```
|
||||
|
@ -221,7 +220,7 @@ def component(parser: Parser, token: Token, tag_name: str) -> ComponentNode:
|
|||
bits = token.split_contents()
|
||||
|
||||
# Let the TagFormatter pre-process the tokens
|
||||
formatter = get_tag_formatter()
|
||||
formatter = get_tag_formatter(registry)
|
||||
result = formatter.parse([*bits])
|
||||
end_tag = formatter.end_tag(result.component_name)
|
||||
|
||||
|
@ -234,14 +233,14 @@ def component(parser: Parser, token: Token, tag_name: str) -> ComponentNode:
|
|||
parser,
|
||||
token,
|
||||
params=True, # Allow many args
|
||||
flags=["only"],
|
||||
flags=[COMP_ONLY_FLAG],
|
||||
keywordonly_kwargs=True,
|
||||
repeatable_kwargs=False,
|
||||
end_tag=end_tag,
|
||||
)
|
||||
|
||||
# Check for isolated context keyword
|
||||
isolated_context = tag.flags["only"] or app_settings.CONTEXT_BEHAVIOR == ContextBehavior.ISOLATED
|
||||
isolated_context = tag.flags[COMP_ONLY_FLAG]
|
||||
|
||||
trace_msg("PARSE", "COMP", result.component_name, tag.id)
|
||||
|
||||
|
@ -260,6 +259,7 @@ def component(parser: Parser, token: Token, tag_name: str) -> ComponentNode:
|
|||
isolated_context=isolated_context,
|
||||
fill_nodes=fill_nodes,
|
||||
node_id=tag.id,
|
||||
registry=registry,
|
||||
)
|
||||
|
||||
trace_msg("PARSE", "COMP", result.component_name, tag.id, "...Done!")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue