feat: allow to highlight slots and components for debugging (#942)

This commit is contained in:
Juro Oravec 2025-02-02 10:14:47 +01:00 committed by GitHub
parent da54d97343
commit de32d449d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 306 additions and 49 deletions

View file

@ -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)

View 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:
![Component highlighting example](../../images/debug-highlight-components.png)
While the slots will be highlighted with a **red** border and label:
![Slot highlighting example](../../images/debug-highlight-slots.png)
!!! 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'>,
}
```

View file

@ -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"],
},
},
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

View file

@ -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
```

View file

@ -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

View file

@ -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>

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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,

View 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

View 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)