mirror of
https://github.com/django-components/django-components.git
synced 2025-10-01 09:51:11 +00:00
refactor: allow to call Component.inject() outside of render (#1414)
This commit is contained in:
parent
1578996b21
commit
b3ea50572d
13 changed files with 614 additions and 236 deletions
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -1,5 +1,58 @@
|
||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
|
## v0.141.6
|
||||||
|
|
||||||
|
#### Fix
|
||||||
|
|
||||||
|
- Fix error that occured when calling `Component.inject()` inside loops:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
data = self.inject("my_provide")
|
||||||
|
return {"data": data}
|
||||||
|
```
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" data=data %}
|
||||||
|
{% for i in range(10) %}
|
||||||
|
{% component "my_component" / %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endprovide %}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Allow to call `Component.inject()` outside of the rendering:
|
||||||
|
|
||||||
|
```py
|
||||||
|
comp = None
|
||||||
|
|
||||||
|
class MyComponent(Component):
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal comp
|
||||||
|
comp = self
|
||||||
|
|
||||||
|
template_str = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" data=data %}
|
||||||
|
{% component "my_component" / %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
assert comp is not None
|
||||||
|
|
||||||
|
injected = comp.inject("my_provide")
|
||||||
|
assert injected.key == "hi"
|
||||||
|
assert injected.data == "data"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Refactor
|
||||||
|
|
||||||
|
- Removed circular references to the Component instances. Component instances
|
||||||
|
are now garbage collected unless you keep a reference to them.
|
||||||
|
|
||||||
## v0.141.5
|
## v0.141.5
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "django_components"
|
name = "django_components"
|
||||||
version = "0.141.5"
|
version = "0.141.6"
|
||||||
requires-python = ">=3.8, <4.0"
|
requires-python = ">=3.8, <4.0"
|
||||||
description = "A way to create simple reusable template components in Django."
|
description = "A way to create simple reusable template components in Django."
|
||||||
keywords = ["django", "components", "css", "js", "html"]
|
keywords = ["django", "components", "css", "js", "html"]
|
||||||
|
@ -180,8 +180,9 @@ known-first-party = ["django_components"]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
exclude = [
|
exclude = [
|
||||||
"test_structures",
|
|
||||||
"build",
|
"build",
|
||||||
|
"sampleproject",
|
||||||
|
"test_structures",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
|
|
|
@ -19,7 +19,7 @@ from typing import (
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
)
|
)
|
||||||
from weakref import ReferenceType, WeakValueDictionary, finalize
|
from weakref import ReferenceType, WeakValueDictionary, finalize, ref
|
||||||
|
|
||||||
from django.forms.widgets import Media as MediaCls
|
from django.forms.widgets import Media as MediaCls
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
@ -61,9 +61,10 @@ from django_components.perfutil.component import (
|
||||||
ComponentRenderer,
|
ComponentRenderer,
|
||||||
OnComponentRenderedResult,
|
OnComponentRenderedResult,
|
||||||
component_context_cache,
|
component_context_cache,
|
||||||
|
component_instance_cache,
|
||||||
component_post_render,
|
component_post_render,
|
||||||
)
|
)
|
||||||
from django_components.perfutil.provide import register_provide_reference, unregister_provide_reference
|
from django_components.perfutil.provide import register_provide_reference, unlink_component_from_provide_on_gc
|
||||||
from django_components.provide import get_injected_context_var
|
from django_components.provide import get_injected_context_var
|
||||||
from django_components.slots import (
|
from django_components.slots import (
|
||||||
Slot,
|
Slot,
|
||||||
|
@ -102,9 +103,11 @@ COMP_ONLY_FLAG = "only"
|
||||||
if sys.version_info >= (3, 9):
|
if sys.version_info >= (3, 9):
|
||||||
AllComponents = List[ReferenceType[Type["Component"]]]
|
AllComponents = List[ReferenceType[Type["Component"]]]
|
||||||
CompHashMapping = WeakValueDictionary[str, Type["Component"]]
|
CompHashMapping = WeakValueDictionary[str, Type["Component"]]
|
||||||
|
ComponentRef = ReferenceType["Component"]
|
||||||
else:
|
else:
|
||||||
AllComponents = List[ReferenceType]
|
AllComponents = List[ReferenceType]
|
||||||
CompHashMapping = WeakValueDictionary
|
CompHashMapping = WeakValueDictionary
|
||||||
|
ComponentRef = ReferenceType
|
||||||
|
|
||||||
|
|
||||||
OnRenderGenerator = Generator[
|
OnRenderGenerator = Generator[
|
||||||
|
@ -516,7 +519,7 @@ class ComponentMeta(ComponentMediaMeta):
|
||||||
# Internal data that are made available within the component's template
|
# Internal data that are made available within the component's template
|
||||||
@dataclass
|
@dataclass
|
||||||
class ComponentContext:
|
class ComponentContext:
|
||||||
component: "Component"
|
component: ComponentRef
|
||||||
component_path: List[str]
|
component_path: List[str]
|
||||||
template_name: Optional[str]
|
template_name: Optional[str]
|
||||||
default_slot: Optional[str]
|
default_slot: Optional[str]
|
||||||
|
@ -527,6 +530,12 @@ class ComponentContext:
|
||||||
post_render_callbacks: Dict[str, Callable[[Optional[str], Optional[Exception]], OnComponentRenderedResult]]
|
post_render_callbacks: Dict[str, Callable[[Optional[str], Optional[Exception]], OnComponentRenderedResult]]
|
||||||
|
|
||||||
|
|
||||||
|
def on_component_garbage_collected(component_id: str) -> None:
|
||||||
|
"""Finalizer function to be called when a Component object is garbage collected."""
|
||||||
|
unlink_component_from_provide_on_gc(component_id)
|
||||||
|
component_context_cache.pop(component_id, None)
|
||||||
|
|
||||||
|
|
||||||
class Component(metaclass=ComponentMeta):
|
class Component(metaclass=ComponentMeta):
|
||||||
# #####################################
|
# #####################################
|
||||||
# PUBLIC API (Configurable by users)
|
# PUBLIC API (Configurable by users)
|
||||||
|
@ -2314,6 +2323,9 @@ class Component(metaclass=ComponentMeta):
|
||||||
self.registry = default(registry, registry_)
|
self.registry = default(registry, registry_)
|
||||||
self.node = node
|
self.node = node
|
||||||
|
|
||||||
|
# Run finalizer when component is garbage collected
|
||||||
|
finalize(self, on_component_garbage_collected, self.id)
|
||||||
|
|
||||||
extensions._init_component_instance(self)
|
extensions._init_component_instance(self)
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||||
|
@ -2940,7 +2952,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
As the `{{ message }}` is taken from the "my_provide" provider.
|
As the `{{ message }}` is taken from the "my_provide" provider.
|
||||||
"""
|
"""
|
||||||
return get_injected_context_var(self.name, self.context, key, default)
|
return get_injected_context_var(self.id, self.name, key, default)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_view(cls, **initkwargs: Any) -> ViewFn:
|
def as_view(cls, **initkwargs: Any) -> ViewFn:
|
||||||
|
@ -3302,26 +3314,34 @@ class Component(metaclass=ComponentMeta):
|
||||||
node: Optional["ComponentNode"] = None,
|
node: Optional["ComponentNode"] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
component_name = _get_component_name(cls, registered_name)
|
component_name = _get_component_name(cls, registered_name)
|
||||||
|
render_id = _gen_component_id()
|
||||||
|
|
||||||
# Modify the error to display full component path (incl. slots)
|
# Modify the error to display full component path (incl. slots)
|
||||||
with component_error_message([component_name]):
|
with component_error_message([component_name]):
|
||||||
return cls._render_impl(
|
try:
|
||||||
context=context,
|
return cls._render_impl(
|
||||||
args=args,
|
render_id=render_id,
|
||||||
kwargs=kwargs,
|
context=context,
|
||||||
slots=slots,
|
args=args,
|
||||||
deps_strategy=deps_strategy,
|
kwargs=kwargs,
|
||||||
request=request,
|
slots=slots,
|
||||||
outer_context=outer_context,
|
deps_strategy=deps_strategy,
|
||||||
# TODO_v2 - Remove `registered_name` and `registry`
|
request=request,
|
||||||
registry=registry,
|
outer_context=outer_context,
|
||||||
registered_name=registered_name,
|
# TODO_v2 - Remove `registered_name` and `registry`
|
||||||
node=node,
|
registry=registry,
|
||||||
)
|
registered_name=registered_name,
|
||||||
|
node=node,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Clean up if rendering fails
|
||||||
|
component_instance_cache.pop(render_id, None)
|
||||||
|
raise e from None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _render_impl(
|
def _render_impl(
|
||||||
comp_cls,
|
comp_cls,
|
||||||
|
render_id: str,
|
||||||
context: Optional[Union[Dict[str, Any], Context]] = None,
|
context: Optional[Union[Dict[str, Any], Context]] = None,
|
||||||
args: Optional[Any] = None,
|
args: Optional[Any] = None,
|
||||||
kwargs: Optional[Any] = None,
|
kwargs: Optional[Any] = None,
|
||||||
|
@ -3348,7 +3368,8 @@ class Component(metaclass=ComponentMeta):
|
||||||
if request is None:
|
if request is None:
|
||||||
_, parent_comp_ctx = _get_parent_component_context(context)
|
_, parent_comp_ctx = _get_parent_component_context(context)
|
||||||
if parent_comp_ctx:
|
if parent_comp_ctx:
|
||||||
request = parent_comp_ctx.component.request
|
parent_comp = parent_comp_ctx.component()
|
||||||
|
request = parent_comp and parent_comp.request
|
||||||
|
|
||||||
component_name = _get_component_name(comp_cls, registered_name)
|
component_name = _get_component_name(comp_cls, registered_name)
|
||||||
|
|
||||||
|
@ -3371,8 +3392,6 @@ class Component(metaclass=ComponentMeta):
|
||||||
if not isinstance(context, (Context, RequestContext)):
|
if not isinstance(context, (Context, RequestContext)):
|
||||||
context = RequestContext(request, context) if request else Context(context)
|
context = RequestContext(request, context) if request else Context(context)
|
||||||
|
|
||||||
render_id = _gen_component_id()
|
|
||||||
|
|
||||||
component = comp_cls(
|
component = comp_cls(
|
||||||
id=render_id,
|
id=render_id,
|
||||||
args=args_list,
|
args=args_list,
|
||||||
|
@ -3446,11 +3465,14 @@ class Component(metaclass=ComponentMeta):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register the component to provide
|
# Register the component to provide
|
||||||
register_provide_reference(context, render_id)
|
register_provide_reference(context, component)
|
||||||
|
|
||||||
# This is data that will be accessible (internally) from within the component's template
|
# This is data that will be accessible (internally) from within the component's template.
|
||||||
|
# NOTE: Be careful with the context - Do not store a strong reference to the component,
|
||||||
|
# because that would prevent the component from being garbage collected.
|
||||||
|
# TODO: Test that ComponentContext and Component are garbage collected after render.
|
||||||
component_ctx = ComponentContext(
|
component_ctx = ComponentContext(
|
||||||
component=component,
|
component=ref(component),
|
||||||
component_path=component_path,
|
component_path=component_path,
|
||||||
# Template name is set only once we've resolved the component's Template instance.
|
# Template name is set only once we've resolved the component's Template instance.
|
||||||
template_name=None,
|
template_name=None,
|
||||||
|
@ -3477,7 +3499,7 @@ class Component(metaclass=ComponentMeta):
|
||||||
# 3. Call data methods
|
# 3. Call data methods
|
||||||
######################################
|
######################################
|
||||||
|
|
||||||
template_data, js_data, css_data = component._call_data_methods(context, args_list, kwargs_dict)
|
template_data, js_data, css_data = component._call_data_methods(args_list, kwargs_dict)
|
||||||
|
|
||||||
extensions.on_component_data(
|
extensions.on_component_data(
|
||||||
OnComponentDataContext(
|
OnComponentDataContext(
|
||||||
|
@ -3589,12 +3611,25 @@ class Component(metaclass=ComponentMeta):
|
||||||
js_input_hash=js_input_hash,
|
js_input_hash=js_input_hash,
|
||||||
)
|
)
|
||||||
|
|
||||||
# This is triggered when a component is rendered, but the component's parents
|
# `on_component_rendered` is triggered when a component is rendered.
|
||||||
# may not have been rendered yet.
|
# The component's parent(s) may not be fully rendered yet.
|
||||||
|
#
|
||||||
|
# NOTE: Inside `on_component_rendered`, we access the component indirectly via `component_instance_cache`.
|
||||||
|
# This is so that the function does not directly hold a strong reference to the component instance,
|
||||||
|
# so that the component instance can be garbage collected.
|
||||||
|
component_instance_cache[render_id] = component
|
||||||
|
|
||||||
def on_component_rendered(
|
def on_component_rendered(
|
||||||
html: Optional[str],
|
html: Optional[str],
|
||||||
error: Optional[Exception],
|
error: Optional[Exception],
|
||||||
) -> OnComponentRenderedResult:
|
) -> OnComponentRenderedResult:
|
||||||
|
# NOTE: We expect `on_component_rendered` to be called only once,
|
||||||
|
# so we can release the strong reference to the component instance.
|
||||||
|
# This way, the component instance will persist only if the user keeps a reference to it.
|
||||||
|
component = component_instance_cache.pop(render_id, None)
|
||||||
|
if component is None:
|
||||||
|
raise RuntimeError("Component has been garbage collected")
|
||||||
|
|
||||||
# Allow the user to either:
|
# Allow the user to either:
|
||||||
# - Override/modify the rendered HTML by returning new value
|
# - Override/modify the rendered HTML by returning new value
|
||||||
# - Raise an exception to discard the HTML and bubble up error
|
# - Raise an exception to discard the HTML and bubble up error
|
||||||
|
@ -3608,10 +3643,6 @@ class Component(metaclass=ComponentMeta):
|
||||||
error = new_error
|
error = new_error
|
||||||
html = None
|
html = None
|
||||||
|
|
||||||
# Remove component from caches
|
|
||||||
del component_context_cache[render_id]
|
|
||||||
unregister_provide_reference(render_id)
|
|
||||||
|
|
||||||
# Allow extensions to either:
|
# Allow extensions to either:
|
||||||
# - Override/modify the rendered HTML by returning new value
|
# - Override/modify the rendered HTML by returning new value
|
||||||
# - Raise an exception to discard the HTML and bubble up error
|
# - Raise an exception to discard the HTML and bubble up error
|
||||||
|
@ -3759,7 +3790,6 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
def _call_data_methods(
|
def _call_data_methods(
|
||||||
self,
|
self,
|
||||||
context: Context,
|
|
||||||
# TODO_V2 - Remove `raw_args` and `raw_kwargs` in v2
|
# TODO_V2 - Remove `raw_args` and `raw_kwargs` in v2
|
||||||
raw_args: List,
|
raw_args: List,
|
||||||
raw_kwargs: Dict,
|
raw_kwargs: Dict,
|
||||||
|
@ -3779,11 +3809,11 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
# TODO - Enable JS and CSS vars - expose, and document
|
# TODO - Enable JS and CSS vars - expose, and document
|
||||||
# JS data
|
# JS data
|
||||||
maybe_js_data = self.get_js_data(self.args, self.kwargs, self.slots, context)
|
maybe_js_data = self.get_js_data(self.args, self.kwargs, self.slots, self.context)
|
||||||
js_data = to_dict(default(maybe_js_data, {}))
|
js_data = to_dict(default(maybe_js_data, {}))
|
||||||
|
|
||||||
# CSS data
|
# CSS data
|
||||||
maybe_css_data = self.get_css_data(self.args, self.kwargs, self.slots, context)
|
maybe_css_data = self.get_css_data(self.args, self.kwargs, self.slots, self.context)
|
||||||
css_data = to_dict(default(maybe_css_data, {}))
|
css_data = to_dict(default(maybe_css_data, {}))
|
||||||
|
|
||||||
# Validate outputs
|
# Validate outputs
|
||||||
|
@ -4005,7 +4035,9 @@ class ComponentNode(BaseNode):
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def _get_parent_component_context(context: Context) -> Union[Tuple[None, None], Tuple[str, ComponentContext]]:
|
def _get_parent_component_context(
|
||||||
|
context: Union[Context, Mapping],
|
||||||
|
) -> Union[Tuple[None, None], Tuple[str, ComponentContext]]:
|
||||||
parent_id = context.get(_COMPONENT_CONTEXT_KEY, None)
|
parent_id = context.get(_COMPONENT_CONTEXT_KEY, None)
|
||||||
if parent_id is None:
|
if parent_id is None:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
|
@ -14,6 +14,7 @@ from typing import (
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
from weakref import ref
|
||||||
|
|
||||||
import django.urls
|
import django.urls
|
||||||
from django.template import Context, Origin, Template
|
from django.template import Context, Origin, Template
|
||||||
|
@ -255,18 +256,25 @@ class ExtensionComponentConfig:
|
||||||
component_class: Type["Component"]
|
component_class: Type["Component"]
|
||||||
"""The [`Component`](./api.md#django_components.Component) class that this extension is defined on."""
|
"""The [`Component`](./api.md#django_components.Component) class that this extension is defined on."""
|
||||||
|
|
||||||
component: "Component"
|
@property
|
||||||
"""
|
def component(self) -> "Component":
|
||||||
When a [`Component`](./api.md#django_components.Component) is instantiated,
|
"""
|
||||||
also the nested extension classes (such as `Component.View`) are instantiated,
|
When a [`Component`](./api.md#django_components.Component) is instantiated,
|
||||||
receiving the component instance as an argument.
|
also the nested extension classes (such as `Component.View`) are instantiated,
|
||||||
|
receiving the component instance as an argument.
|
||||||
|
|
||||||
This attribute holds the owner [`Component`](./api.md#django_components.Component) instance
|
This attribute holds the owner [`Component`](./api.md#django_components.Component) instance
|
||||||
that this extension is defined on.
|
that this extension is defined on.
|
||||||
"""
|
"""
|
||||||
|
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: "Component") -> None:
|
||||||
self.component = component
|
# NOTE: Use weak reference to avoid a circular reference between the component instance
|
||||||
|
# and the extension class.
|
||||||
|
self._component_ref = ref(component)
|
||||||
|
|
||||||
|
|
||||||
# TODO_v1 - Delete
|
# TODO_v1 - Delete
|
||||||
|
|
|
@ -73,6 +73,9 @@ def _extract_defaults(defaults: Optional[Type]) -> List[ComponentDefaultField]:
|
||||||
|
|
||||||
default_field = getattr(defaults, default_field_key)
|
default_field = getattr(defaults, default_field_key)
|
||||||
|
|
||||||
|
if isinstance(default_field, property):
|
||||||
|
continue
|
||||||
|
|
||||||
# If the field was defined with dataclass.field(), take the default / factory from there.
|
# If the field was defined with dataclass.field(), take the default / factory from there.
|
||||||
if isinstance(default_field, Field):
|
if isinstance(default_field, Field):
|
||||||
if default_field.default is not MISSING:
|
if default_field.default is not MISSING:
|
||||||
|
|
|
@ -161,6 +161,9 @@ class ComponentView(ExtensionComponentConfig, View):
|
||||||
ComponentExtension.ComponentConfig.__init__(self, component)
|
ComponentExtension.ComponentConfig.__init__(self, component)
|
||||||
View.__init__(self, **kwargs)
|
View.__init__(self, **kwargs)
|
||||||
|
|
||||||
|
# TODO_v1 - Remove. Superseded by `component_cls`. This was used for backwards compatibility.
|
||||||
|
self.component = component
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self) -> str:
|
def url(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django_components.constants import COMP_ID_LENGTH
|
||||||
from django_components.util.exception import component_error_message
|
from django_components.util.exception import component_error_message
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components.component import ComponentContext, OnRenderGenerator
|
from django_components.component import Component, ComponentContext, OnRenderGenerator
|
||||||
|
|
||||||
OnComponentRenderedResult = Tuple[Optional[str], Optional[Exception]]
|
OnComponentRenderedResult = Tuple[Optional[str], Optional[Exception]]
|
||||||
|
|
||||||
|
@ -30,6 +30,15 @@ OnComponentRenderedResult = Tuple[Optional[str], Optional[Exception]]
|
||||||
# is only a key to this dictionary.
|
# is only a key to this dictionary.
|
||||||
component_context_cache: Dict[str, "ComponentContext"] = {}
|
component_context_cache: Dict[str, "ComponentContext"] = {}
|
||||||
|
|
||||||
|
# ComponentID -> Component instance mapping
|
||||||
|
# This is used so that we can access the component instance from inside `on_component_rendered()`,
|
||||||
|
# to call `Component.on_render_after()`.
|
||||||
|
# These are strong references to ensure that the Component instance stays alive until after
|
||||||
|
# `on_component_rendered()` has been called.
|
||||||
|
# After that, we release the reference. If user does not keep a reference to the component,
|
||||||
|
# it will be garbage collected.
|
||||||
|
component_instance_cache: Dict[str, "Component"] = {}
|
||||||
|
|
||||||
|
|
||||||
class ComponentPart(NamedTuple):
|
class ComponentPart(NamedTuple):
|
||||||
"""Queue item where a component is nested in another component."""
|
"""Queue item where a component is nested in another component."""
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
"""This module contains optimizations for the `{% provide %}` feature."""
|
"""This module contains optimizations for the `{% provide %}` feature."""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Dict, Generator, NamedTuple, Set
|
from typing import TYPE_CHECKING, Dict, Generator, NamedTuple, Set, cast
|
||||||
|
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
|
|
||||||
from django_components.context import _INJECT_CONTEXT_KEY_PREFIX
|
from django_components.context import _INJECT_CONTEXT_KEY_PREFIX
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django_components.component import Component
|
||||||
|
|
||||||
# Originally, when `{% provide %}` was used, the provided data was passed down
|
# Originally, when `{% provide %}` was used, the provided data was passed down
|
||||||
# through the Context object.
|
# through the Context object.
|
||||||
#
|
#
|
||||||
|
@ -77,77 +81,103 @@ from django_components.context import _INJECT_CONTEXT_KEY_PREFIX
|
||||||
# outside of the Context object, to make it easier to debug the data flow.
|
# outside of the Context object, to make it easier to debug the data flow.
|
||||||
provide_cache: Dict[str, NamedTuple] = {}
|
provide_cache: Dict[str, NamedTuple] = {}
|
||||||
|
|
||||||
# Keep track of how many components are referencing each provided data.
|
# Given a `{% provide %}` instance, keep track of which components are referencing it.
|
||||||
provide_references: Dict[str, Set[str]] = {}
|
# ProvideID -> Component[]
|
||||||
|
# NOTE: We manually clean up the entries when either:
|
||||||
|
# - `{% provide %}` ends and there are no more references to it
|
||||||
|
# - The last component that referenced it is garbage collected
|
||||||
|
provide_references: Dict[str, Set[str]] = defaultdict(set)
|
||||||
|
|
||||||
# Keep track of all the listeners that are referencing any provided data.
|
# The opposite - Given a component, keep track of which `{% provide %}` instances it is referencing.
|
||||||
all_reference_ids: Set[str] = set()
|
# Component -> ProvideID[]
|
||||||
|
# NOTE: We manually clean up the entries when components are garbage collected.
|
||||||
|
component_provides: Dict[str, Dict[str, str]] = defaultdict(dict)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def managed_provide_cache(provide_id: str) -> Generator[None, None, None]:
|
def managed_provide_cache(provide_id: str) -> Generator[None, None, None]:
|
||||||
all_reference_ids_before = all_reference_ids.copy()
|
|
||||||
|
|
||||||
def cache_cleanup() -> None:
|
|
||||||
# Lastly, remove provided data from the cache that was generated during this run,
|
|
||||||
# IF there are no more references to it.
|
|
||||||
if provide_id in provide_references and not provide_references[provide_id]:
|
|
||||||
provide_references.pop(provide_id)
|
|
||||||
provide_cache.pop(provide_id)
|
|
||||||
|
|
||||||
# Case: `{% provide %}` contained no components in its body.
|
|
||||||
# The provided data was not referenced by any components, but it's still in the cache.
|
|
||||||
elif provide_id not in provide_references and provide_id in provide_cache:
|
|
||||||
provide_cache.pop(provide_id)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# In case of an error in `Component.render()`, there may be some
|
# NOTE: In case of an error in within the `{% provide %}` block (e.g. when rendering a component),
|
||||||
# references left hanging, so we remove them.
|
# we rely on the component finalizer to remove the references.
|
||||||
new_reference_ids = all_reference_ids - all_reference_ids_before
|
# But we still want to call cleanup in case `{% provide %}` contained no components.
|
||||||
for reference_id in new_reference_ids:
|
_cache_cleanup(provide_id)
|
||||||
unregister_provide_reference(reference_id)
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
cache_cleanup()
|
|
||||||
# Forward the error
|
# Forward the error
|
||||||
raise e from None
|
raise e from None
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup on success
|
||||||
cache_cleanup()
|
_cache_cleanup(provide_id)
|
||||||
|
|
||||||
|
|
||||||
def register_provide_reference(context: Context, reference_id: str) -> None:
|
def _cache_cleanup(provide_id: str) -> None:
|
||||||
|
# Remove provided data from the cache, IF there are no more references to it.
|
||||||
|
# A `{% provide %}` will have no reference if:
|
||||||
|
# - It contains no components in its body
|
||||||
|
# - It contained components, but those components were already garbage collected
|
||||||
|
if provide_id in provide_references and not provide_references[provide_id]:
|
||||||
|
provide_references.pop(provide_id)
|
||||||
|
provide_cache.pop(provide_id, None)
|
||||||
|
|
||||||
|
# Case: `{% provide %}` contained no components in its body.
|
||||||
|
# The provided data was not referenced by any components, but it's still in the cache.
|
||||||
|
elif provide_id not in provide_references and provide_id in provide_cache:
|
||||||
|
provide_cache.pop(provide_id)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO - Once components can access their parents:
|
||||||
|
# Do NOT pass provide keys through components in isolated mode.
|
||||||
|
# Instead get parent's provide keys by getting the parent's id, `component.parent.id`
|
||||||
|
# and then accessing `component_provides[component.parent.id]`.
|
||||||
|
# The logic below would still remain, as that defines the `{% provide %}`
|
||||||
|
# instances defined INSIDE the parent component.
|
||||||
|
# And we would combine the two sources, and set that to `component_provides[component.id]`.
|
||||||
|
def register_provide_reference(context: Context, component: "Component") -> None:
|
||||||
# No `{% provide %}` among the ancestors, nothing to register to
|
# No `{% provide %}` among the ancestors, nothing to register to
|
||||||
if not provide_cache:
|
if not provide_cache:
|
||||||
return
|
return
|
||||||
|
|
||||||
all_reference_ids.add(reference_id)
|
# For all instances of `{% provide %}` that the current component is within,
|
||||||
|
# make note that this component has access to them.
|
||||||
for key, provide_id in context.flatten().items():
|
for key, value in context.flatten().items():
|
||||||
|
# NOTE: Provided data is stored on the Context object as e.g.
|
||||||
|
# `{"_DJC_INJECT__my_provide": "a1b3c3"}`
|
||||||
|
# Where "a1b3c3" is the ID of the provided data.
|
||||||
if not key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
|
if not key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if provide_id not in provide_references:
|
provide_id = cast("str", value)
|
||||||
provide_references[provide_id] = set()
|
provide_key = key.split(_INJECT_CONTEXT_KEY_PREFIX, 1)[1]
|
||||||
provide_references[provide_id].add(reference_id)
|
|
||||||
|
# Update the Provide -> Component[] mapping.
|
||||||
|
provide_references[provide_id].add(component.id)
|
||||||
|
|
||||||
|
# Update the Component -> Provide[] mapping.
|
||||||
|
component_provides[component.id][provide_key] = provide_id
|
||||||
|
|
||||||
|
|
||||||
def unregister_provide_reference(reference_id: str) -> None:
|
def unregister_provide_reference(component_id: str) -> None:
|
||||||
# No registered references, nothing to unregister
|
# List of `{% provide %}` IDs that the component had access to.
|
||||||
if reference_id not in all_reference_ids:
|
component_provides_ids = component_provides.get(component_id)
|
||||||
|
if not component_provides_ids:
|
||||||
return
|
return
|
||||||
|
|
||||||
all_reference_ids.remove(reference_id)
|
# Remove this component from all provide references it was subscribed to
|
||||||
|
for provide_id in component_provides_ids.values():
|
||||||
|
references_to_this_provide = provide_references.get(provide_id)
|
||||||
|
if references_to_this_provide:
|
||||||
|
references_to_this_provide.discard(component_id)
|
||||||
|
|
||||||
for provide_id in list(provide_references.keys()):
|
|
||||||
if reference_id not in provide_references[provide_id]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
provide_references[provide_id].remove(reference_id)
|
def unlink_component_from_provide_on_gc(component_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Finalizer function to be called when a Component object is garbage collected.
|
||||||
|
|
||||||
# There are no more references to the provided data, so we can delete it.
|
Unlinking the component at this point ensures that one can call `Component.inject()`
|
||||||
if not provide_references[provide_id]:
|
even after the component was rendered, as long as one keeps the reference to the component object.
|
||||||
provide_cache.pop(provide_id)
|
"""
|
||||||
provide_references.pop(provide_id)
|
unregister_provide_reference(component_id)
|
||||||
|
provide_ids = component_provides.pop(component_id, None)
|
||||||
|
if provide_ids:
|
||||||
|
for provide_id in provide_ids.values():
|
||||||
|
_cache_cleanup(provide_id)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.utils.safestring import SafeString
|
||||||
|
|
||||||
from django_components.context import _INJECT_CONTEXT_KEY_PREFIX
|
from django_components.context import _INJECT_CONTEXT_KEY_PREFIX
|
||||||
from django_components.node import BaseNode
|
from django_components.node import BaseNode
|
||||||
from django_components.perfutil.provide import managed_provide_cache, provide_cache
|
from django_components.perfutil.provide import component_provides, managed_provide_cache, provide_cache
|
||||||
from django_components.util.misc import gen_id
|
from django_components.util.misc import gen_id
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,8 +102,8 @@ class ProvideNode(BaseNode):
|
||||||
|
|
||||||
|
|
||||||
def get_injected_context_var(
|
def get_injected_context_var(
|
||||||
|
component_id: str,
|
||||||
component_name: str,
|
component_name: str,
|
||||||
context: Context,
|
|
||||||
key: str,
|
key: str,
|
||||||
default: Optional[Any] = None,
|
default: Optional[Any] = None,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
|
@ -111,15 +111,13 @@ def get_injected_context_var(
|
||||||
Retrieve a 'provided' field. The field MUST have been previously 'provided'
|
Retrieve a 'provided' field. The field MUST have been previously 'provided'
|
||||||
by the component's ancestors using the `{% provide %}` template tag.
|
by the component's ancestors using the `{% provide %}` template tag.
|
||||||
"""
|
"""
|
||||||
# NOTE: For simplicity, we keep the provided values directly on the context.
|
# NOTE: `component_provides` is defaultdict. Use `.get()` to avoid making an empty dictionary.
|
||||||
# This plays nicely with Django's Context, which behaves like a stack, so "newer"
|
providers = component_provides.get(component_id)
|
||||||
# values overshadow the "older" ones.
|
|
||||||
internal_key = _INJECT_CONTEXT_KEY_PREFIX + key
|
|
||||||
|
|
||||||
# Return provided value if found
|
# Return provided value if found
|
||||||
if internal_key in context:
|
if providers and key in providers:
|
||||||
cache_key = context[internal_key]
|
provide_id = providers[key]
|
||||||
return provide_cache[cache_key]
|
return provide_cache[provide_id]
|
||||||
|
|
||||||
# If a default was given, return that
|
# If a default was given, return that
|
||||||
if default is not None:
|
if default is not None:
|
||||||
|
@ -133,6 +131,8 @@ def get_injected_context_var(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO_v2 - Once we wrap all executions of Django's Template as our Components,
|
||||||
|
# we'll be able to store the provided data on ComponentContext instead of on Context.
|
||||||
def set_provided_context_var(
|
def set_provided_context_var(
|
||||||
context: Context,
|
context: Context,
|
||||||
key: str,
|
key: str,
|
||||||
|
@ -161,8 +161,12 @@ def set_provided_context_var(
|
||||||
tuple_cls = NamedTuple("DepInject", fields) # type: ignore[misc]
|
tuple_cls = NamedTuple("DepInject", fields) # type: ignore[misc]
|
||||||
payload = tuple_cls(**provided_kwargs)
|
payload = tuple_cls(**provided_kwargs)
|
||||||
|
|
||||||
# Instead of storing the provided data on the Context object, we store it
|
# To allow the components nested inside `{% provide %}` to access the provided data,
|
||||||
# in a separate dictionary, and we set only the key to the data on the Context.
|
# we pass the data through the Context.
|
||||||
|
# But instead of storing the data directly on the Context object, we store it
|
||||||
|
# in a separate dictionary, and we only set a key to the data on the Context.
|
||||||
|
# This helps with debugging as the Context is easier to inspect. It also helps
|
||||||
|
# with testing and garbage collection, as we can easily access/modify the provided data.
|
||||||
context_key = _INJECT_CONTEXT_KEY_PREFIX + key
|
context_key = _INJECT_CONTEXT_KEY_PREFIX + key
|
||||||
provide_id = gen_id()
|
provide_id = gen_id()
|
||||||
context[context_key] = provide_id
|
context[context_key] = provide_id
|
||||||
|
|
|
@ -680,7 +680,11 @@ class SlotNode(BaseNode):
|
||||||
# Component info
|
# Component info
|
||||||
component_id: str = context[_COMPONENT_CONTEXT_KEY]
|
component_id: str = context[_COMPONENT_CONTEXT_KEY]
|
||||||
component_ctx = component_context_cache[component_id]
|
component_ctx = component_context_cache[component_id]
|
||||||
component = component_ctx.component
|
component = component_ctx.component()
|
||||||
|
if component is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Component with id '{component_id}' was garbage collected before its slots could be rendered."
|
||||||
|
)
|
||||||
component_name = component.name
|
component_name = component.name
|
||||||
component_path = component_ctx.component_path
|
component_path = component_ctx.component_path
|
||||||
is_dynamic_component = getattr(component, "_is_dynamic_component", False)
|
is_dynamic_component = getattr(component, "_is_dynamic_component", False)
|
||||||
|
@ -828,7 +832,12 @@ class SlotNode(BaseNode):
|
||||||
if parent_index is not None:
|
if parent_index is not None:
|
||||||
ctx_id_with_fills = context.dicts[parent_index][_COMPONENT_CONTEXT_KEY]
|
ctx_id_with_fills = context.dicts[parent_index][_COMPONENT_CONTEXT_KEY]
|
||||||
ctx_with_fills = component_context_cache[ctx_id_with_fills]
|
ctx_with_fills = component_context_cache[ctx_id_with_fills]
|
||||||
slot_fills = ctx_with_fills.component.raw_slots
|
parent_component = ctx_with_fills.component()
|
||||||
|
if parent_component is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Component with id '{component_id}' was garbage collected before its slots could be rendered."
|
||||||
|
)
|
||||||
|
slot_fills = parent_component.raw_slots
|
||||||
|
|
||||||
# Add trace message when slot_fills are overwritten
|
# Add trace message when slot_fills are overwritten
|
||||||
trace_component_msg(
|
trace_component_msg(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
<div>
|
<div>
|
||||||
{% component "injectee" %}
|
{% component "injectee17" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -736,8 +736,18 @@ class TestSlot:
|
||||||
assert len(seen_slots) == 3
|
assert len(seen_slots) == 3
|
||||||
|
|
||||||
results = [slot().strip() for slot in seen_slots]
|
results = [slot().strip() for slot in seen_slots]
|
||||||
assert results == [
|
|
||||||
"<!-- _RENDERED MyInnerComponent_fb676b,ca1bc49,, -->Hello!",
|
if components_settings["context_behavior"] == "django":
|
||||||
"<!-- _RENDERED MyInnerComponent_fb676b,ca1bc4a,, -->Hello!",
|
assert results == [
|
||||||
"<!-- _RENDERED MyInnerComponent_fb676b,ca1bc4b,, -->Hello!",
|
"<!-- _RENDERED MyInnerComponent_fb676b,ca1bc49,, -->Hello!",
|
||||||
]
|
"<!-- _RENDERED MyInnerComponent_fb676b,ca1bc4a,, -->Hello!",
|
||||||
|
"<!-- _RENDERED MyInnerComponent_fb676b,ca1bc4b,, -->Hello!",
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
# TODO - Incorrect for slots!
|
||||||
|
# To be fixed in https://github.com/django-components/django-components/issues/1259
|
||||||
|
assert results == [
|
||||||
|
'<template djc-render-id="ca1bc49"></template>',
|
||||||
|
'<template djc-render-id="ca1bc4a"></template>',
|
||||||
|
'<template djc-render-id="ca1bc4b"></template>',
|
||||||
|
]
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
|
import gc
|
||||||
import re
|
import re
|
||||||
|
from weakref import ref
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
from pytest_django.asserts import assertHTMLEqual
|
from pytest_django.asserts import assertHTMLEqual
|
||||||
|
|
||||||
from django_components import Component, register, types
|
from django_components import Component, register, types
|
||||||
from django_components.perfutil.provide import all_reference_ids, provide_cache, provide_references
|
from django_components.component import ComponentContext
|
||||||
|
from django_components.perfutil.component import component_context_cache, component_instance_cache
|
||||||
|
from django_components.perfutil.provide import component_provides, provide_cache, provide_references
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
|
|
||||||
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
|
@ -13,16 +17,24 @@ from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||||
setup_test_config({"autodiscover": False})
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: By running garbage collection and then checking for empty caches,
|
||||||
|
# we ensure that we are not introducing any memory leaks.
|
||||||
|
def _assert_clear_cache():
|
||||||
|
# Ensure that finalizers have run
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
assert provide_cache == {}
|
||||||
|
assert provide_references == {}
|
||||||
|
assert component_provides == {}
|
||||||
|
assert component_instance_cache == {}
|
||||||
|
assert component_context_cache == {}
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestProvideTemplateTag:
|
class TestProvideTemplateTag:
|
||||||
def _assert_clear_cache(self):
|
|
||||||
assert provide_cache == {}
|
|
||||||
assert provide_references == {}
|
|
||||||
assert all_reference_ids == set()
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_basic(self, components_settings):
|
def test_provide_basic(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee1")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -35,7 +47,7 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=1 %}
|
{% provide "my_provide" key="hi" another=1 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee1" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
@ -48,7 +60,7 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc41> injected: DepInject(key='hi', another=1) </div>
|
<div data-djc-id-ca1bc41> injected: DepInject(key='hi', another=1) </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_basic_self_closing(self, components_settings):
|
def test_provide_basic_self_closing(self, components_settings):
|
||||||
|
@ -67,11 +79,11 @@ class TestProvideTemplateTag:
|
||||||
<div></div>
|
<div></div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_access_keys_in_python(self, components_settings):
|
def test_provide_access_keys_in_python(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee2")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> key: {{ key }} </div>
|
<div> key: {{ key }} </div>
|
||||||
|
@ -88,7 +100,7 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=3 %}
|
{% provide "my_provide" key="hi" another=3 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee2" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
@ -102,11 +114,11 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc41> another: 3 </div>
|
<div data-djc-id-ca1bc41> another: 3 </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_access_keys_in_django(self, components_settings):
|
def test_provide_access_keys_in_django(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee3")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> key: {{ my_provide.key }} </div>
|
<div> key: {{ my_provide.key }} </div>
|
||||||
|
@ -122,7 +134,7 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=4 %}
|
{% provide "my_provide" key="hi" another=4 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee3" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
@ -136,11 +148,11 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc41> another: 4 </div>
|
<div data-djc-id-ca1bc41> another: 4 </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_does_not_leak(self, components_settings):
|
def test_provide_does_not_leak(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee4")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -154,7 +166,7 @@ class TestProvideTemplateTag:
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=5 %}
|
{% provide "my_provide" key="hi" another=5 %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee4" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -166,13 +178,13 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc41> injected: default </div>
|
<div data-djc-id-ca1bc41> injected: default </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_empty(self, components_settings):
|
def test_provide_empty(self, components_settings):
|
||||||
"""Check provide tag with no kwargs"""
|
"""Check provide tag with no kwargs"""
|
||||||
|
|
||||||
@register("injectee")
|
@register("injectee5")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -185,10 +197,10 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" %}
|
{% provide "my_provide" %}
|
||||||
{% component "injectee" %}
|
{% component "injectee5" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee5" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -201,13 +213,13 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc43> injected: default </div>
|
<div data-djc-id-ca1bc43> injected: default </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(components_settings={"context_behavior": "django"})
|
@djc_test(components_settings={"context_behavior": "django"})
|
||||||
def test_provide_no_inject(self):
|
def test_provide_no_inject(self):
|
||||||
"""Check that nothing breaks if we do NOT inject even if some data is provided"""
|
"""Check that nothing breaks if we do NOT inject even if some data is provided"""
|
||||||
|
|
||||||
@register("injectee")
|
@register("injectee6")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div></div>
|
<div></div>
|
||||||
|
@ -216,10 +228,10 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=6 %}
|
{% provide "my_provide" key="hi" another=6 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee6" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee6" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -232,11 +244,11 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc43></div>
|
<div data-djc-id-ca1bc43></div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_name_single_quotes(self, components_settings):
|
def test_provide_name_single_quotes(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee7")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -249,10 +261,10 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide 'my_provide' key="hi" another=7 %}
|
{% provide 'my_provide' key="hi" another=7 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee7" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee7" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -265,11 +277,11 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc43> injected: default </div>
|
<div data-djc-id-ca1bc43> injected: default </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_name_as_var(self, components_settings):
|
def test_provide_name_as_var(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee8")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -282,10 +294,10 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide var_a key="hi" another=8 %}
|
{% provide var_a key="hi" another=8 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee8" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee8" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -304,11 +316,11 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc43> injected: default </div>
|
<div data-djc-id-ca1bc43> injected: default </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_name_as_spread(self, components_settings):
|
def test_provide_name_as_spread(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee9")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -321,10 +333,10 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide ...provide_props %}
|
{% provide ...provide_props %}
|
||||||
{% component "injectee" %}
|
{% component "injectee9" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee9" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -347,11 +359,11 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc43> injected: default </div>
|
<div data-djc-id-ca1bc43> injected: default </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_no_name_raises(self, components_settings):
|
def test_provide_no_name_raises(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee10")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -364,10 +376,10 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide key="hi" another=10 %}
|
{% provide key="hi" another=10 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee10" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee10" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
|
@ -376,11 +388,11 @@ class TestProvideTemplateTag:
|
||||||
):
|
):
|
||||||
Template(template_str).render(Context({}))
|
Template(template_str).render(Context({}))
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_name_must_be_string_literal(self, components_settings):
|
def test_provide_name_must_be_string_literal(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee11")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -393,10 +405,10 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide my_var key="hi" another=11 %}
|
{% provide my_var key="hi" another=11 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee11" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee11" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
|
@ -405,11 +417,11 @@ class TestProvideTemplateTag:
|
||||||
):
|
):
|
||||||
Template(template_str).render(Context({}))
|
Template(template_str).render(Context({}))
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_name_must_be_identifier(self, components_settings):
|
def test_provide_name_must_be_identifier(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee12")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -422,21 +434,21 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "%heya%" key="hi" another=12 %}
|
{% provide "%heya%" key="hi" another=12 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee12" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee12" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
|
||||||
with pytest.raises(TemplateSyntaxError):
|
with pytest.raises(TemplateSyntaxError):
|
||||||
template.render(Context({}))
|
template.render(Context({}))
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_aggregate_dics(self, components_settings):
|
def test_provide_aggregate_dics(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee13")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -449,7 +461,7 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" var1:key="hi" var1:another=13 var2:x="y" %}
|
{% provide "my_provide" var1:key="hi" var1:another=13 var2:x="y" %}
|
||||||
{% component "injectee" %}
|
{% component "injectee13" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
@ -462,13 +474,13 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc41> injected: DepInject(var1={'key': 'hi', 'another': 13}, var2={'x': 'y'}) </div>
|
<div data-djc-id-ca1bc41> injected: DepInject(var1={'key': 'hi', 'another': 13}, var2={'x': 'y'}) </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_does_not_expose_kwargs_to_context(self, components_settings):
|
def test_provide_does_not_expose_kwargs_to_context(self, components_settings):
|
||||||
"""Check that `provide` tag doesn't assign the keys to the context like `with` tag does"""
|
"""Check that `provide` tag doesn't assign the keys to the context like `with` tag does"""
|
||||||
|
|
||||||
@register("injectee")
|
@register("injectee14")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -499,13 +511,13 @@ class TestProvideTemplateTag:
|
||||||
key_in:
|
key_in:
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_nested_in_provide_same_key(self, components_settings):
|
def test_provide_nested_in_provide_same_key(self, components_settings):
|
||||||
"""Check that inner `provide` with same key overshadows outer `provide`"""
|
"""Check that inner `provide` with same key overshadows outer `provide`"""
|
||||||
|
|
||||||
@register("injectee")
|
@register("injectee15")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -519,14 +531,14 @@ class TestProvideTemplateTag:
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=15 lost=0 %}
|
{% provide "my_provide" key="hi" another=15 lost=0 %}
|
||||||
{% provide "my_provide" key="hi1" another=16 new=3 %}
|
{% provide "my_provide" key="hi1" another=16 new=3 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee15" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
|
|
||||||
{% component "injectee" %}
|
{% component "injectee15" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee15" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -541,13 +553,13 @@ class TestProvideTemplateTag:
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_nested_in_provide_different_key(self, components_settings):
|
def test_provide_nested_in_provide_different_key(self, components_settings):
|
||||||
"""Check that `provide` tag with different keys don't affect each other"""
|
"""Check that `provide` tag with different keys don't affect each other"""
|
||||||
|
|
||||||
@register("injectee")
|
@register("injectee16")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> first_provide: {{ first_provide|safe }} </div>
|
<div> first_provide: {{ first_provide|safe }} </div>
|
||||||
|
@ -566,7 +578,7 @@ class TestProvideTemplateTag:
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "first_provide" key="hi" another=17 lost=0 %}
|
{% provide "first_provide" key="hi" another=17 lost=0 %}
|
||||||
{% provide "second_provide" key="hi1" another=18 new=3 %}
|
{% provide "second_provide" key="hi1" another=18 new=3 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee16" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
|
@ -581,11 +593,11 @@ class TestProvideTemplateTag:
|
||||||
<div data-djc-id-ca1bc43> second_provide: DepInject(key='hi1', another=18, new=3) </div>
|
<div data-djc-id-ca1bc43> second_provide: DepInject(key='hi1', another=18, new=3) </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_provide_in_include(self, components_settings):
|
def test_provide_in_include(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee17")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -612,11 +624,11 @@ class TestProvideTemplateTag:
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_slot_in_provide(self, components_settings):
|
def test_slot_in_provide(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee18")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -638,7 +650,7 @@ class TestProvideTemplateTag:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "parent" %}
|
{% component "parent" %}
|
||||||
{% component "injectee" %}{% endcomponent %}
|
{% component "injectee18" %}{% endcomponent %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -652,19 +664,143 @@ class TestProvideTemplateTag:
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
|
# TODO - Enable once globals and finalizers are scoped to a single DJC instance")
|
||||||
|
# See https://github.com/django-components/django-components/issues/1413
|
||||||
|
@pytest.mark.skip("#TODO")
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_provide_component_inside_forloop(self, components_settings):
|
||||||
|
@register("loop_component")
|
||||||
|
class LoopComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div>Item {{ item_num }}: {{ provided_value }}</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
provided_data = self.inject("loop_provide")
|
||||||
|
return {
|
||||||
|
"item_num": kwargs["item_num"],
|
||||||
|
"provided_value": provided_data.shared_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "loop_provide" shared_value="shared_data" %}
|
||||||
|
{% for i in items %}
|
||||||
|
{% component "loop_component" item_num=i / %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(template_str)
|
||||||
|
context = Context({"items": [1, 2, 3, 4, 5]})
|
||||||
|
rendered = template.render(context)
|
||||||
|
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div data-djc-id-ca1bc41>Item 1: shared_data</div>
|
||||||
|
<div data-djc-id-ca1bc42>Item 2: shared_data</div>
|
||||||
|
<div data-djc-id-ca1bc43>Item 3: shared_data</div>
|
||||||
|
<div data-djc-id-ca1bc44>Item 4: shared_data</div>
|
||||||
|
<div data-djc-id-ca1bc45>Item 5: shared_data</div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure that finalizers have run
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
# Ensure all caches are properly cleaned up even with multiple component instances
|
||||||
|
_assert_clear_cache()
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_provide_component_inside_nested_forloop(self, components_settings):
|
||||||
|
@register("nested_loop_component")
|
||||||
|
class NestedLoopComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<span>{{ outer }}-{{ inner }}: {{ provided_value }}</span>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
provided_data = self.inject("nested_provide")
|
||||||
|
return {
|
||||||
|
"outer": kwargs["outer"],
|
||||||
|
"inner": kwargs["inner"],
|
||||||
|
"provided_value": provided_data.nested_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "nested_provide" nested_value="nested_data" %}
|
||||||
|
{% for outer in outer_items %}
|
||||||
|
{% for inner in inner_items %}
|
||||||
|
{% component "nested_loop_component" outer=outer inner=inner / %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(template_str)
|
||||||
|
context = Context({"outer_items": ["A", "B"], "inner_items": [1, 2]})
|
||||||
|
rendered = template.render(context)
|
||||||
|
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<span data-djc-id-ca1bc41>A-1: nested_data</span>
|
||||||
|
<span data-djc-id-ca1bc42>A-2: nested_data</span>
|
||||||
|
<span data-djc-id-ca1bc43>B-1: nested_data</span>
|
||||||
|
<span data-djc-id-ca1bc44>B-2: nested_data</span>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure all caches are properly cleaned up even with many component instances
|
||||||
|
_assert_clear_cache()
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_provide_component_forloop_with_error(self, components_settings):
|
||||||
|
@register("error_loop_component")
|
||||||
|
class ErrorLoopComponent(Component):
|
||||||
|
template = ""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
provided_data = self.inject("error_provide")
|
||||||
|
item_num = kwargs["item_num"]
|
||||||
|
|
||||||
|
# Throw error on the third item
|
||||||
|
if item_num == 3:
|
||||||
|
raise ValueError(f"Error on item {item_num}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"item_num": item_num,
|
||||||
|
"provided_value": provided_data.error_value,
|
||||||
|
}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "error_provide" error_value="error_data" %}
|
||||||
|
{% for i in items %}
|
||||||
|
{% component "error_loop_component" item_num=i / %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
template = Template(template_str)
|
||||||
|
context = Context({"items": [1, 2, 3, 4, 5]})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=re.escape("Error on item 3")):
|
||||||
|
template.render(context)
|
||||||
|
|
||||||
|
# Ensure all caches are properly cleaned up even when errors occur
|
||||||
|
_assert_clear_cache()
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestInject:
|
class TestInject:
|
||||||
def _assert_clear_cache(self):
|
|
||||||
assert provide_cache == {}
|
|
||||||
assert provide_references == {}
|
|
||||||
assert all_reference_ids == set()
|
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_inject_basic(self, components_settings):
|
def test_inject_basic(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee19")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -677,7 +813,7 @@ class TestInject:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=21 %}
|
{% provide "my_provide" key="hi" another=21 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee19" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
@ -690,11 +826,11 @@ class TestInject:
|
||||||
<div data-djc-id-ca1bc41> injected: DepInject(key='hi', another=21) </div>
|
<div data-djc-id-ca1bc41> injected: DepInject(key='hi', another=21) </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_inject_missing_key_raises_without_default(self, components_settings):
|
def test_inject_missing_key_raises_without_default(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee20")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -706,7 +842,7 @@ class TestInject:
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "injectee" %}
|
{% component "injectee20" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -714,11 +850,11 @@ class TestInject:
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
template.render(Context({}))
|
template.render(Context({}))
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_inject_missing_key_ok_with_default(self, components_settings):
|
def test_inject_missing_key_ok_with_default(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee21")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -730,7 +866,7 @@ class TestInject:
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "injectee" %}
|
{% component "injectee21" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -741,11 +877,11 @@ class TestInject:
|
||||||
<div data-djc-id-ca1bc3f> injected: default </div>
|
<div data-djc-id-ca1bc3f> injected: default </div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_inject_empty_string(self, components_settings):
|
def test_inject_empty_string(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee22")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
@ -758,10 +894,10 @@ class TestInject:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=22 %}
|
{% provide "my_provide" key="hi" another=22 %}
|
||||||
{% component "injectee" %}
|
{% component "injectee22" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
{% component "injectee" %}
|
{% component "injectee22" %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
|
@ -769,29 +905,114 @@ class TestInject:
|
||||||
with pytest.raises(KeyError):
|
with pytest.raises(KeyError):
|
||||||
template.render(Context({}))
|
template.render(Context({}))
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
# TODO - Enable once globals and finalizers are scoped to a single DJC instance")
|
||||||
def test_inject_called_outside_rendering(self, components_settings):
|
# See https://github.com/django-components/django-components/issues/1413
|
||||||
@register("injectee")
|
# @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
@djc_test(
|
||||||
|
parametrize=(
|
||||||
|
["components_settings"],
|
||||||
|
[
|
||||||
|
[{"context_behavior": "isolated"}],
|
||||||
|
],
|
||||||
|
["isolated"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
def test_inject_called_outside_rendering__persisted_ref(self, components_settings):
|
||||||
|
comp = None
|
||||||
|
|
||||||
|
@register("injectee23")
|
||||||
class InjectComponent(Component):
|
class InjectComponent(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
<div> injected: {{ var|safe }} </div>
|
<div> injected: {{ var|safe }} </div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_template_data(self, args, kwargs, slots, context):
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
var = self.inject("abc", "default")
|
nonlocal comp
|
||||||
|
comp = self
|
||||||
|
|
||||||
|
var = self.inject(key="my_provide")
|
||||||
return {"var": var}
|
return {"var": var}
|
||||||
|
|
||||||
comp = InjectComponent()
|
template_str: types.django_html = """
|
||||||
comp.inject("abc", "def")
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" value=23 %}
|
||||||
|
{% component "injectee23" / %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
self._assert_clear_cache()
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div data-djc-id-ca1bc41> injected: DepInject(key='hi', value=23) </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert comp is not None
|
||||||
|
|
||||||
|
# Check that we can inject the data even after the component was rendered.
|
||||||
|
injected = comp.inject(key="my_provide", default="def")
|
||||||
|
assert isinstance(injected, tuple)
|
||||||
|
assert injected.key == "hi" # type: ignore[attr-defined]
|
||||||
|
assert injected.value == 23 # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
# NOTE: Because we kept the reference to the component, it's not garbage collected yet.
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
assert provide_cache == {"a1bc40": ("hi", 23)}
|
||||||
|
assert provide_references == {"a1bc40": {"ca1bc41"}}
|
||||||
|
assert component_provides == {"ca1bc41": {"my_provide": "a1bc40"}}
|
||||||
|
assert component_instance_cache == {}
|
||||||
|
assert len(component_context_cache) == 1
|
||||||
|
assert isinstance(component_context_cache["ca1bc41"], ComponentContext)
|
||||||
|
|
||||||
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
|
def test_inject_called_outside_rendering__not_persisted(self, components_settings):
|
||||||
|
comp = None
|
||||||
|
|
||||||
|
@register("injectee24")
|
||||||
|
class InjectComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
<div> injected: {{ var|safe }} </div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal comp
|
||||||
|
comp = ref(self)
|
||||||
|
|
||||||
|
var = self.inject(key="my_provide")
|
||||||
|
return {"var": var}
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% provide "my_provide" key="hi" value=23 %}
|
||||||
|
{% component "injectee24" / %}
|
||||||
|
{% endprovide %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
assertHTMLEqual(
|
||||||
|
rendered,
|
||||||
|
"""
|
||||||
|
<div data-djc-id-ca1bc41> injected: DepInject(key='hi', value=23) </div>
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
|
# We didn't keep the reference, so the caches should be cleared.
|
||||||
|
assert comp is not None
|
||||||
|
assert comp() is None
|
||||||
|
_assert_clear_cache()
|
||||||
|
|
||||||
# See https://github.com/django-components/django-components/pull/778
|
# See https://github.com/django-components/django-components/pull/778
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_inject_in_fill(self, components_settings):
|
def test_inject_in_fill(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee25")
|
||||||
class Injectee(Component):
|
class Injectee(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -825,7 +1046,7 @@ class TestInject:
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "provider" data=data %}
|
{% component "provider" data=data %}
|
||||||
{% component "injectee" %}
|
{% component "injectee25" %}
|
||||||
{% slot "content" default / %}
|
{% slot "content" default / %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
|
@ -855,12 +1076,12 @@ class TestInject:
|
||||||
</main>
|
</main>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
# See https://github.com/django-components/django-components/pull/786
|
# See https://github.com/django-components/django-components/pull/786
|
||||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||||
def test_inject_in_slot_in_fill(self, components_settings):
|
def test_inject_in_slot_in_fill(self, components_settings):
|
||||||
@register("injectee")
|
@register("injectee26")
|
||||||
class Injectee(Component):
|
class Injectee(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -903,7 +1124,7 @@ class TestInject:
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% component "parent" data=123 %}
|
{% component "parent" data=123 %}
|
||||||
{% component "injectee" / %}
|
{% component "injectee26" / %}
|
||||||
{% endcomponent %}
|
{% endcomponent %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -919,7 +1140,7 @@ class TestInject:
|
||||||
</main>
|
</main>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
|
|
||||||
# When there is `{% component %}` that's a descendant of `{% provide %}`,
|
# When there is `{% component %}` that's a descendant of `{% provide %}`,
|
||||||
|
@ -930,13 +1151,8 @@ class TestInject:
|
||||||
# when the component rendered is done.
|
# when the component rendered is done.
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestProvideCache:
|
class TestProvideCache:
|
||||||
def _assert_clear_cache(self):
|
|
||||||
assert provide_cache == {}
|
|
||||||
assert provide_references == {}
|
|
||||||
assert all_reference_ids == set()
|
|
||||||
|
|
||||||
def test_provide_outside_component(self):
|
def test_provide_outside_component(self):
|
||||||
@register("injectee")
|
@register("injectee27")
|
||||||
class Injectee(Component):
|
class Injectee(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -953,14 +1169,14 @@ class TestProvideCache:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=23 %}
|
{% provide "my_provide" key="hi" another=23 %}
|
||||||
{% component "injectee" / %}
|
{% component "injectee27" / %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
rendered = template.render(Context({}))
|
rendered = template.render(Context({}))
|
||||||
|
|
||||||
|
@ -975,11 +1191,11 @@ class TestProvideCache:
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
# Cache should be cleared even if there is an error.
|
# Cache should be cleared even if there is an error.
|
||||||
def test_provide_outside_component_with_error(self):
|
def test_provide_outside_component_with_error(self):
|
||||||
@register("injectee")
|
@register("injectee28")
|
||||||
class Injectee(Component):
|
class Injectee(Component):
|
||||||
template = ""
|
template = ""
|
||||||
|
|
||||||
|
@ -993,22 +1209,22 @@ class TestProvideCache:
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=24 %}
|
{% provide "my_provide" key="hi" another=24 %}
|
||||||
{% component "injectee" / %}
|
{% component "injectee28" / %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
template = Template(template_str)
|
template = Template(template_str)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=re.escape("Oops")):
|
with pytest.raises(ValueError, match=re.escape("Oops")):
|
||||||
template.render(Context({}))
|
template.render(Context({}))
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
def test_provide_inside_component(self):
|
def test_provide_inside_component(self):
|
||||||
@register("injectee")
|
@register("injectee29")
|
||||||
class Injectee(Component):
|
class Injectee(Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
|
@ -1027,11 +1243,11 @@ class TestProvideCache:
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=25 %}
|
{% provide "my_provide" key="hi" another=25 %}
|
||||||
{% component "injectee" / %}
|
{% component "injectee29" / %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
rendered = Root.render()
|
rendered = Root.render()
|
||||||
|
|
||||||
|
@ -1046,10 +1262,10 @@ class TestProvideCache:
|
||||||
</div>
|
</div>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
def test_provide_inside_component_with_error(self):
|
def test_provide_inside_component_with_error(self):
|
||||||
@register("injectee")
|
@register("injectee30")
|
||||||
class Injectee(Component):
|
class Injectee(Component):
|
||||||
template = ""
|
template = ""
|
||||||
|
|
||||||
|
@ -1065,13 +1281,13 @@ class TestProvideCache:
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% provide "my_provide" key="hi" another=26 %}
|
{% provide "my_provide" key="hi" another=26 %}
|
||||||
{% component "injectee" / %}
|
{% component "injectee30" / %}
|
||||||
{% endprovide %}
|
{% endprovide %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=re.escape("Oops")):
|
with pytest.raises(ValueError, match=re.escape("Oops")):
|
||||||
Root.render()
|
Root.render()
|
||||||
|
|
||||||
self._assert_clear_cache()
|
_assert_clear_cache()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue