mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 14:28:18 +00:00
feat: allow to highlight slots and components for debugging (#942)
This commit is contained in:
parent
da54d97343
commit
de32d449d9
14 changed files with 306 additions and 49 deletions
|
@ -6,6 +6,7 @@
|
|||
- [Advanced](concepts/advanced/)
|
||||
- Guides
|
||||
- [Setup](guides/setup/)
|
||||
- [Other](guides/other/)
|
||||
- [Dev guides](guides/devguides/)
|
||||
- [API Documentation](reference/)
|
||||
- [Release notes](release_notes.md)
|
||||
|
|
145
docs/guides/other/troubleshooting.md
Normal file
145
docs/guides/other/troubleshooting.md
Normal file
|
@ -0,0 +1,145 @@
|
|||
---
|
||||
title: Troubleshooting
|
||||
weight: 1
|
||||
---
|
||||
|
||||
As larger projects get more complex, it can be hard to debug issues. Django Components provides a number of tools and approaches that can help you with that.
|
||||
|
||||
## Component and slot highlighting
|
||||
|
||||
Django Components provides a visual debugging feature that helps you understand the structure and boundaries of your components and slots. When enabled, it adds a colored border and a label around each component and slot on your rendered page.
|
||||
|
||||
To enable component and slot highlighting, set
|
||||
[`debug_highlight_components`](../../../reference/settings/#django_components.app_settings.ComponentsSettings.debug_highlight_components)
|
||||
and/or [`debug_highlight_slots`](../../../reference/settings/#django_components.app_settings.ComponentsSettings.debug_highlight_slots)
|
||||
to `True` in your `settings.py` file:
|
||||
|
||||
```python
|
||||
from django_components import ComponentsSettings
|
||||
|
||||
COMPONENTS = ComponentsSettings(
|
||||
debug_highlight_components=True,
|
||||
debug_highlight_slots=True,
|
||||
)
|
||||
```
|
||||
|
||||
Components will be highlighted with a **blue** border and label:
|
||||
|
||||

|
||||
|
||||
While the slots will be highlighted with a **red** border and label:
|
||||
|
||||

|
||||
|
||||
!!! warning
|
||||
|
||||
Use this feature ONLY in during development. Do NOT use it in production.
|
||||
|
||||
## Component path in errors
|
||||
|
||||
When an error occurs, the error message will show the path to the component that
|
||||
caused the error. E.g.
|
||||
|
||||
```
|
||||
KeyError: "An error occured while rendering components MyPage > MyLayout > MyComponent > Childomponent(slot:content)
|
||||
```
|
||||
|
||||
The error message contains also the slot paths, so if you have a template like this:
|
||||
|
||||
```django
|
||||
{% component "my_page" %}
|
||||
{% slot "content" %}
|
||||
{% component "table" %}
|
||||
{% slot "header" %}
|
||||
{% component "table_header" %}
|
||||
... {# ERROR HERE #}
|
||||
{% endcomponent %}
|
||||
{% endslot %}
|
||||
{% endcomponent %}
|
||||
{% endslot %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
Then the error message will show the path to the component that caused the error:
|
||||
|
||||
```
|
||||
KeyError: "An error occured while rendering components my_page > layout > layout(slot:content) > my_page(slot:content) > table > table(slot:header) > table_header > table_header(slot:content)
|
||||
```
|
||||
|
||||
## Debug and trace logging
|
||||
|
||||
Django components supports [logging with Django](https://docs.djangoproject.com/en/5.0/howto/logging/#logging-how-to).
|
||||
|
||||
To configure logging for Django components, set the `django_components` logger in
|
||||
[`LOGGING`](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-LOGGING)
|
||||
in `settings.py` (below).
|
||||
|
||||
Also see the [`settings.py` file in sampleproject](https://github.com/django-components/django-components/blob/master/sampleproject/sampleproject/settings.py) for a real-life example.
|
||||
|
||||
```py
|
||||
import logging
|
||||
import sys
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
'class': 'logging.StreamHandler',
|
||||
'stream': sys.stdout,
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django_components": {
|
||||
"level": logging.DEBUG,
|
||||
"handlers": ["console"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
To set TRACE level, set `"level"` to `5`:
|
||||
|
||||
```py
|
||||
LOGGING = {
|
||||
"loggers": {
|
||||
"django_components": {
|
||||
"level": 5,
|
||||
"handlers": ["console"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Logger levels
|
||||
|
||||
As of v0.126, django-components primarily uses these logger levels:
|
||||
|
||||
- `DEBUG`: Report on loading associated HTML / JS / CSS files, autodiscovery, etc.
|
||||
- `TRACE`: Detailed interaction of components and slots. Logs when template tags,
|
||||
components, and slots are started / ended rendering, and when a slot is filled.
|
||||
|
||||
## Slot origin
|
||||
|
||||
When you pass a slot fill to a Component, the component and slot names is remebered
|
||||
on the slot object.
|
||||
|
||||
Thus, you can check where a slot was filled from by printing it out:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
def on_render_before(self):
|
||||
print(self.input.slots)
|
||||
```
|
||||
|
||||
might print:
|
||||
|
||||
```txt
|
||||
{
|
||||
'content': <Slot component_name='layout' slot_name='content'>,
|
||||
'header': <Slot component_name='my_page' slot_name='header'>,
|
||||
'left_panel': <Slot component_name='layout' slot_name='left_panel'>,
|
||||
}
|
||||
```
|
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
weight: 3
|
||||
---
|
||||
|
||||
Django components supports [logging with Django](https://docs.djangoproject.com/en/5.0/howto/logging/#logging-how-to).
|
||||
This can help with troubleshooting.
|
||||
|
||||
To configure logging for Django components, set the `django_components` logger in
|
||||
[`LOGGING`](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-LOGGING)
|
||||
in `settings.py` (below).
|
||||
|
||||
Also see the [`settings.py` file in sampleproject](https://github.com/django-components/django-components/blob/master/sampleproject/sampleproject/settings.py) for a real-life example.
|
||||
|
||||
```py
|
||||
import logging
|
||||
import sys
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
'class': 'logging.StreamHandler',
|
||||
'stream': sys.stdout,
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"django_components": {
|
||||
"level": logging.DEBUG,
|
||||
"handlers": ["console"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
BIN
docs/images/debug-highlight-components.png
Normal file
BIN
docs/images/debug-highlight-components.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 493 KiB |
BIN
docs/images/debug-highlight-slots.png
Normal file
BIN
docs/images/debug-highlight-slots.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 365 KiB |
|
@ -10,8 +10,7 @@ that will be added by installing `django_components`:
|
|||
|
||||
```txt
|
||||
usage: manage.py upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||
[--skip-checks]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
|
||||
```
|
||||
|
||||
|
@ -53,10 +52,9 @@ Updates component and component_block tags to the new syntax
|
|||
## `startcomponent`
|
||||
|
||||
```txt
|
||||
usage: manage.py startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force]
|
||||
[--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||
[--skip-checks]
|
||||
usage: manage.py startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
||||
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
name
|
||||
|
||||
```
|
||||
|
|
|
@ -38,6 +38,8 @@ defaults = ComponentsSettings(
|
|||
dirs=[Path(settings.BASE_DIR) / "components"],
|
||||
# App-level "components" dirs, e.g. `[app]/components/`
|
||||
app_dirs=["components"],
|
||||
debug_highlight_components=False,
|
||||
debug_highlight_slots=False,
|
||||
dynamic_component_name="dynamic",
|
||||
libraries=[], # E.g. ["mysite.components.forms", ...]
|
||||
multiline_tags=True,
|
||||
|
@ -93,6 +95,26 @@ defaults = ComponentsSettings(
|
|||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
::: django_components.app_settings.ComponentsSettings.debug_highlight_components
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
::: django_components.app_settings.ComponentsSettings.debug_highlight_slots
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
::: django_components.app_settings.ComponentsSettings.dirs
|
||||
options:
|
||||
show_root_heading: true
|
||||
|
|
|
@ -20,7 +20,7 @@ Import as
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1118" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1066" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -43,7 +43,7 @@ If you insert this tag multiple times, ALL CSS links will be duplicately inserte
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1140" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1088" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1257" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1436" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -175,7 +175,7 @@ can access only the data that was explicitly passed to it:
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L466" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L577" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -336,7 +336,7 @@ renders
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L9" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L12" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -416,7 +416,7 @@ user = self.inject("user_data")["user"]
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L149" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L153" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -239,6 +239,34 @@ class ComponentsSettings(NamedTuple):
|
|||
> [here](https://github.com/django-components/django-components/issues/498).
|
||||
"""
|
||||
|
||||
debug_highlight_components: Optional[bool] = None
|
||||
"""
|
||||
Enable / disable component highlighting.
|
||||
See [Troubleshooting](../../guides/other/troubleshooting#component-highlighting) for more details.
|
||||
|
||||
Defaults to `False`.
|
||||
|
||||
```python
|
||||
COMPONENTS = ComponentsSettings(
|
||||
debug_highlight_components=True,
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
debug_highlight_slots: Optional[bool] = None
|
||||
"""
|
||||
Enable / disable slot highlighting.
|
||||
See [Troubleshooting](../../guides/other/troubleshooting#slot-highlighting) for more details.
|
||||
|
||||
Defaults to `False`.
|
||||
|
||||
```python
|
||||
COMPONENTS = ComponentsSettings(
|
||||
debug_highlight_slots=True,
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
dynamic_component_name: Optional[str] = None
|
||||
"""
|
||||
By default, the [dynamic component](../components#django_components.components.dynamic.DynamicComponent)
|
||||
|
@ -594,6 +622,8 @@ defaults = ComponentsSettings(
|
|||
dirs=Dynamic(lambda: [Path(settings.BASE_DIR) / "components"]), # type: ignore[arg-type]
|
||||
# App-level "components" dirs, e.g. `[app]/components/`
|
||||
app_dirs=["components"],
|
||||
debug_highlight_components=False,
|
||||
debug_highlight_slots=False,
|
||||
dynamic_component_name="dynamic",
|
||||
libraries=[], # E.g. ["mysite.components.forms", ...]
|
||||
multiline_tags=True,
|
||||
|
@ -643,6 +673,14 @@ class InternalSettings:
|
|||
def APP_DIRS(self) -> Sequence[str]:
|
||||
return default(self._settings.app_dirs, cast(List[str], defaults.app_dirs))
|
||||
|
||||
@property
|
||||
def DEBUG_HIGHLIGHT_COMPONENTS(self) -> bool:
|
||||
return default(self._settings.debug_highlight_components, cast(bool, defaults.debug_highlight_components))
|
||||
|
||||
@property
|
||||
def DEBUG_HIGHLIGHT_SLOTS(self) -> bool:
|
||||
return default(self._settings.debug_highlight_slots, cast(bool, defaults.debug_highlight_slots))
|
||||
|
||||
@property
|
||||
def DYNAMIC_COMPONENT_NAME(self) -> str:
|
||||
return default(self._settings.dynamic_component_name, cast(str, defaults.dynamic_component_name))
|
||||
|
|
|
@ -34,7 +34,7 @@ from django.test.signals import template_rendered
|
|||
from django.utils.html import conditional_escape
|
||||
from django.views import View
|
||||
|
||||
from django_components.app_settings import ContextBehavior
|
||||
from django_components.app_settings import ContextBehavior, app_settings
|
||||
from django_components.component_media import ComponentMediaInput, ComponentMediaMeta
|
||||
from django_components.component_registry import ComponentRegistry
|
||||
from django_components.component_registry import registry as registry_
|
||||
|
@ -69,6 +69,7 @@ from django_components.slots import (
|
|||
resolve_fills,
|
||||
)
|
||||
from django_components.template import cached_template
|
||||
from django_components.util.component_highlight import apply_component_highlight
|
||||
from django_components.util.context import snapshot_context
|
||||
from django_components.util.django_monkeypatch import is_template_cls_patched
|
||||
from django_components.util.exception import component_error_message
|
||||
|
@ -1153,6 +1154,9 @@ class Component(
|
|||
del component_context_cache[render_id] # type: ignore[arg-type]
|
||||
unregister_provide_reference(render_id) # type: ignore[arg-type]
|
||||
|
||||
if app_settings.DEBUG_HIGHLIGHT_COMPONENTS:
|
||||
html = apply_component_highlight("component", html, f"{self.name} ({render_id})")
|
||||
|
||||
return html
|
||||
|
||||
post_render_callbacks[render_id] = on_component_rendered
|
||||
|
|
|
@ -180,7 +180,7 @@ def component_post_render(
|
|||
# </div>
|
||||
# ```
|
||||
#
|
||||
# Then we end up with 3 bits - 1. test before, 2. component, and 3. text after
|
||||
# Then we end up with 3 bits - 1. text before, 2. component, and 3. text after
|
||||
#
|
||||
# We know when we've arrived at component's end, because `child_id` will be set to `None`.
|
||||
# So we can collect the HTML parts by the component ID, and when we hit the end, we join
|
||||
|
|
|
@ -24,10 +24,11 @@ from django.template.base import NodeList, TextNode
|
|||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.utils.safestring import SafeString, mark_safe
|
||||
|
||||
from django_components.app_settings import ContextBehavior
|
||||
from django_components.app_settings import ContextBehavior, app_settings
|
||||
from django_components.context import _COMPONENT_CONTEXT_KEY, _INJECT_CONTEXT_KEY_PREFIX
|
||||
from django_components.node import BaseNode
|
||||
from django_components.perfutil.component import component_context_cache
|
||||
from django_components.util.component_highlight import apply_component_highlight
|
||||
from django_components.util.exception import add_slot_to_error_message
|
||||
from django_components.util.logger import trace_component_msg
|
||||
from django_components.util.misc import get_index, get_last_index, is_identifier
|
||||
|
@ -537,6 +538,9 @@ class SlotNode(BaseNode):
|
|||
# the render function ALWAYS receives them.
|
||||
output = slot_fill.slot(used_ctx, kwargs, slot_ref)
|
||||
|
||||
if app_settings.DEBUG_HIGHLIGHT_SLOTS:
|
||||
output = apply_component_highlight("slot", output, f"{component_name} - {slot_name}")
|
||||
|
||||
trace_component_msg(
|
||||
"RENDER_SLOT_END",
|
||||
component_name=component_name,
|
||||
|
|
43
src/django_components/util/component_highlight.py
Normal file
43
src/django_components/util/component_highlight.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
from typing import Literal, NamedTuple
|
||||
|
||||
from django_components.util.misc import gen_id
|
||||
|
||||
|
||||
class HighlightColor(NamedTuple):
|
||||
text_color: str
|
||||
border_color: str
|
||||
|
||||
|
||||
COLORS = {
|
||||
"component": HighlightColor(text_color="#2f14bb", border_color="blue"),
|
||||
"slot": HighlightColor(text_color="#bb1414", border_color="#e40c0c"),
|
||||
}
|
||||
|
||||
|
||||
def apply_component_highlight(type: Literal["component", "slot"], output: str, name: str) -> str:
|
||||
"""
|
||||
Wrap HTML (string) in a div with a border and a highlight color.
|
||||
|
||||
This is part of the component / slot highlighting feature. User can toggle on
|
||||
to see the component / slot boundaries.
|
||||
"""
|
||||
color = COLORS[type]
|
||||
|
||||
# Because the component / slot name is set via styling as a `::before` pseudo-element,
|
||||
# we need to generate a unique ID for each component / slot to avoid conflicts.
|
||||
highlight_id = gen_id()
|
||||
|
||||
output = f"""
|
||||
<style>
|
||||
.{type}-highlight-{highlight_id}::before {{
|
||||
content: "{name}: ";
|
||||
font-weight: bold;
|
||||
color: {color.text_color};
|
||||
}}
|
||||
</style>
|
||||
<div class="{type}-highlight-{highlight_id}" style="border: 1px solid {color.border_color}">
|
||||
{output}
|
||||
</div>
|
||||
"""
|
||||
|
||||
return output
|
36
tests/test_component_highlight.py
Normal file
36
tests/test_component_highlight.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from django_components.util.component_highlight import apply_component_highlight, COLORS
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
||||
class ComponentHighlightTests(BaseTestCase):
|
||||
def test_component_highlight(self):
|
||||
# Test component highlighting
|
||||
test_html = "<div>Test content</div>"
|
||||
component_name = "TestComponent"
|
||||
result = apply_component_highlight("component", test_html, component_name)
|
||||
|
||||
# Check that the output contains the component name
|
||||
self.assertIn(component_name, result)
|
||||
# Check that the output contains the original HTML
|
||||
self.assertIn(test_html, result)
|
||||
# Check that the component colors are used
|
||||
self.assertIn(COLORS["component"].text_color, result)
|
||||
self.assertIn(COLORS["component"].border_color, result)
|
||||
|
||||
def test_slot_highlight(self):
|
||||
# Test slot highlighting
|
||||
test_html = "<span>Slot content</span>"
|
||||
slot_name = "content-slot"
|
||||
result = apply_component_highlight("slot", test_html, slot_name)
|
||||
|
||||
# Check that the output contains the slot name
|
||||
self.assertIn(slot_name, result)
|
||||
# Check that the output contains the original HTML
|
||||
self.assertIn(test_html, result)
|
||||
# Check that the slot colors are used
|
||||
self.assertIn(COLORS["slot"].text_color, result)
|
||||
self.assertIn(COLORS["slot"].border_color, result)
|
Loading…
Add table
Add a link
Reference in a new issue