refactor: make Component.View.public optional (#1451)
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (windows-latest, 3.10) (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.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

Co-authored-by: RohanDisa <105740583+RohanDisa@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2025-10-12 20:33:14 +02:00 committed by GitHub
parent b17214f536
commit 485027b308
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 202 additions and 71 deletions

View file

@ -1,5 +1,45 @@
# Release notes
## v0.142.3
#### Refactor
- `Component.View.public = True` is now optional.
Before, to create component endpoints, you had to set both:
1. HTTP handlers on `Component.View`
2. `Component.View.public = True`.
Now, you can set only the HTTP handlers, and the component will be automatically exposed
when any of the HTTP handlers are defined.
You can still explicitly expose/hide the component with `Component.View.public = True/False`.
Before:
```py
class MyTable(Component):
class View:
public = True
def get(self, request):
return self.render_to_response()
url = get_component_url(MyTable)
```
After:
```py
class MyTable(Component):
class View:
def get(self, request):
return self.render_to_response()
url = get_component_url(MyTable)
```
## v0.142.2
_06 Oct 2025_

View file

@ -347,13 +347,10 @@ class Calendar(Component):
template_file = "calendar.html"
class View:
# Register Component with `urlpatterns`
public = True
# Define handlers
def get(self, request, *args, **kwargs):
page = request.GET.get("page", 1)
return self.component.render_to_response(
return Calendar.render_to_response(
request=request,
kwargs={
"page": page,

View file

@ -13,7 +13,9 @@ 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.2/topics/http/urls/). This works the same way as [`View.as_view()`](https://docs.djangoproject.com/en/5.2/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.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).
- Or use [`get_component_url()`](../../../reference/api#django_components.get_component_url) to retrieve the component URL - an anonymous HTTP endpoint that triggers the component's handlers without having to register the component in `urlpatterns`.
To expose a component, simply define a handler, or explicitly expose/hide the component with [`Component.View.public = True/False`](../../../reference/api#django_components.ComponentView.public).
- 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.
@ -114,19 +116,13 @@ class 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.View.public`](../../../reference/api#django_components.ComponentView.public) attribute to `True`:
If you don't care about the exact URL of the component, you can let django-components manage the URLs.
```py
class MyComponent(Component):
class View:
public = True
Each component has an "anonymous" URL that triggers the component's HTTP handlers without having to define the component in `urlpatterns`.
def get(self, request):
return self.component_cls.render_to_response(request=request)
...
```
This way you don't have to mix your app URLs with component URLs.
Then, to get the URL for the component, use [`get_component_url()`](../../../reference/api#django_components.get_component_url):
To obtain such "anonymous" URL, use [`get_component_url()`](../../../reference/api#django_components.get_component_url):
```py
from django_components import get_component_url
@ -134,7 +130,17 @@ from django_components import get_component_url
url = get_component_url(MyComponent)
```
This way you don't have to mix your app URLs with component URLs.
The component is automatically registered in `urlpatterns` when you define a handler. You can also explicitly expose/hide the component with [`Component.View.public`](../../../reference/api#django_components.ComponentView.public):
```py
class MyComponent(Component):
class View:
public = False
def get(self, request):
return self.component_cls.render_to_response(request=request)
...
```
!!! info

View file

@ -55,8 +55,6 @@ class ContactFormComponent(Component):
""" # noqa: E501
class View:
public = True
# Submit handler
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Access the submitted data

View file

@ -121,8 +121,6 @@ class FragmentsPage(Component):
"""
class View:
public = True
# The same GET endpoint handles rendering either the whole page or a fragment.
# We use the `type` query parameter to determine which one to render.
def get(self, request: HttpRequest) -> HttpResponse:

View file

@ -261,18 +261,22 @@ Next, you need to set the URL for the component.
You can either:
1. Automatically assign the URL by setting the [`Component.View.public`](../../reference/api#django_components.ComponentView.public) attribute to `True`.
1. Use [`get_component_url()`](../../reference/api#django_components.get_component_url) to retrieve the component URL - an anonymous HTTP endpoint that triggers the component's handlers without having to register the component in `urlpatterns`.
In this case, use [`get_component_url()`](../../reference/api#django_components.get_component_url) to get the URL for the component view.
```py
from django_components import get_component_url
url = get_component_url(Calendar)
```
The component endpoint is automatically registered in `urlpatterns` when you define a handler. To explicitly expose/hide the component, use [`Component.View.public`](../../../reference/api#django_components.ComponentView.public).
```djc_py
from django_components import Component, get_component_url
from django_components import Component
class Calendar(Component):
class View:
public = True
url = get_component_url(Calendar)
public = False
```
2. Manually assign the URL by setting [`Component.as_view()`](../../reference/api#django_components.Component.as_view) to your `urlpatterns`:

View file

@ -335,13 +335,10 @@ class Calendar(Component):
template_file = "calendar.html"
class View:
# Register Component with `urlpatterns`
public = True
# Define handlers
def get(self, request, *args, **kwargs):
page = request.GET.get("page", 1)
return self.component.render_to_response(
return Calendar.render_to_response(
request=request,
kwargs={
"page": page,

View file

@ -44,6 +44,11 @@ def get_component_url(
Raises `RuntimeError` if the component is not public.
Component is public when:
- You set any of the HTTP methods in the [`Component.View`](../api#django_components.ComponentView) class,
- Or you explicitly set [`Component.View.public = True`](../api#django_components.ComponentView.public).
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
`get_component_url()` optionally accepts `query` and `fragment` arguments.
@ -59,9 +64,10 @@ def get_component_url(
```py
from django_components import Component, get_component_url
class MyComponent(Component):
class MyTable(Component):
class View:
public = True
def get(self, request: HttpRequest, **kwargs: Any):
return MyTable.render_to_response()
# Get the URL for the component
url = get_component_url(
@ -89,15 +95,18 @@ class ComponentView(ExtensionComponentConfig, View):
This class is a subclass of
[`django.views.View`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#view).
The [`Component`](../api#django_components.Component) class is available
via `self.component_cls`.
Override the methods of this class to define the behavior of the component.
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
The [`Component`](../api#django_components.Component) class is available
via `self.component_cls`.
**Example:**
Define a handler that runs for GET HTTP requests:
```python
class MyComponent(Component):
class View:
@ -107,27 +116,26 @@ class ComponentView(ExtensionComponentConfig, View):
**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.
Use [`get_component_url()`](../api#django_components.get_component_url) to retrieve
the component URL - an anonymous HTTP endpoint that triggers the component's handlers without having to register
the component in `urlpatterns`.
A component is automatically exposed when you define at least one HTTP handler. To explicitly
expose/hide the component, use
[`Component.View.public = True`](../api#django_components.ComponentView.public).
```py
from django_components import Component
from django_components import Component, get_component_url
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()`](../api#django_components.get_component_url):
```py
url = get_component_url(MyComponent)
```
This will create a URL route like `/components/ext/view/components/a1b2c3/`.
"""
# NOTE: The `component` / `component_cls` attributes are NOT user input, but still must be declared
@ -176,15 +184,17 @@ class ComponentView(ExtensionComponentConfig, View):
The URL for the component.
Raises `RuntimeError` if the component is not public.
See [`Component.View.public`](../api#django_components.ComponentView.public).
This is the same as calling [`get_component_url()`](../api#django_components.get_component_url)
with the parent [`Component`](../api#django_components.Component) class:
with the current [`Component`](../api#django_components.Component) class:
```py
class MyComponent(Component):
class View:
def get(self, request):
assert self.url == get_component_url(self.component_cls)
component_url = get_component_url(self.component_cls)
assert self.url == component_url
```
"""
return get_component_url(self.component_cls)
@ -193,26 +203,42 @@ class ComponentView(ExtensionComponentConfig, View):
# PUBLIC API (Configurable by users)
# #####################################
public: ClassVar[bool] = False
public: ClassVar[Optional[bool]] = None
"""
Whether the component should be available via a URL.
Whether the component HTTP handlers should be available via a URL.
By default (`None`), the component HTTP handlers are available via a URL
if any of the HTTP methods are defined.
You can explicitly set `public` to `True` or `False` to override this behaviour.
**Example:**
Define the component HTTP handlers and get its URL using
[`get_component_url()`](../api#django_components.get_component_url):
```py
from django_components import Component
from django_components import Component, get_component_url
class MyComponent(Component):
class View:
public = True
def get(self, request):
return self.component_cls.render_to_response(request=request)
url = get_component_url(MyComponent)
```
Will create a URL route like `/components/ext/view/components/a1b2c3/`.
This will create a URL route like `/components/ext/view/components/a1b2c3/`.
To get the URL for the component, use [`get_component_url()`](../api#django_components.get_component_url):
To explicitly hide the component, set `public = False`:
```py
url = get_component_url(MyComponent)
class MyComponent(Component):
class View:
public = False
def get(self, request):
return self.component_cls.render_to_response(request=request)
```
"""
@ -222,10 +248,13 @@ class ComponentView(ExtensionComponentConfig, View):
# Each method actually delegates to the component's method of the same name.
# E.g. When `get()` is called, it delegates to `component.get()`.
# TODO_V1 - In v1 handlers like `get()` should be defined on the Component.View class,
# not the Component class directly. This is to align Views with the extensions API
# where each extension should keep its methods in the extension class.
# Instead, the defaults for these methods should be something like
# TODO_V1 - For backwards compatibility, the HTTP methods can be defined directly on
# the Component class, e.g. `Component.post()`.
# This should be no longer supported in v1.
# In v1, handlers like `get()` should be defined on the Component.View class.
# This is to align Views with the extensions API, where each extension should
# keep its methods in the extension class.
# And instead, the defaults for these methods should be something like
# `return self.component_cls.render_to_response(request, *args, **kwargs)` or similar
# or raise NotImplementedError.
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
@ -306,4 +335,25 @@ class ViewExtension(ComponentExtension):
def _is_view_public(view_cls: Optional[Type[ComponentView]]) -> bool:
if view_cls is None:
return False
return getattr(view_cls, "public", False)
# Allow users to skip setting `View.public = True` if any of the HTTP methods
# are defined. Users can still opt-out by explicitly setting `View.public` to `True` or `False`.
public = getattr(view_cls, "public", None)
if public is not None:
return public
# Auto-decide whether the view is public by checking if any of the HTTP methods
# are overridden in the user's View class.
# We do this only once, so if user dynamically adds or removes the methods,
# we will not pick up on that.
http_methods = ["get", "post", "put", "patch", "delete", "head", "options", "trace"]
for method in http_methods:
if not hasattr(view_cls, method):
continue
did_change_method = getattr(view_cls, method) != getattr(ComponentView, method)
if did_change_method:
view_cls.public = True
return True
view_cls.public = False
return False

View file

@ -19,7 +19,6 @@ from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import (
Component,
ComponentRegistry,
ComponentView,
Slot,
SlotInput,
all_components,
@ -1240,12 +1239,12 @@ class TestComponentRender:
def get_template_data(self, args, kwargs, slots, context):
return {"how": kwargs.pop("how")}
class View(ComponentView):
class View:
def get(self, request):
how = "via GET request"
return self.component.render_to_response(
context=RequestContext(self.request),
return self.component_cls.render_to_response( # type: ignore[attr-defined]
context=RequestContext(request),
kwargs={"how": how},
)

View file

@ -8,7 +8,7 @@ from django.test import Client
from django.urls import path
from pytest_django.asserts import assertInHTML
from django_components import Component, ComponentView, get_component_url, register, types
from django_components import Component, get_component_url, register, types
from django_components.testing import djc_test
from django_components.urls import urlpatterns as dc_urlpatterns
from django_components.util.misc import format_url
@ -96,9 +96,9 @@ class TestComponentAsView:
def get_template_data(self, args, kwargs, slots, context):
return {"inner_var": kwargs["variable"]}
class View(ComponentView):
class View:
def get(self, request, *args, **kwargs) -> HttpResponse:
return self.component.render_to_response(kwargs={"variable": "GET"})
return self.component_cls.render_to_response(kwargs={"variable": "GET"}) # type: ignore[attr-defined]
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
response = client.get("/test/")
@ -145,10 +145,10 @@ class TestComponentAsView:
def get_template_data(self, args, kwargs, slots, context):
return {"inner_var": kwargs["variable"]}
class View(ComponentView):
class View:
def post(self, request, *args, **kwargs) -> HttpResponse:
variable = request.POST.get("variable")
return self.component.render_to_response(kwargs={"variable": variable})
return self.component_cls.render_to_response(kwargs={"variable": variable}) # type: ignore[attr-defined]
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
response = client.post("/test/", {"variable": "POST"})
@ -366,7 +366,20 @@ class TestComponentAsView:
assert response.content == b"Hello"
assert did_call_get
def test_non_public_url(self):
def test_public_url_implicit(self):
class TestComponent(Component):
template = "Hello"
class View:
def get(self, request: HttpRequest, **kwargs: Any):
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}/"
def test_public_url_disabled(self):
did_call_get = False
class TestComponent(Component):

View file

@ -1,5 +1,5 @@
import gc
from typing import Any, Callable, Dict, List, cast
from typing import Any, Callable, Dict, List, Optional, cast
import pytest
from django.http import HttpRequest, HttpResponse
@ -215,7 +215,7 @@ class OverrideAssetExtension(ComponentExtension):
@djc_test
class TestExtension:
class TestExtensions:
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_extensions_setting(self):
assert len(app_settings.EXTENSIONS) == 6
@ -258,6 +258,35 @@ class TestExtension:
with pytest.raises(ValueError, match="Multiple extensions cannot have the same name 'test_extension'"):
inner()
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_nested_extension_config_inheritance(self):
component: Optional[Component] = None
class TestExtensionParent:
parent_var = "from_parent"
class MyComponent(Component):
template = "hello"
class TestExtension(TestExtensionParent):
nested_var = "from_nested"
def get_template_data(self, args, kwargs, slots, context):
nonlocal component
component = self
# Rendering the component will execute get_template_data
MyComponent.render()
assert component is not None
# Check properties from DummyExtension.ComponentConfig
assert component.test_extension.foo == "1" # type: ignore[attr-defined]
assert component.test_extension.bar == "2" # type: ignore[attr-defined]
# Check properties from nested class
assert component.test_extension.nested_var == "from_nested" # type: ignore[attr-defined]
# Check properties from parent of nested class
assert component.test_extension.parent_var == "from_parent" # type: ignore[attr-defined]
@djc_test
class TestExtensionHooks: