mirror of
https://github.com/django-components/django-components.git
synced 2025-11-03 16:42:52 +00:00
refactor: use lamba with yield in Component.on_render() (#1428)
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run
This commit is contained in:
parent
3e837e20c6
commit
eee3910b54
6 changed files with 277 additions and 116 deletions
37
CHANGELOG.md
37
CHANGELOG.md
|
|
@ -4,23 +4,48 @@
|
|||
|
||||
#### Feat
|
||||
|
||||
- Wrap the template rendering in `Component.on_render()` in a lambda function.
|
||||
|
||||
When you wrap the rendering call in a lambda function, and the rendering fails,
|
||||
the error will be yielded back in the `(None, Exception)` tuple.
|
||||
|
||||
Before:
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
try:
|
||||
intermediate = template.render(context)
|
||||
html, error = yield intermediate
|
||||
except Exception as e:
|
||||
html, error = None, e
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield lambda: template.render(context)
|
||||
```
|
||||
|
||||
- Multiple yields in `Component.on_render()` - You can now yield multiple times within the same `on_render` method for complex rendering scenarios.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
# First yield - render with one context
|
||||
# First yield
|
||||
with context.push({"mode": "header"}):
|
||||
header_html, header_error = yield template.render(context)
|
||||
header_html, header_error = yield lambda: template.render(context)
|
||||
|
||||
# Second yield - render with different context
|
||||
# Second yield
|
||||
with context.push({"mode": "body"}):
|
||||
body_html, body_error = yield template.render(context)
|
||||
body_html, body_error = yield lambda: template.render(context)
|
||||
|
||||
# Third yield - render a string directly
|
||||
# Third yield
|
||||
footer_html, footer_error = yield "Footer content"
|
||||
|
||||
# Process all results and return final output
|
||||
# Process all results
|
||||
if header_error or body_error or footer_error:
|
||||
return "Error occurred during rendering"
|
||||
|
||||
|
|
|
|||
|
|
@ -54,9 +54,6 @@ class MyTable(Component):
|
|||
|
||||
Do NOT modify the template in this hook. The template is reused across renders.
|
||||
|
||||
Since this hook is called for every component, this means that the template would be modified
|
||||
every time a component is rendered.
|
||||
|
||||
### `on_render`
|
||||
|
||||
_New in version 0.140_
|
||||
|
|
@ -86,9 +83,7 @@ with the given
|
|||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
if template is None:
|
||||
return None
|
||||
else:
|
||||
if template:
|
||||
return template.render(context)
|
||||
```
|
||||
|
||||
|
|
@ -98,14 +93,26 @@ The `template` argument is `None` if the component has no template.
|
|||
|
||||
To change what gets rendered, you can:
|
||||
|
||||
- Render a different template
|
||||
- Render a component
|
||||
- Return a different string or SafeString
|
||||
- Render a template
|
||||
- Return a string or SafeString
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
return "Hello"
|
||||
# Return a string
|
||||
return "<p>Hello</p>"
|
||||
|
||||
# Render a component
|
||||
return MyOtherTable.render(
|
||||
args=self.args,
|
||||
kwargs=self.kwargs,
|
||||
slots=self.slots,
|
||||
context=context,
|
||||
)
|
||||
|
||||
# Render a template
|
||||
return get_template("my_other_table.html").render(context)
|
||||
```
|
||||
|
||||
You can also use [`on_render()`](../../../reference/api#django_components.Component.on_render) as a router,
|
||||
|
|
@ -145,14 +152,19 @@ are not rendered yet.
|
|||
Instead, django-components needs to take this result and process it
|
||||
to actually render the child components.
|
||||
|
||||
To access the final output, you can `yield` the result instead of returning it.
|
||||
This is not a problem when you return the result directly as above. Django-components will take care of rendering the child components.
|
||||
|
||||
This will return a tuple of (rendered HTML, error). The error is `None` if the rendering succeeded.
|
||||
But if you want to access the final output, you must `yield` the result instead of returning it.
|
||||
|
||||
Yielding the result will return a tuple of `(rendered_html, error)`:
|
||||
|
||||
- On success, the error is `None` - `(string, None)`
|
||||
- On failure, the rendered HTML is `None` - `(None, Exception)`
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
html, error = yield lambda: template.render(context)
|
||||
|
||||
if error is None:
|
||||
# The rendering succeeded
|
||||
|
|
@ -162,23 +174,34 @@ class MyTable(Component):
|
|||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
Notice that we actually yield a **lambda function** instead of the result itself.
|
||||
This is because calling `template.render(context)` may raise an exception.
|
||||
|
||||
When you wrap the result in a lambda function, and the rendering fails,
|
||||
the error will be yielded back in the `(None, Exception)` tuple.
|
||||
|
||||
At this point you can do 3 things:
|
||||
|
||||
1. Return a new HTML
|
||||
1. Return new HTML
|
||||
|
||||
The new HTML will be used as the final output.
|
||||
|
||||
If the original template raised an error, it will be ignored.
|
||||
If the original template raised an error, the original error will be ignored.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
html, error = yield lambda: template.render(context)
|
||||
|
||||
return "NEW HTML"
|
||||
# Fallback if rendering failed
|
||||
# Otherwise, we keep the original HTML
|
||||
if error is not None:
|
||||
return "FALLBACK HTML"
|
||||
```
|
||||
|
||||
2. Raise a new exception
|
||||
2. Raise new exception
|
||||
|
||||
The new exception is what will bubble up from the component.
|
||||
|
||||
|
|
@ -187,48 +210,55 @@ At this point you can do 3 things:
|
|||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
html, error = yield lambda: template.render(context)
|
||||
|
||||
raise Exception("Error message")
|
||||
# Override the original error
|
||||
# Otherwise, we keep the original HTML
|
||||
if error is not None:
|
||||
raise Exception("My new error") from error
|
||||
```
|
||||
|
||||
3. Return nothing (or `None`) to handle the result as usual
|
||||
3. No change - Return nothing or `None`
|
||||
|
||||
If you don't raise an exception, and neither return a new HTML,
|
||||
then original HTML / error will be used:
|
||||
If you neither raise an exception, nor return a new HTML,
|
||||
then the original HTML / error will be used:
|
||||
|
||||
- If rendering succeeded, the original HTML will be used as the final output.
|
||||
- If rendering failed, the original error will be propagated.
|
||||
|
||||
This can be useful for side effects like tracking the errors that occurred during the rendering:
|
||||
|
||||
```py
|
||||
from myapp.metrics import track_rendering_error
|
||||
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
html, error = yield lambda: template.render(context)
|
||||
|
||||
# Track how many times the rendering failed
|
||||
if error is not None:
|
||||
# The rendering failed
|
||||
print(f"Error: {error}")
|
||||
track_rendering_error(error)
|
||||
```
|
||||
|
||||
#### Multiple yields
|
||||
|
||||
You can yield multiple times within the same `on_render` method. This is useful for complex rendering scenarios where you need to render different templates or handle multiple rendering operations:
|
||||
You can yield multiple times within the same [`on_render()`](../../../reference/api#django_components.Component.on_render) method. This is useful for complex rendering scenarios:
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
# First yield - render with one context
|
||||
# First yield
|
||||
with context.push({"mode": "header"}):
|
||||
header_html, header_error = yield template.render(context)
|
||||
header_html, header_error = yield lambda: template.render(context)
|
||||
|
||||
# Second yield - render with different context
|
||||
# Second yield
|
||||
with context.push({"mode": "body"}):
|
||||
body_html, body_error = yield template.render(context)
|
||||
body_html, body_error = yield lambda: template.render(context)
|
||||
|
||||
# Third yield - render a string directly
|
||||
# Third yield
|
||||
footer_html, footer_error = yield "Footer content"
|
||||
|
||||
# Process all results and return final output
|
||||
# Process all
|
||||
if header_error or body_error or footer_error:
|
||||
return "Error occurred during rendering"
|
||||
|
||||
|
|
@ -246,7 +276,7 @@ That is, a component that catches errors in nested components and displays a fal
|
|||
|
||||
```django
|
||||
{% component "error_boundary" %}
|
||||
{% fill "content" %}
|
||||
{% fill "default" %}
|
||||
{% component "nested_component" %}
|
||||
{% endfill %}
|
||||
{% fill "fallback" %}
|
||||
|
|
@ -259,10 +289,13 @@ To implement this, we render the fallback slot in [`on_render()`](../../../refer
|
|||
and return it if an error occured:
|
||||
|
||||
```djc_py
|
||||
class ErrorFallback(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
fallback: Optional[str] = None
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from django.template import Context, Template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django_components import Component, OnRenderGenerator, SlotInput, types
|
||||
|
||||
class ErrorFallback(Component):
|
||||
class Slots(NamedTuple):
|
||||
default: Optional[SlotInput] = None
|
||||
fallback: Optional[SlotInput] = None
|
||||
|
|
@ -280,30 +313,22 @@ class ErrorFallback(Component):
|
|||
context: Context,
|
||||
template: Template,
|
||||
) -> OnRenderGenerator:
|
||||
fallback_kwarg = cast(ErrorFallback.Kwargs, self.kwargs).fallback
|
||||
fallback_slot = cast(ErrorFallback.Slots, self.slots).default
|
||||
fallback_slot = self.slots.default
|
||||
|
||||
if fallback_kwarg is not None and fallback_slot is not None:
|
||||
raise TemplateSyntaxError(
|
||||
"The 'fallback' argument and slot cannot both be provided. Please provide only one.",
|
||||
)
|
||||
result, error = yield lambda: template.render(context)
|
||||
|
||||
result, error = yield template.render(context)
|
||||
|
||||
# No error, return the result
|
||||
# No error, return the original result
|
||||
if error is None:
|
||||
return result
|
||||
return None
|
||||
|
||||
# Error, return the fallback
|
||||
if fallback_kwarg is not None:
|
||||
return fallback_kwarg
|
||||
elif fallback_slot is not None:
|
||||
# Render the template second time, this time with the error
|
||||
# So that we render the fallback slot with proper access to the outer context and whatnot.
|
||||
if fallback_slot is not None:
|
||||
# Render the template second time, this time rendering
|
||||
# the fallback branch
|
||||
with context.push({"error": error}):
|
||||
return template.render(context)
|
||||
else:
|
||||
return ""
|
||||
return mark_safe("<pre>An error occurred</pre>")
|
||||
```
|
||||
|
||||
### `on_render_after`
|
||||
|
|
@ -333,30 +358,31 @@ as the second part of [`on_render()`](#on_render) (after the `yield`).
|
|||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
if error is None:
|
||||
# The rendering succeeded
|
||||
return result
|
||||
else:
|
||||
# The rendering failed
|
||||
# If rendering succeeded, keep the original result
|
||||
# Otherwise, print the error
|
||||
if error is not None:
|
||||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
Same as [`on_render()`](#on_render),
|
||||
you can return a new HTML, raise a new exception, or return nothing:
|
||||
|
||||
1. Return a new HTML
|
||||
1. Return new HTML
|
||||
|
||||
The new HTML will be used as the final output.
|
||||
|
||||
If the original template raised an error, it will be ignored.
|
||||
If the original template raised an error, the original error will be ignored.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
return "NEW HTML"
|
||||
# Fallback if rendering failed
|
||||
# Otherwise, we keep the original HTML
|
||||
if error is not None:
|
||||
return "FALLBACK HTML"
|
||||
```
|
||||
|
||||
2. Raise a new exception
|
||||
2. Raise new exception
|
||||
|
||||
The new exception is what will bubble up from the component.
|
||||
|
||||
|
|
@ -365,26 +391,33 @@ you can return a new HTML, raise a new exception, or return nothing:
|
|||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
raise Exception("Error message")
|
||||
# Override the original error
|
||||
# Otherwise, we keep the original HTML
|
||||
if error is not None:
|
||||
raise Exception("My new error") from error
|
||||
```
|
||||
|
||||
3. Return nothing (or `None`) to handle the result as usual
|
||||
3. No change - Return nothing or `None`
|
||||
|
||||
If you don't raise an exception, and neither return a new HTML,
|
||||
then original HTML / error will be used:
|
||||
If you neither raise an exception, nor return a new HTML,
|
||||
then the original HTML / error will be used:
|
||||
|
||||
- If rendering succeeded, the original HTML will be used as the final output.
|
||||
- If rendering failed, the original error will be propagated.
|
||||
|
||||
This can be useful for side effects like tracking the errors that occurred during the rendering:
|
||||
|
||||
```py
|
||||
from myapp.metrics import track_rendering_error
|
||||
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
# Track how many times the rendering failed
|
||||
if error is not None:
|
||||
# The rendering failed
|
||||
print(f"Error: {error}")
|
||||
track_rendering_error(error)
|
||||
```
|
||||
|
||||
## Example
|
||||
## Example: Tabs
|
||||
|
||||
You can use hooks together with [provide / inject](#how-to-use-provide--inject) to create components
|
||||
that accept a list of items via a slot.
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ ignore = [
|
|||
"PLR0913", # Too many arguments in function definition (6 > 5)
|
||||
"PLR2004", # Magic value used in comparison, consider replacing `123` with a constant variable
|
||||
"RET504", # Unnecessary assignment to `collected` before `return` statement
|
||||
"RET505", # Unnecessary `elif` after `return` statement
|
||||
"S308", # Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
"S603", # `subprocess` call: check for execution of untrusted input
|
||||
"SIM108", # Use ternary operator `...` instead of `if`-`else`-block
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ else:
|
|||
|
||||
|
||||
OnRenderGenerator = Generator[
|
||||
Optional[SlotResult],
|
||||
Optional[Union[SlotResult, Callable[[], SlotResult]]],
|
||||
Tuple[Optional[SlotResult], Optional[Exception]],
|
||||
Optional[SlotResult],
|
||||
]
|
||||
|
|
@ -122,7 +122,7 @@ method if it yields (and thus returns a generator).
|
|||
|
||||
When `on_render()` is a generator then it:
|
||||
|
||||
- Yields a rendered template (string or `None`)
|
||||
- Yields a rendered template (string or `None`) or a lambda function to be called later.
|
||||
|
||||
- Receives back a tuple of `(final_output, error)`.
|
||||
|
||||
|
|
@ -151,13 +151,15 @@ class MyTable(Component):
|
|||
# Same as `Component.on_render_before()`
|
||||
context["hello"] = "world"
|
||||
|
||||
# Yield rendered template to receive fully-rendered template or error
|
||||
html, error = yield template.render(context)
|
||||
# Yield a function that renders the template
|
||||
# to receive fully-rendered template or error.
|
||||
html, error = yield lambda: template.render(context)
|
||||
|
||||
# Do something AFTER rendering template, or post-process
|
||||
# the rendered template.
|
||||
# Same as `Component.on_render_after()`
|
||||
return html + "<p>Hello</p>"
|
||||
if html is not None:
|
||||
return html + "<p>Hello</p>"
|
||||
```
|
||||
|
||||
**Multiple yields example:**
|
||||
|
|
@ -165,18 +167,18 @@ class MyTable(Component):
|
|||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template) -> OnRenderGenerator:
|
||||
# First yield - render with one context
|
||||
# First yield
|
||||
with context.push({"mode": "header"}):
|
||||
header_html, header_error = yield template.render(context)
|
||||
header_html, header_error = yield lambda: template.render(context)
|
||||
|
||||
# Second yield - render with different context
|
||||
# Second yield
|
||||
with context.push({"mode": "body"}):
|
||||
body_html, body_error = yield template.render(context)
|
||||
body_html, body_error = yield lambda: template.render(context)
|
||||
|
||||
# Third yield - render a string directly
|
||||
# Third yield
|
||||
footer_html, footer_error = yield "Footer content"
|
||||
|
||||
# Process all results and return final output
|
||||
# Process all results
|
||||
if header_error or body_error or footer_error:
|
||||
return "Error occurred during rendering"
|
||||
|
||||
|
|
@ -2023,7 +2025,7 @@ class Component(metaclass=ComponentMeta):
|
|||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
html, error = yield lambda: template.render(context)
|
||||
|
||||
if error is None:
|
||||
# The rendering succeeded
|
||||
|
|
@ -2044,7 +2046,7 @@ class Component(metaclass=ComponentMeta):
|
|||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
html, error = yield lambda: template.render(context)
|
||||
|
||||
return "NEW HTML"
|
||||
```
|
||||
|
|
@ -2058,7 +2060,7 @@ class Component(metaclass=ComponentMeta):
|
|||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
html, error = yield lambda: template.render(context)
|
||||
|
||||
raise Exception("Error message")
|
||||
```
|
||||
|
|
@ -2074,7 +2076,7 @@ class Component(metaclass=ComponentMeta):
|
|||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
html, error = yield lambda: template.render(context)
|
||||
|
||||
if error is not None:
|
||||
# The rendering failed
|
||||
|
|
@ -2091,11 +2093,11 @@ class Component(metaclass=ComponentMeta):
|
|||
def on_render(self, context, template):
|
||||
# First yield - render with one context
|
||||
with context.push({"mode": "header"}):
|
||||
header_html, header_error = yield template.render(context)
|
||||
header_html, header_error = yield lambda: template.render(context)
|
||||
|
||||
# Second yield - render with different context
|
||||
with context.push({"mode": "body"}):
|
||||
body_html, body_error = yield template.render(context)
|
||||
body_html, body_error = yield lambda: template.render(context)
|
||||
|
||||
# Third yield - render a string directly
|
||||
footer_html, footer_error = yield "Footer content"
|
||||
|
|
@ -3852,7 +3854,7 @@ class Component(metaclass=ComponentMeta):
|
|||
# ```
|
||||
# class MyTable(Component):
|
||||
# def on_render(self, context, template):
|
||||
# html, error = yield template.render(context)
|
||||
# html, error = yield lamba: template.render(context)
|
||||
# return html + "<p>Hello</p>"
|
||||
# ```
|
||||
#
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import re
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING, Callable, Deque, Dict, List, NamedTuple, Optional, Set, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Callable, Deque, Dict, List, Literal, NamedTuple, Optional, Set, Tuple, Union
|
||||
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ class ErrorPart(NamedTuple):
|
|||
class GeneratorResult(NamedTuple):
|
||||
html: Optional[str]
|
||||
error: Optional[Exception]
|
||||
needs_processing: bool
|
||||
action: Literal["needs_processing", "rerender", "stop"]
|
||||
spent: bool
|
||||
"""Whether the generator has been "spent" - e.g. reached its end with `StopIteration`."""
|
||||
|
||||
|
|
@ -431,7 +431,7 @@ def component_post_render(
|
|||
|
||||
# The generator yielded or returned a new HTML. We want to process it as if
|
||||
# it's a new component's HTML.
|
||||
if result.needs_processing:
|
||||
if result.action == "needs_processing":
|
||||
# Ignore the old version of the component
|
||||
ignored_components.add(item_id)
|
||||
|
||||
|
|
@ -455,8 +455,20 @@ def component_post_render(
|
|||
parts_to_process = parse_component_result(new_html or "", new_item_id, full_path)
|
||||
process_queue.extendleft(reversed(parts_to_process))
|
||||
return
|
||||
# If we don't need to re-do the processing, then we can just use the result.
|
||||
component_html, error = new_html, result.error
|
||||
elif result.action == "rerender":
|
||||
# Ignore the old version of the component
|
||||
ignored_components.add(item_id)
|
||||
|
||||
new_version = item_id.version + 1
|
||||
new_item_id = QueueItemId(component_id=item_id.component_id, version=new_version)
|
||||
# Set the current parent as the parent of the new version
|
||||
child_to_parent[new_item_id] = parent_id
|
||||
|
||||
next_renderer_result(item_id=new_item_id, error=result.error, full_path=full_path)
|
||||
return
|
||||
else:
|
||||
# If we don't need to re-do the processing, then we can just use the result.
|
||||
component_html, error = new_html, result.error
|
||||
|
||||
# Allow to optionally override/modify the rendered content from `Component.on_render_after()`
|
||||
# and by extensions' `on_component_rendered` hooks.
|
||||
|
|
@ -590,22 +602,55 @@ def _call_generator(
|
|||
# The return value is on `StopIteration.value`
|
||||
new_output = generator_err.value
|
||||
if new_output is not None:
|
||||
return GeneratorResult(html=new_output, error=None, needs_processing=True, spent=True)
|
||||
return GeneratorResult(html=new_output, error=None, action="needs_processing", spent=True)
|
||||
# Nothing returned at the end of the generator, keep the original HTML and error
|
||||
return GeneratorResult(html=html, error=error, needs_processing=False, spent=True)
|
||||
return GeneratorResult(html=html, error=error, action="stop", spent=True)
|
||||
|
||||
# Catch if `Component.on_render()` raises an exception, in which case this becomes
|
||||
# the new error.
|
||||
except Exception as new_error: # noqa: BLE001
|
||||
set_component_error_message(new_error, full_path[1:])
|
||||
return GeneratorResult(html=None, error=new_error, needs_processing=False, spent=True)
|
||||
return GeneratorResult(html=None, error=new_error, action="stop", spent=True)
|
||||
|
||||
# If the generator didn't raise an error then `Component.on_render()` yielded a new HTML result,
|
||||
# that we need to process.
|
||||
else:
|
||||
# NOTE: Users may yield a function from `on_render()` instead of rendered template:
|
||||
# ```py
|
||||
# class MyTable(Component):
|
||||
# def on_render(self, context, template):
|
||||
# html, error = yield lambda: template.render(context)
|
||||
# return html + "<p>Hello</p>"
|
||||
# ```
|
||||
# This is so that we can keep the API simple, handling the errors in template rendering.
|
||||
# Otherwise, people would have to write out:
|
||||
# ```py
|
||||
# try:
|
||||
# intermediate = template.render(context)
|
||||
# except Exception as err:
|
||||
# result = None
|
||||
# error = err
|
||||
# else:
|
||||
# result, error = yield intermediate
|
||||
# ```
|
||||
if callable(new_result):
|
||||
try:
|
||||
new_result = new_result()
|
||||
except Exception as new_err: # noqa: BLE001
|
||||
started_generators_cache[on_render_generator] = True
|
||||
set_component_error_message(new_err, full_path[1:])
|
||||
# In other cases, when a component raises an error during rendering,
|
||||
# we discard the errored component and move up to the parent component
|
||||
# to decide what to do (propagate or return a new HTML).
|
||||
#
|
||||
# But if user yielded a function from `Component.on_render()`,
|
||||
# we want to let the CURRENT component decide what to do.
|
||||
# Hence why the action is "rerender" instead of "stop".
|
||||
return GeneratorResult(html=None, error=new_err, action="rerender", spent=False)
|
||||
|
||||
if is_first_send or new_result is not None:
|
||||
started_generators_cache[on_render_generator] = True
|
||||
return GeneratorResult(html=new_result, error=None, needs_processing=True, spent=False)
|
||||
return GeneratorResult(html=new_result, error=None, action="needs_processing", spent=False)
|
||||
|
||||
# Generator yielded `None`, keep the previous HTML and error
|
||||
return GeneratorResult(html=html, error=error, needs_processing=False, spent=False)
|
||||
return GeneratorResult(html=html, error=error, action="stop", spent=False)
|
||||
|
|
|
|||
|
|
@ -1533,7 +1533,7 @@ class TestComponentHook:
|
|||
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
calls.append("slotted__on_render_pre")
|
||||
_html, _error = yield template.render(context) # type: ignore[union-attr]
|
||||
_html, _error = yield lambda: template.render(context) # type: ignore[union-attr]
|
||||
|
||||
calls.append("slotted__on_render_post")
|
||||
|
||||
|
|
@ -1566,7 +1566,7 @@ class TestComponentHook:
|
|||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
_html, _error = yield template.render(context)
|
||||
_html, _error = yield lambda: template.render(context)
|
||||
|
||||
calls.append("inner__on_render_post")
|
||||
|
||||
|
|
@ -1600,7 +1600,7 @@ class TestComponentHook:
|
|||
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
calls.append("middle__on_render_pre")
|
||||
_html, _error = yield template.render(context) # type: ignore[union-attr]
|
||||
_html, _error = yield lambda: template.render(context) # type: ignore[union-attr]
|
||||
|
||||
calls.append("middle__on_render_post")
|
||||
|
||||
|
|
@ -1632,7 +1632,7 @@ class TestComponentHook:
|
|||
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
calls.append("outer__on_render_pre")
|
||||
_html, _error = yield template.render(context) # type: ignore[union-attr]
|
||||
_html, _error = yield lambda: template.render(context) # type: ignore[union-attr]
|
||||
|
||||
calls.append("outer__on_render_post")
|
||||
|
||||
|
|
@ -1752,7 +1752,7 @@ class TestComponentHook:
|
|||
# Check we can modify entries set by other methods
|
||||
context["from_on_before__edited1"] = context["from_on_before"] + " (on_render)"
|
||||
|
||||
_html, _error = yield template.render(context)
|
||||
_html, _error = yield lambda: template.render(context)
|
||||
|
||||
context["from_on_render_post"] = "3"
|
||||
|
||||
|
|
@ -1804,7 +1804,7 @@ class TestComponentHook:
|
|||
def on_render(self, context: Context, template: Template):
|
||||
template.nodelist.append(TextNode("\n---\nFROM_ON_RENDER_PRE"))
|
||||
|
||||
_html, _error = yield template.render(context)
|
||||
_html, _error = yield lambda: template.render(context)
|
||||
|
||||
template.nodelist.append(TextNode("\n---\nFROM_ON_RENDER_POST"))
|
||||
|
||||
|
|
@ -1831,6 +1831,61 @@ class TestComponentHook:
|
|||
""",
|
||||
)
|
||||
|
||||
def test_lambda_yield(self):
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
text
|
||||
"""
|
||||
|
||||
def on_render(self, context: Context, template: Template):
|
||||
html, _error = yield lambda: template.render(context)
|
||||
return html + "<p>Hello</p>"
|
||||
|
||||
rendered = SimpleComponent.render()
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"text<p data-djc-id-ca1bc3e>Hello</p>",
|
||||
)
|
||||
|
||||
# Works without lambda
|
||||
class SimpleComponent2(SimpleComponent):
|
||||
def on_render(self, context: Context, template: Template):
|
||||
html, _error = yield template.render(context)
|
||||
return html + "<p>Hello</p>"
|
||||
|
||||
rendered2 = SimpleComponent2.render()
|
||||
assertHTMLEqual(
|
||||
rendered2,
|
||||
"text<p data-djc-id-ca1bc3f>Hello</p>",
|
||||
)
|
||||
|
||||
def test_lambda_yield_error(self):
|
||||
def broken_template():
|
||||
raise ValueError("BROKEN")
|
||||
|
||||
class SimpleComponent(Component):
|
||||
def on_render(self, context: Context, template: Template):
|
||||
_html, error = yield lambda: broken_template()
|
||||
error.args = ("ERROR MODIFIED",)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match=re.escape("An error occured while rendering components SimpleComponent:\nERROR MODIFIED")
|
||||
):
|
||||
SimpleComponent.render()
|
||||
|
||||
# Does NOT work without lambda
|
||||
class SimpleComponent2(SimpleComponent):
|
||||
def on_render(self, context: Context, template: Template):
|
||||
# This raises an error instead of capturing it,
|
||||
# so we never get to modifying the error.
|
||||
_html, error = yield broken_template()
|
||||
error.args = ("ERROR MODIFIED",)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match=re.escape("An error occured while rendering components SimpleComponent2:\nBROKEN")
|
||||
):
|
||||
SimpleComponent2.render()
|
||||
|
||||
def test_on_render_no_yield(self):
|
||||
class SimpleComponent(Component):
|
||||
template: types.django_html = """
|
||||
|
|
@ -1852,7 +1907,7 @@ class TestComponentHook:
|
|||
"""
|
||||
|
||||
def on_render(self, context: Context, template: Template):
|
||||
_html, error = yield template.render(context)
|
||||
_html, error = yield lambda: template.render(context)
|
||||
|
||||
raise error from None # Re-raise original error
|
||||
|
||||
|
|
@ -1879,15 +1934,15 @@ class TestComponentHook:
|
|||
assert template is not None
|
||||
|
||||
with context.push({"case": 1}):
|
||||
html1, error1 = yield template.render(context)
|
||||
html1, error1 = yield lambda: template.render(context)
|
||||
results.append((html1, error1))
|
||||
|
||||
with context.push({"case": 2}):
|
||||
html2, error2 = yield template.render(context)
|
||||
html2, error2 = yield lambda: template.render(context)
|
||||
results.append((html2.strip(), error2))
|
||||
|
||||
with context.push({"case": 3}):
|
||||
html3, error3 = yield template.render(context)
|
||||
html3, error3 = yield lambda: template.render(context)
|
||||
results.append((html3.strip(), error3))
|
||||
|
||||
html4, error4 = yield "<div>Other result</div>"
|
||||
|
|
@ -1990,7 +2045,7 @@ class TestComponentHook:
|
|||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
_html, _error = yield template.render(context)
|
||||
_html, _error = yield lambda: template.render(context)
|
||||
return None # noqa: PLR1711
|
||||
|
||||
elif action == "no_return":
|
||||
|
|
@ -2000,7 +2055,7 @@ class TestComponentHook:
|
|||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
_html, _error = yield template.render(context)
|
||||
_html, _error = yield lambda: template.render(context)
|
||||
|
||||
elif action == "raise_error":
|
||||
|
||||
|
|
@ -2009,7 +2064,7 @@ class TestComponentHook:
|
|||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
_html, _error = yield template.render(context)
|
||||
_html, _error = yield lambda: template.render(context)
|
||||
raise ValueError("ERROR_FROM_ON_RENDER")
|
||||
|
||||
elif action == "return_html":
|
||||
|
|
@ -2019,7 +2074,7 @@ class TestComponentHook:
|
|||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
_html, _error = yield template.render(context)
|
||||
_html, _error = yield lambda: template.render(context)
|
||||
return "HTML_FROM_ON_RENDER"
|
||||
|
||||
else:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue