Merge branch 'master' into dependabot/pip/packaging-25.0

This commit is contained in:
Juro Oravec 2025-04-21 23:12:57 +02:00 committed by GitHub
commit 2b69980845
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 291 additions and 336 deletions

View file

@ -95,6 +95,35 @@
) -> HttpResponse:
```
- The `Component.Url` class was merged with `Component.View`.
Instead of `Component.Url.public`, use `Component.View.public`.
If you imported `ComponentUrl` from `django_components`, you need to update your import to `ComponentView`.
Before:
```py
class MyComponent(Component):
class Url:
public = True
class View:
def get(self, request):
return self.render_to_response()
```
After:
```py
class MyComponent(Component):
class View:
public = True
def get(self, request):
return self.render_to_response()
```
#### 🚨📢 Deprecation
- `get_context_data()` is now deprecated. Use `get_template_data()` instead.

View file

@ -325,7 +325,7 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
- Components can be [exposed as Django Views](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/) with `get()`, `post()`, `put()`, `patch()`, `delete()` methods
- Automatically create an endpoint for a component with [`Component.Url.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
- Automatically create an endpoint for a component with [`Component.View.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
```py
# components/calendar/calendar.py
@ -333,12 +333,11 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
class Calendar(Component):
template_file = "calendar.html"
# Register Component with `urlpatterns`
class Url:
class View:
# Register Component with `urlpatterns`
public = True
# Define handlers
class View:
# Define handlers
def get(self, request, *args, **kwargs):
page = request.GET.get("page", 1)
return self.component.render_to_response(

View file

@ -13,7 +13,7 @@ django-components has a suite of features that help you write and manage views a
- Use [`Component.as_view()`](../../../reference/api#django_components.Component.as_view) to be able to use your Components with Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/). This works the same way as [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view).
- To avoid having to manually define the endpoints for each component, you can set the component to be "public" with [`Component.Url.public = True`](../../../reference/api#django_components.ComponentUrl.public). This will automatically create a URL for the component. To retrieve the component URL, use [`get_component_url()`](../../../reference/api#django_components.get_component_url).
- To avoid having to manually define the endpoints for each component, you can set the component to be "public" with [`Component.View.public = True`](../../../reference/api#django_components.ComponentView.public). This will automatically create a URL for the component. To retrieve the component URL, use [`get_component_url()`](../../../reference/api#django_components.get_component_url).
- In addition, [`Component`](../../../reference/api#django_components.Component) has a [`render_to_response()`](../../../reference/api#django_components.Component.render_to_response) method that renders the component template based on the provided input and returns an `HttpResponse` object.
@ -126,14 +126,13 @@ instance as one of the arguments.
## Register URLs automatically
If you don't care about the exact URL of the component, you can let django-components manage the URLs for you by setting the [`Component.Url.public`](../../../reference/api#django_components.ComponentUrl.public) attribute to `True`:
If you don't care about the exact URL of the component, you can let django-components manage the URLs for you by setting the [`Component.View.public`](../../../reference/api#django_components.ComponentView.public) attribute to `True`:
```py
class MyComponent(Component):
class Url:
class View:
public = True
class View:
def get(self, request):
return self.component.render_to_response(request=request)
...

View file

@ -47,7 +47,7 @@ Component.render(
context: Mapping | django.template.Context | None = None,
args: List[Any] | None = None,
kwargs: Dict[str, Any] | None = None,
slots: Dict[str, str | SafeString | SlotContent] | None = None,
slots: Dict[str, str | SafeString | SlotInput] | None = None,
escape_slots_content: bool = True
) -> str:
```

View file

@ -257,7 +257,7 @@ Next, you need to set the URL for the component.
You can either:
1. Automatically assign the URL by setting the [`Component.Url.public`](../../reference/api#django_components.ComponentUrl.public) attribute to `True`.
1. Automatically assign the URL by setting the [`Component.View.public`](../../reference/api#django_components.ComponentView.public) attribute to `True`.
In this case, use [`get_component_url()`](../../reference/api#django_components.get_component_url) to get the URL for the component view.
@ -265,7 +265,7 @@ You can either:
from django_components import Component, get_component_url
class Calendar(Component):
class Url:
class View:
public = True
url = get_component_url(Calendar)

View file

@ -315,7 +315,7 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
- Components can be [exposed as Django Views](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/) with `get()`, `post()`, `put()`, `patch()`, `delete()` methods
- Automatically create an endpoint for a component with [`Component.Url.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
- Automatically create an endpoint for a component with [`Component.View.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
```py
# components/calendar/calendar.py
@ -323,12 +323,11 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
class Calendar(Component):
template_file = "calendar.html"
# Register Component with `urlpatterns`
class Url:
class View:
# Register Component with `urlpatterns`
public = True
# Define handlers
class View:
# Define handlers
def get(self, request, *args, **kwargs):
page = request.GET.get("page", 1)
return self.component.render_to_response(

View file

@ -47,10 +47,6 @@
options:
show_if_no_docstring: true
::: django_components.ComponentUrl
options:
show_if_no_docstring: true
::: django_components.ComponentVars
options:
show_if_no_docstring: true

View file

@ -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#L2622" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L2619" target="_blank">See source code</a>

View file

@ -48,8 +48,7 @@ from django_components.extension import (
)
from django_components.extensions.cache import ComponentCache
from django_components.extensions.defaults import ComponentDefaults, Default
from django_components.extensions.view import ComponentView
from django_components.extensions.url import ComponentUrl, get_component_url
from django_components.extensions.view import ComponentView, get_component_url
from django_components.library import TagProtectedError
from django_components.node import BaseNode, template_tag
from django_components.slots import Slot, SlotContent, SlotFunc, SlotInput, SlotRef, SlotResult
@ -96,7 +95,6 @@ __all__ = [
"ComponentRegistry",
"ComponentVars",
"ComponentView",
"ComponentUrl",
"ComponentsSettings",
"component_formatter",
"component_shorthand_formatter",

View file

@ -752,10 +752,9 @@ class InternalSettings:
# Prepend built-in extensions
from django_components.extensions.cache import CacheExtension
from django_components.extensions.defaults import DefaultsExtension
from django_components.extensions.url import UrlExtension
from django_components.extensions.view import ViewExtension
extensions = [CacheExtension, DefaultsExtension, ViewExtension, UrlExtension] + list(extensions)
extensions = [CacheExtension, DefaultsExtension, ViewExtension] + list(extensions)
# Extensions may be passed in either as classes or import strings.
extension_instances: List["ComponentExtension"] = []

View file

@ -61,7 +61,6 @@ from django_components.extension import (
)
from django_components.extensions.cache import ComponentCache
from django_components.extensions.defaults import ComponentDefaults
from django_components.extensions.url import ComponentUrl
from django_components.extensions.view import ComponentView, ViewFn
from django_components.node import BaseNode
from django_components.perfutil.component import ComponentRenderer, component_context_cache, component_post_render
@ -1538,8 +1537,6 @@ class Component(metaclass=ComponentMeta):
"""
Instance of [`ComponentView`](../api#django_components.ComponentView) available at component render time.
"""
Url: Type[ComponentUrl]
url: ComponentUrl
# #####################################
# MISC

View file

@ -1,181 +0,0 @@
import sys
from typing import TYPE_CHECKING, Optional, Type, Union
from weakref import WeakKeyDictionary
import django.urls
from django_components.extension import (
ComponentExtension,
OnComponentClassCreatedContext,
OnComponentClassDeletedContext,
URLRoute,
extensions,
)
if TYPE_CHECKING:
from django_components.component import Component
# NOTE: `WeakKeyDictionary` is NOT a generic pre-3.9
if sys.version_info >= (3, 9):
ComponentRouteCache = WeakKeyDictionary[Type["Component"], URLRoute]
else:
ComponentRouteCache = WeakKeyDictionary
def _get_component_route_name(component: Union[Type["Component"], "Component"]) -> str:
return f"__component_url__{component.class_id}"
def get_component_url(component: Union[Type["Component"], "Component"]) -> str:
"""
Get the URL for a [`Component`](../api#django_components.Component).
Raises `RuntimeError` if the component is not public.
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
**Example:**
```py
from django_components import Component, get_component_url
class MyComponent(Component):
class Url:
public = True
# Get the URL for the component
url = get_component_url(MyComponent)
```
"""
url_cls: Optional[Type[ComponentUrl]] = getattr(component, "Url", None)
if url_cls is None or not url_cls.public:
raise RuntimeError("Component URL is not available - Component is not public")
route_name = _get_component_route_name(component)
return django.urls.reverse(route_name)
class ComponentUrl(ComponentExtension.ExtensionClass): # type: ignore
"""
The interface for `Component.Url`.
This class is used to configure whether the component should be available via a URL.
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
**Example:**
```python
from django_components import Component
class MyComponent(Component):
class Url:
public = True
# Get the URL for the component
url = get_component_url(MyComponent)
```
"""
public: bool = False
"""
Whether this [`Component`](../api#django_components.Component) should be available
via a URL. Defaults to `False`.
If `True`, the Component will have its own unique URL path.
You can use this to write components that will correspond to HTML fragments
for HTMX or similar libraries.
To obtain the component URL, either access the url from
[`Component.Url.url`](../api#django_components.ComponentUrl.url) or
use the [`get_component_url()`](../api#django_components.get_component_url) function.
**Example:**
```py
from django_components import Component, get_component_url
class MyComponent(Component):
class Url:
public = True
# Get the URL for the component
url = get_component_url(MyComponent)
```
"""
@property
def url(self) -> str:
"""
The URL for the component.
Raises `RuntimeError` if the component is not public.
"""
return get_component_url(self.component.__class__)
class UrlExtension(ComponentExtension):
"""
This extension adds a nested `Url` class to each [`Component`](../api#django_components.Component).
This nested `Url` class configures whether the component should be available via a URL.
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
**Example:**
```py
from django_components import Component
class MyComponent(Component):
class Url:
public = True
```
Will create a URL route like `/components/ext/url/components/a1b2c3/`.
To get the URL for the component, use `get_component_url`:
```py
url = get_component_url(MyComponent)
```
This extension is automatically added to all [`Component`](../api#django_components.Component)
classes.
"""
name = "url"
ExtensionClass = ComponentUrl
def __init__(self) -> None:
# Remember which route belongs to which component
self.routes_by_component: ComponentRouteCache = WeakKeyDictionary()
# Create URL route on creation
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
url_cls: Optional[Type[ComponentUrl]] = getattr(ctx.component_cls, "Url", None)
if url_cls is None or not url_cls.public:
return
# Create a URL route like `components/MyTable_a1b2c3/`
# And since this is within the `url` extension, the full URL path will then be:
# `/components/ext/url/components/MyTable_a1b2c3/`
route_path = f"components/{ctx.component_cls.class_id}/"
route_name = _get_component_route_name(ctx.component_cls)
route = URLRoute(
path=route_path,
handler=ctx.component_cls.as_view(),
name=route_name,
)
self.routes_by_component[ctx.component_cls] = route
extensions.add_extension_urls(self.name, [route])
# Remove URL route on deletion
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
route = self.routes_by_component.pop(ctx.component_cls, None)
if route is None:
return
extensions.remove_extension_urls(self.name, [route])

View file

@ -1,18 +1,66 @@
from typing import TYPE_CHECKING, Any, Protocol, cast
import sys
from typing import TYPE_CHECKING, Any, Optional, Protocol, Type, Union, cast
from weakref import WeakKeyDictionary
import django.urls
from django.http import HttpRequest, HttpResponse
from django.views.generic import View
from django_components.extension import ComponentExtension
from django_components.extension import (
ComponentExtension,
OnComponentClassCreatedContext,
OnComponentClassDeletedContext,
URLRoute,
extensions,
)
if TYPE_CHECKING:
from django_components.component import Component
# NOTE: `WeakKeyDictionary` is NOT a generic pre-3.9
if sys.version_info >= (3, 9):
ComponentRouteCache = WeakKeyDictionary[Type["Component"], URLRoute]
else:
ComponentRouteCache = WeakKeyDictionary
class ViewFn(Protocol):
def __call__(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Any: ... # noqa: E704
def _get_component_route_name(component: Union[Type["Component"], "Component"]) -> str:
return f"__component_url__{component.class_id}"
def get_component_url(component: Union[Type["Component"], "Component"]) -> str:
"""
Get the URL for a [`Component`](../api#django_components.Component).
Raises `RuntimeError` if the component is not public.
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
**Example:**
```py
from django_components import Component, get_component_url
class MyComponent(Component):
class View:
public = True
# Get the URL for the component
url = get_component_url(MyComponent)
```
"""
view_cls: Optional[Type[ComponentView]] = getattr(component, "View", None)
if view_cls is None or not view_cls.public:
raise RuntimeError("Component URL is not available - Component is not public")
route_name = _get_component_route_name(component)
return django.urls.reverse(route_name)
class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
"""
The interface for `Component.View`.
@ -36,9 +84,33 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
return HttpResponse("Hello, world!")
```
**Component URL:**
If the `public` attribute is set to `True`, the component will have its own URL
that will point to the Component's View.
```py
from django_components import Component
class MyComponent(Component):
class View:
public = True
def get(self, request, *args, **kwargs):
return HttpResponse("Hello, world!")
```
Will create a URL route like `/components/ext/view/components/a1b2c3/`.
To get the URL for the component, use `get_component_url`:
```py
url = get_component_url(MyComponent)
```
"""
# NOTE: This attribute must be declared on the class for `View.as_view()` to allow
# NOTE: This class attribute must be declared on the class for `View.as_view()` to allow
# us to pass `component` kwarg.
component = cast("Component", None)
"""
@ -54,10 +126,42 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
```
"""
public = False
"""
Whether the component should be available via a URL.
**Example:**
```py
from django_components import Component
class MyComponent(Component):
class View:
public = True
```
Will create a URL route like `/components/ext/view/components/a1b2c3/`.
To get the URL for the component, use `get_component_url`:
```py
url = get_component_url(MyComponent)
```
"""
def __init__(self, component: "Component", **kwargs: Any) -> None:
ComponentExtension.ExtensionClass.__init__(self, component)
View.__init__(self, **kwargs)
@property
def url(self) -> str:
"""
The URL for the component.
Raises `RuntimeError` if the component is not public.
"""
return get_component_url(self.component.__class__)
# NOTE: The methods below are defined to satisfy the `View` class. All supported methods
# are defined in `View.http_method_names`.
#
@ -101,9 +205,44 @@ class ViewExtension(ComponentExtension):
This nested class is a subclass of `django.views.View`, and allows the component
to be used as a view by calling `ComponentView.as_view()`.
This extension also allows the component to be available via a unique URL.
This extension is automatically added to all components.
"""
name = "view"
ExtensionClass = ComponentView
def __init__(self) -> None:
# Remember which route belongs to which component
self.routes_by_component: ComponentRouteCache = WeakKeyDictionary()
# Create URL route on creation
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
comp_cls: Type["Component"] = ctx.component_cls
view_cls: Optional[Type[ComponentView]] = getattr(comp_cls, "View", None)
if view_cls is None or not view_cls.public:
return
# Create a URL route like `components/MyTable_a1b2c3/`
# And since this is within the `view` extension, the full URL path will then be:
# `/components/ext/view/components/MyTable_a1b2c3/`
route_path = f"components/{comp_cls.class_id}/"
route_name = _get_component_route_name(comp_cls)
route = URLRoute(
path=route_path,
handler=comp_cls.as_view(),
name=route_name,
)
self.routes_by_component[comp_cls] = route
extensions.add_extension_urls(self.name, [route])
# Remove URL route on deletion
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
comp_cls: Type["Component"] = ctx.component_cls
route = self.routes_by_component.pop(comp_cls, None)
if route is None:
return
extensions.remove_extension_urls(self.name, [route])

View file

@ -97,7 +97,7 @@ class TestExtensionsListCommand:
call_command("components", "ext", "list")
output = out.getvalue()
assert output.strip() == "name \n========\ncache \ndefaults\nview \nurl"
assert output.strip() == "name \n========\ncache \ndefaults\nview"
@djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
@ -108,7 +108,7 @@ class TestExtensionsListCommand:
call_command("components", "ext", "list")
output = out.getvalue()
assert output.strip() == "name \n========\ncache \ndefaults\nview \nurl \nempty \ndummy"
assert output.strip() == "name \n========\ncache \ndefaults\nview \nempty \ndummy"
@djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
@ -119,7 +119,7 @@ class TestExtensionsListCommand:
call_command("components", "ext", "list", "--all")
output = out.getvalue()
assert output.strip() == "name \n========\ncache \ndefaults\nview \nurl \nempty \ndummy"
assert output.strip() == "name \n========\ncache \ndefaults\nview \nempty \ndummy"
@djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
@ -130,7 +130,7 @@ class TestExtensionsListCommand:
call_command("components", "ext", "list", "--columns", "name")
output = out.getvalue()
assert output.strip() == "name \n========\ncache \ndefaults\nview \nurl \nempty \ndummy"
assert output.strip() == "name \n========\ncache \ndefaults\nview \nempty \ndummy"
@djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
@ -141,7 +141,7 @@ class TestExtensionsListCommand:
call_command("components", "ext", "list", "--simple")
output = out.getvalue()
assert output.strip() == "cache \ndefaults\nview \nurl \nempty \ndummy"
assert output.strip() == "cache \ndefaults\nview \nempty \ndummy"
@djc_test
@ -159,7 +159,7 @@ class TestExtensionsRunCommand:
output
== dedent(
f"""
usage: components ext run [-h] {{cache,defaults,view,url,empty,dummy}} ...
usage: components ext run [-h] {{cache,defaults,view,empty,dummy}} ...
Run a command added by an extension.
@ -167,11 +167,10 @@ class TestExtensionsRunCommand:
-h, --help show this help message and exit
subcommands:
{{cache,defaults,view,url,empty,dummy}}
{{cache,defaults,view,empty,dummy}}
cache Run commands added by the 'cache' extension.
defaults Run commands added by the 'defaults' extension.
view Run commands added by the 'view' extension.
url Run commands added by the 'url' extension.
empty Run commands added by the 'empty' extension.
dummy Run commands added by the 'dummy' extension.
"""

View file

@ -1,104 +0,0 @@
from typing import Any
import pytest
from django.http import HttpRequest
from django.test import Client
from django_components import Component, get_component_url
from django_components.testing import djc_test
from .testutils import setup_test_config
# DO NOT REMOVE!
#
# This is intentionally defined before `setup_test_config()` in order to test that
# the URL extension works even before the Django has been set up.
#
# Because if we define the component before `django.setup()`, then we store it in
# event queue, and will register it when `AppConfig.ready()` is finally called.
#
# This test relies on the "url" extension calling `add_extension_urls()` from within
# the `on_component_class_created()` hook.
class ComponentBeforeReady(Component):
class Url:
public = True
template = "Hello"
setup_test_config({"autodiscover": False})
@djc_test
class TestComponentUrl:
def test_public_url(self):
did_call_get = False
did_call_post = False
class TestComponent(Component):
template = "Hello"
class Url:
public = True
class View:
def get(self, request: HttpRequest, **kwargs: Any):
nonlocal did_call_get
did_call_get = True
component: Component = self.component # type: ignore[attr-defined]
return component.render_to_response()
def post(self, request: HttpRequest, **kwargs: Any):
nonlocal did_call_post
did_call_post = True
component: Component = self.component # type: ignore[attr-defined]
return component.render_to_response()
# Check if the URL is correctly generated
component_url = get_component_url(TestComponent)
assert component_url == f"/components/ext/url/components/{TestComponent.class_id}/"
client = Client()
response = client.get(component_url)
assert response.status_code == 200
assert response.content == b"Hello"
assert did_call_get
response = client.post(component_url)
assert response.status_code == 200
assert response.content == b"Hello"
assert did_call_get
def test_non_public_url(self):
did_call_get = False
class TestComponent(Component):
template = "Hi"
class Url:
public = False
class View:
def get(self, request: HttpRequest, **attrs: Any):
nonlocal did_call_get
did_call_get = True
component: Component = self.component # type: ignore[attr-defined]
return component.render_to_response()
# Attempt to get the URL should raise RuntimeError
with pytest.raises(
RuntimeError,
match="Component URL is not available - Component is not public",
):
get_component_url(TestComponent)
# Even calling the URL directly should raise an error
component_url = f"/components/ext/url/components/{TestComponent.class_id}/"
client = Client()
response = client.get(component_url)
assert response.status_code == 404
assert not did_call_get

View file

@ -1,17 +1,35 @@
from typing import Any, Dict
import pytest
from django.conf import settings
from django.http import HttpResponse
from django.http import HttpRequest, HttpResponse
from django.template import Context, Template
from django.test import Client, SimpleTestCase
from django.urls import path
from django_components import Component, ComponentView, register, types
from django_components import Component, ComponentView, get_component_url, register, types
from django_components.urls import urlpatterns as dc_urlpatterns
from django_components.testing import djc_test
from .testutils import setup_test_config
# DO NOT REMOVE!
#
# This is intentionally defined before `setup_test_config()` in order to test that
# the URL extension works even before the Django has been set up.
#
# Because if we define the component before `django.setup()`, then we store it in
# event queue, and will register it when `AppConfig.ready()` is finally called.
#
# This test relies on the "url" extension calling `add_extension_urls()` from within
# the `on_component_class_created()` hook.
class ComponentBeforeReady(Component):
class View:
public = True
template = "Hello"
setup_test_config({"autodiscover": False})
@ -278,3 +296,73 @@ class TestComponentAsView(SimpleTestCase):
b"<script>",
response.content,
)
def test_public_url(self):
did_call_get = False
did_call_post = False
class TestComponent(Component):
template = "Hello"
class View:
public = True
def get(self, request: HttpRequest, **kwargs: Any):
nonlocal did_call_get
did_call_get = True
component: Component = self.component # type: ignore[attr-defined]
return component.render_to_response()
def post(self, request: HttpRequest, **kwargs: Any):
nonlocal did_call_post
did_call_post = True
component: Component = self.component # type: ignore[attr-defined]
return component.render_to_response()
# Check if the URL is correctly generated
component_url = get_component_url(TestComponent)
assert component_url == f"/components/ext/view/components/{TestComponent.class_id}/"
client = Client()
response = client.get(component_url)
assert response.status_code == 200
assert response.content == b"Hello"
assert did_call_get
response = client.post(component_url)
assert response.status_code == 200
assert response.content == b"Hello"
assert did_call_get
def test_non_public_url(self):
did_call_get = False
class TestComponent(Component):
template = "Hi"
class View:
public = False
def get(self, request: HttpRequest, **attrs: Any):
nonlocal did_call_get
did_call_get = True
component: Component = self.component # type: ignore[attr-defined]
return component.render_to_response()
# Attempt to get the URL should raise RuntimeError
with pytest.raises(
RuntimeError,
match="Component URL is not available - Component is not public",
):
get_component_url(TestComponent)
# Even calling the URL directly should raise an error
component_url = f"/components/ext/view/components/{TestComponent.class_id}/"
client = Client()
response = client.get(component_url)
assert response.status_code == 404
assert not did_call_get

View file

@ -24,7 +24,6 @@ from django_components.extension import (
from django_components.extensions.cache import CacheExtension
from django_components.extensions.defaults import DefaultsExtension
from django_components.extensions.view import ViewExtension
from django_components.extensions.url import UrlExtension
from django_components.testing import djc_test
from .testutils import setup_test_config
@ -133,12 +132,11 @@ def with_registry(on_created: Callable):
class TestExtension:
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_extensions_setting(self):
assert len(app_settings.EXTENSIONS) == 5
assert len(app_settings.EXTENSIONS) == 4
assert isinstance(app_settings.EXTENSIONS[0], CacheExtension)
assert isinstance(app_settings.EXTENSIONS[1], DefaultsExtension)
assert isinstance(app_settings.EXTENSIONS[2], ViewExtension)
assert isinstance(app_settings.EXTENSIONS[3], UrlExtension)
assert isinstance(app_settings.EXTENSIONS[4], DummyExtension)
assert isinstance(app_settings.EXTENSIONS[3], DummyExtension)
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_access_component_from_extension(self):
@ -177,7 +175,7 @@ class TestExtension:
class TestExtensionHooks:
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_component_class_lifecycle_hooks(self):
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
assert len(extension.calls["on_component_class_created"]) == 0
assert len(extension.calls["on_component_class_deleted"]) == 0
@ -209,7 +207,7 @@ class TestExtensionHooks:
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_registry_lifecycle_hooks(self):
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
assert len(extension.calls["on_registry_created"]) == 0
assert len(extension.calls["on_registry_deleted"]) == 0
@ -246,7 +244,7 @@ class TestExtensionHooks:
return {"name": name}
registry.register("test_comp", TestComponent)
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
# Verify on_component_registered was called
assert len(extension.calls["on_component_registered"]) == 1
@ -284,7 +282,7 @@ class TestExtensionHooks:
test_slots = {"content": "Some content"}
TestComponent.render(context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots)
extension = cast(DummyExtension, app_settings.EXTENSIONS[4])
extension = cast(DummyExtension, app_settings.EXTENSIONS[3])
# Verify on_component_input was called with correct args
assert len(extension.calls["on_component_input"]) == 1