refactor: add missing exports, better error handling, and handle boolean query params (#1422)
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:
Juro Oravec 2025-09-30 21:49:28 +02:00 committed by GitHub
parent e9d1b6c4b2
commit 8957befd1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 552 additions and 371 deletions

View file

@ -34,7 +34,6 @@ from django_components.component_registry import (
registry,
all_registries,
)
from django_components.components import DynamicComponent
from django_components.dependencies import DependenciesStrategy, render_dependencies
from django_components.extension import (
ComponentExtension,
@ -47,6 +46,10 @@ from django_components.extension import (
OnComponentClassDeletedContext,
OnComponentInputContext,
OnComponentDataContext,
OnComponentRenderedContext,
OnSlotRenderedContext,
OnTemplateCompiledContext,
OnTemplateLoadedContext,
)
from django_components.extensions.cache import ComponentCache
from django_components.extensions.defaults import ComponentDefaults, Default
@ -81,6 +84,9 @@ from django_components.util.loader import ComponentFileEntry, get_component_dirs
from django_components.util.routing import URLRoute, URLRouteHandler
from django_components.util.types import Empty
# NOTE: Import built-in components last to avoid circular imports
from django_components.components import DynamicComponent
# isort: on
@ -122,10 +128,14 @@ __all__ = [
"OnComponentDataContext",
"OnComponentInputContext",
"OnComponentRegisteredContext",
"OnComponentRenderedContext",
"OnComponentUnregisteredContext",
"OnRegistryCreatedContext",
"OnRegistryDeletedContext",
"OnRenderGenerator",
"OnSlotRenderedContext",
"OnTemplateCompiledContext",
"OnTemplateLoadedContext",
"ProvideNode",
"RegistrySettings",
"ShorthandComponentFormatter",

View file

@ -48,6 +48,12 @@ def get_component_url(
`get_component_url()` optionally accepts `query` and `fragment` arguments.
**Query parameter handling:**
- `True` values are rendered as flag parameters without values (e.g., `?enabled`)
- `False` and `None` values are omitted from the URL
- Other values are rendered normally (e.g., `?foo=bar`)
**Example:**
```py
@ -60,10 +66,10 @@ def get_component_url(
# Get the URL for the component
url = get_component_url(
MyComponent,
query={"foo": "bar"},
query={"foo": "bar", "enabled": True, "debug": False, "unused": None},
fragment="baz",
)
# /components/ext/view/components/c1ab2c3?foo=bar#baz
# /components/ext/view/components/c1ab2c3?foo=bar&enabled#baz
```
"""
view_cls: Optional[Type[ComponentView]] = getattr(component, "View", None)

View file

@ -20,20 +20,27 @@ def component_error_message(component_path: List[str]) -> Generator[None, None,
components = getattr(err, "_components", [])
components = err._components = [*component_path, *components] # type: ignore[attr-defined]
# Access the exception's message, see https://stackoverflow.com/a/75549200/9788634
if len(err.args) and err.args[0] is not None:
if not components:
orig_msg = str(err.args[0])
else:
orig_msg = str(err.args[0]).split("\n", 1)[-1]
else:
orig_msg = str(err)
# Format component path as
# "MyPage > MyComponent > MyComponent(slot:content) > Base(slot:tab)"
comp_path = " > ".join(components)
prefix = f"An error occured while rendering components {comp_path}:\n"
# Access the exception's message, see https://stackoverflow.com/a/75549200/9788634
if len(err.args) and err.args[0] is not None:
orig_msg = str(err.args[0])
if components and "An error occured while rendering components" in orig_msg:
orig_msg = str(err.args[0]).split("\n", 1)[-1]
else:
# When the exception has no message, it may be that the exception
# does NOT rely on the `args` attribute. Such case is for example
# Pydantic exceptions.
#
# In this case, we still try to use the `args` attribute, but
# it's not guaranteed to work. So we also print out the component
# path ourselves.
print(prefix) # noqa: T201
orig_msg = str(err)
err.args = (prefix + orig_msg,) # tuple of one
# `from None` should still raise the original error, but without showing this

View file

@ -162,12 +162,35 @@ def format_url(url: str, query: Optional[Dict] = None, fragment: Optional[str] =
```
`query` and `fragment` are optional, and not applied if `None`.
Boolean `True` values in query parameters are rendered as flag parameters without values.
`False` and `None` values in query parameters are omitted.
```py
url = format_url(
url="https://example.com",
query={"foo": "bar", "baz": None, "enabled": True, "debug": False},
)
# https://example.com?foo=bar&enabled
```
"""
parts = parse.urlsplit(url)
fragment_enc = parse.quote(fragment or parts.fragment, safe="")
base_qs = dict(parse.parse_qsl(parts.query))
merged = {**base_qs, **(query or {})}
encoded_qs = parse.urlencode(merged, safe="")
# Filter out `None` and `False` values
filtered_query = {k: v for k, v in (query or {}).items() if v is not None and v is not False}
merged = {**base_qs, **filtered_query}
# Handle boolean True values as flag parameters (no explicit value)
query_parts = []
for key, value in merged.items():
if value is True:
query_parts.append(parse.quote_plus(str(key)))
else:
query_parts.append(f"{parse.quote_plus(str(key))}={parse.quote_plus(str(value))}")
encoded_qs = "&".join(query_parts)
return parse.urlunsplit(parts._replace(query=encoded_qs, fragment=fragment_enc))