chore: util to manage URLs in the codebase (#1179)

* chore: util to manage URLs in the codebase

* docs: mentiion validate_links and supported_versions in docs

* refactor: fix linter errors
This commit is contained in:
Juro Oravec 2025-05-11 14:59:34 +02:00 committed by GitHub
parent 5f4fbe76e5
commit ccf02fa316
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 678 additions and 309 deletions

8
.gitignore vendored
View file

@ -1,3 +1,6 @@
# Project-specific files
sampleproject/staticfiles/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@ -43,6 +46,7 @@ htmlcov/
nosetests.xml
coverage.xml
*,cover
.pytest_cache/
# Translations
*.mo
@ -76,7 +80,11 @@ poetry.lock
site
.direnv/
.envrc
.mypy_cache/
# JS, NPM Dependency directories
node_modules/
jspm_packages/
# Cursor
.cursorrules

View file

@ -718,7 +718,7 @@ where each class name or style property can be managed separately.
- If using `pytest`, the decorator allows you to parametrize Django or Components settings.
- The decorator also serves as a stand-in for Django's `@override_settings`.
See the API reference for [`@djc_test`](https://django-components.github.io/django-components/0.131/reference/testing_api/#djc_test) for more details.
See the API reference for [`@djc_test`](https://django-components.github.io/django-components/0.131/reference/testing_api/#django_components.testing.djc_test) for more details.
- `ComponentRegistry` now has a `has()` method to check if a component is registered
without raising an error.
@ -920,12 +920,12 @@ If you see any broken links or other issues, please report them in [#922](https:
- Component inheritance:
- When you subclass a component, the JS and CSS defined on parent's `Media` class is now inherited by the child component.
- You can disable or customize Media inheritance by setting `extend` attribute on the `Component.Media` nested class. This work similarly to Django's [`Media.extend`](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend).
- You can disable or customize Media inheritance by setting `extend` attribute on the `Component.Media` nested class. This work similarly to Django's [`Media.extend`](https://docs.djangoproject.com/en/5.2/topics/forms/media/#extend).
- When child component defines either `template` or `template_file`, both of parent's `template` and `template_file` are ignored. The same applies to `js_file` and `css_file`.
- Autodiscovery now ignores files and directories that start with an underscore (`_`), except `__init__.py`
- The [Signals](https://docs.djangoproject.com/en/5.1/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal.
- The [Signals](https://docs.djangoproject.com/en/5.2/topics/signals/) emitted by or during the use of django-components are now documented, together the `template_rendered` signal.
## v0.123
@ -937,7 +937,7 @@ If you see any broken links or other issues, please report them in [#922](https:
#### Feat
- Add support for HTML fragments. HTML fragments can be rendered by passing `type="fragment"` to `Component.render()` or `Component.render_to_response()`. Read more on how to [use HTML fragments with HTMX, AlpineJS, or vanillaJS](https://django-components.github.io/django-components/latest/concepts/advanced/html_tragments).
- Add support for HTML fragments. HTML fragments can be rendered by passing `type="fragment"` to `Component.render()` or `Component.render_to_response()`. Read more on how to [use HTML fragments with HTMX, AlpineJS, or vanillaJS](https://django-components.github.io/django-components/latest/concepts/advanced/html_fragments).
## v0.121
@ -1555,7 +1555,7 @@ importing them.
- `SETTINGS_MODULE` - Define component dirs using `STATICFILES_DIRS`
- Previously, autodiscovery handled relative files in `STATICFILES_DIRS`. To align with Django, `STATICFILES_DIRS` now must be full paths ([Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS)).
- Previously, autodiscovery handled relative files in `STATICFILES_DIRS`. To align with Django, `STATICFILES_DIRS` now must be full paths ([Django docs](https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS)).
## 🚨📢 v0.81

View file

@ -121,7 +121,7 @@ class Calendar(Component):
# Additional JS and CSS
class Media:
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2.1.1/dist/htmx.min.js"]
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"]
css = ["bootstrap/dist/css/bootstrap.min.css"]
# Variables available in the template

View file

@ -26,7 +26,7 @@ django-components uses `asv` for these use cases:
1. When a git tag is created and pushed, we also update the documentation website (see `docs.yml`).
2. Before we publish the docs website, we generate the HTML report for the benchmark results.
3. The generated report is placed in the `docs/benchmarks/` directory, and is thus
published with the rest of the docs website and available under [`/benchmarks/`](https://django-components.github.io/django-components/benchmarks).
published with the rest of the docs website and available under [`/benchmarks/`](https://django-components.github.io/django-components/latest/benchmarks).
- NOTE: The location where the report is placed is defined in `asv.conf.json`.
- Compare performance between commits on pull requests:

View file

@ -53,8 +53,8 @@ This has two modes:
you can access are a union of:
- All the variables that were OUTSIDE the fill tag, including any\
[`{% with %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#with) tags.
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#cycle))
[`{% with %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#with) tags.
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#cycle))
that the `{% fill %}` tag is part of.
- Data returned from [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
of the component that owns the fill tag.
@ -67,7 +67,7 @@ This has two modes:
Inside the [`{% fill %}`](../../../reference/template_tags#fill) tag, you can ONLY access variables from 2 places:
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#cycle))
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#cycle))
that the `{% fill %}` tag is part of.
- [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
of the component which defined the template (AKA the "root" component).
@ -177,5 +177,5 @@ But since `"cheese"` is not defined there, it's empty.
!!! info
Notice that the variables defined with the [`{% with %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#with)
Notice that the variables defined with the [`{% with %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#with)
tag are ignored inside the [`{% fill %}`](../../../reference/template_tags#fill) tag with the `"isolated"` mode.

View file

@ -23,13 +23,13 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|-- mytags.py
```
2. Create custom [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
2. Create custom [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
and [`ComponentRegistry`](django_components.component_registry.ComponentRegistry) instances in `mytags.py`
This will be the entrypoint for using the components inside Django templates.
Remember that Django requires the [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
instance to be accessible under the `register` variable ([See Django docs](https://docs.djangoproject.com/en/dev/howto/custom-template-tags)):
Remember that Django requires the [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
instance to be accessible under the `register` variable ([See Django docs](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags)):
```py
from django.template import Library
@ -148,7 +148,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
Since you, as the library author, are not in control of the file system, it is recommended to load the components manually.
We recommend doing this in the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
We recommend doing this in the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
hook of your `apps.py`:
```py
@ -170,7 +170,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
```
Note that you can also include any other startup logic within
[`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready).
[`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready).
And that's it! The next step is to publish it.
@ -185,7 +185,7 @@ django_components uses the [`build`](https://build.pypa.io/en/stable/) utility t
python -m build --sdist --wheel --outdir dist/ .
```
And to publish to PyPI, you can use [`twine`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
And to publish to PyPI, you can use [`twine`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
([See Python user guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/#uploading-the-distribution-archives))
```bash
@ -219,7 +219,7 @@ After the package has been published, all that remains is to install it in other
]
```
3. Optionally add the template tags to the [`builtins`](https://docs.djangoproject.com/en/5.1/topics/templates/#django.template.backends.django.DjangoTemplates),
3. Optionally add the template tags to the [`builtins`](https://docs.djangoproject.com/en/5.2/topics/templates/#django.template.backends.django.DjangoTemplates),
so you don't have to call `{% load mytags %}` in every template:
```python

View file

@ -624,7 +624,7 @@ The help message prints out all the arguments and options available for the comm
### Testing Commands
Commands can be tested using Django's [`call_command()`](https://docs.djangoproject.com/en/5.1/ref/django-admin/#running-management-commands-from-your-code)
Commands can be tested using Django's [`call_command()`](https://docs.djangoproject.com/en/5.2/ref/django-admin/#running-management-commands-from-your-code)
function, which allows you to simulate running the command in tests.
```python
@ -699,8 +699,8 @@ class MyExtension(ComponentExtension):
The [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) objects
are different from objects created with Django's
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path).
Do NOT use `URLRoute` objects in Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/#example)
[`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path).
Do NOT use `URLRoute` objects in Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/#example)
and vice versa!
django-components uses a custom [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class to define framework-agnostic routing rules.
@ -758,7 +758,7 @@ The [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) c
so that extensions could be used with non-Django frameworks in the future.
However, that means that there may be some extra fields that Django's
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path)
[`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path)
accepts, but which are not defined on the `URLRoute` object.
To address this, the [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) object has

View file

@ -82,7 +82,6 @@ class ChildComponent(Component):
my_data = self.inject("my_data")
print(my_data.hello) # hi
print(my_data.another) # 123
return {}
```
First argument to [`Component.inject()`](../../../reference/api/#django_components.Component.inject) is the _key_ (or _name_) of the provided data. This
@ -97,7 +96,6 @@ class ChildComponent(Component):
def get_template_data(self, args, kwargs, slots, context):
my_data = self.inject("invalid_key", DEFAULT_DATA)
assert my_data == DEFAULT_DATA
return {}
```
!!! note

View file

@ -109,8 +109,8 @@ fragment = MyComponent.render_to_response(deps_strategy="fragment")
The `deps_strategy` parameter is set at the root of a component render tree, which is why it is not available for
the [`{% component %}`](../../../reference/template_tags#component) tag.
When you use Django's [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
or [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render) to render templates,
When you use Django's [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
or [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render) to render templates,
you can't directly set the `deps_strategy` parameter.
In this case, you can set the `deps_strategy` with the `DJC_DEPS_STRATEGY` context variable.
@ -351,8 +351,8 @@ or templates can be rendered:
- [`Component.render()`](../../../reference/api/#django_components.Component.render)
- [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
- [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render)
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
- [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render)
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
This way you don't need to manually handle rendering of JS / CSS.

View file

@ -48,7 +48,7 @@ By default, the Python files found in the
[`COMPONENTS.app_dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.app_dirs)
are auto-imported in order to execute the code that registers the components.
Autodiscovery occurs when Django is loaded, during the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
Autodiscovery occurs when Django is loaded, during the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
hook of the `apps.py` file.
If you are using autodiscovery, keep a few points in mind:

View file

@ -11,7 +11,7 @@ django-components has a suite of features that help you write and manage views a
- For each component, you can define methods for handling HTTP requests (GET, POST, etc.) - `get()`, `post()`, etc.
- 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).
- 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).
@ -54,11 +54,11 @@ class Calendar(Component):
!!! info
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
`get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()`, `trace()`
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest) object as the first argument.
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument.
<!-- TODO_V1 REMOVE -->
@ -108,7 +108,7 @@ class Calendar(Component):
## Register URLs manually
To register the component as a route / endpoint in Django, add an entry to your
[`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/).
[`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/).
In place of the view function, create a view object with [`Component.as_view()`](../../../reference/api#django_components.Component.as_view):
```python title="[project root]/urls.py"
@ -121,7 +121,7 @@ urlpatterns = [
```
[`Component.as_view()`](../../../reference/api#django_components.Component.as_view)
internally calls [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view), passing the component
internally calls [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view), passing the component
instance as one of the arguments.
## Register URLs automatically

View file

@ -56,7 +56,7 @@ class MyComponent(Component):
## Context Processors
Components support Django's [context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
Components support Django's [context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#using-requestcontext).
In regular Django templates, the context processors are applied only when the template is rendered with [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext).

View file

@ -80,8 +80,6 @@ class Table(Component):
def get_template_data(self, args, kwargs, slots, context):
# Access component's ID
assert self.id == "c1A2b3c"
return {}
```
## Component inputs
@ -96,7 +94,7 @@ All the component inputs are captured and available as [`self.input`](../../../r
- `context` - [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) object that should be used to render the component
- And other kwargs passed to [`Component.render()`](../../../reference/api/#django_components.Component.render) like `type` and `render_dependencies`
Thus, use can use [`self.input.args`](../../../reference/api/#django_components.ComponentInput.args)
For example, you can use [`self.input.args`](../../../reference/api/#django_components.ComponentInput.args)
and [`self.input.kwargs`](../../../reference/api/#django_components.ComponentInput.kwargs)
to access the positional and keyword arguments passed to [`Component.render()`](../../../reference/api/#django_components.Component.render).
@ -109,8 +107,6 @@ class Table(Component):
footer_slot = self.input.slots["footer"]
some_var = self.input.context["some_var"]
return {}
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
@ -120,9 +116,9 @@ rendered = TestComponent.render(
## Request object and context processors
If the component was either:
Components have access to the request object and context processors data if the component was:
- Given a [`request`](../../../reference/api/#django_components.Component.render) kwarg
- Given a [`request`](../../../reference/api/#django_components.Component.render) kwarg directly
- Rendered with [`RenderContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
- Nested in another component for which any of these conditions is true
@ -145,8 +141,6 @@ class Table(Component):
assert self.request.GET == {"query": "something"}
assert self.context_processors_data['user'].username == "admin"
return {}
rendered = Table.render(
request=HttpRequest(),
)

View file

@ -147,39 +147,39 @@ If you have embedded the component in a Django template using the
You can simply render the template with the Django's API:
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
```python
from django.shortcuts import render
```python
from django.shortcuts import render
context = {"date": "2024-12-13"}
rendered_template = render(request, "my_template.html", context)
```
context = {"date": "2024-12-13"}
rendered_template = render(request, "my_template.html", context)
```
- [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render)
- [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render)
```python
from django.template import Template
from django.template.loader import get_template
```python
from django.template import Template
from django.template.loader import get_template
# Either from a file
template = get_template("my_template.html")
# Either from a file
template = get_template("my_template.html")
# or inlined
template = Template("""
{% load component_tags %}
<div>
{% component "calendar" date="2024-12-13" / %}
</div>
""")
# or inlined
template = Template("""
{% load component_tags %}
<div>
{% component "calendar" date="2024-12-13" / %}
</div>
""")
rendered_template = template.render()
```
rendered_template = template.render()
```
### Isolating components
By default, components behave similarly to Django's
[`{% include %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#include),
[`{% include %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#include),
and the template inside the component has access to the variables defined in the outer template.
You can selectively isolate a component, using the `only` flag, so that the inner template
@ -244,8 +244,8 @@ Button.render(
- `kwargs` - Keyword arguments to pass to the component (as a dictionary)
- `slots` - Slot content to pass to the component (as a dictionary)
- `context` - Django context for rendering (can be a dictionary or a `Context` object)
- `deps_strategy` - Dependencies rendering strategy (default: `"document"`)
- `request` - HTTP request object, used for context processors (optional)
- `deps_strategy` - [Dependencies rendering strategy](#dependencies-rendering) (default: `"document"`)
- `request` - [HTTP request object](../http_request), used for context processors (optional)
- `escape_slots_content` - Whether to HTML-escape slot content (default: `True`)
All arguments are optional. If not provided, they default to empty values or sensible defaults.
@ -416,7 +416,7 @@ Instead, use `args`, `kwargs`, and `slots` to pass data to the component.
However, you can pass
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
to the `context` argument, so that the component will gain access to the request object and will use
[context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
[context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#using-requestcontext).
Read more on [Working with HTTP requests](../http_request).
```py

View file

@ -28,11 +28,11 @@ class Calendar(Component):
!!! note
django-component's management of files is inspired by [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media/).
django-component's management of files is inspired by [Django's `Media` class](https://docs.djangoproject.com/en/5.2/topics/forms/media/).
To be familiar with how Django handles static files, we recommend reading also:
- [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.0/howto/static-files/)
- [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.2/howto/static-files/)
## `Media` class
@ -50,14 +50,14 @@ class Calendar(Component):
Use the `Media` class to define secondary JS / CSS files for a component.
This `Media` class behaves similarly to
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition):
[Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition):
- **Static paths** - Paths are handled as static file paths, and are resolved to URLs with Django's
[`{% static %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#static) template tag.
- **URLs** - A path that starts with `http`, `https`, or `/` is considered a URL. URLs are NOT resolved with [`{% static %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#static).
[`{% static %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#static) template tag.
- **URLs** - A path that starts with `http`, `https`, or `/` is considered a URL. URLs are NOT resolved with [`{% static %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#static).
- **HTML tags** - Both static paths and URLs are rendered to `<script>` and `<link>` HTML tags with
`media_class.render_js()` and `media_class.render_css()`.
- **Bypass formatting** - A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
- **Bypass formatting** - A [`SafeString`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.SafeString),
or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
- **Inheritance** - You can set [`extend`](../../../reference/api#django_components.ComponentMediaInput.extend) to configure
@ -68,7 +68,7 @@ However, there's a few differences from Django's Media class:
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list,
or (CSS-only) a dictonary (See [`ComponentMediaInput`](../../../reference/api#django_components.ComponentMediaInput)).
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
[`SafeString`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.SafeString), or a function
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
4. If you set [`Media.extend`](../../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
@ -104,7 +104,7 @@ class MyTable(Component):
You can define which stylesheets will be associated with which
[CSS media types](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries#targeting_media_types). You do so by defining CSS files as a dictionary.
See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.0/topics/forms/media/#css).
See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.2/topics/forms/media/#css).
Again, you can set either a single file or a list of files per media type:
@ -196,7 +196,7 @@ print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
!!! info
The `extend` behaves consistently with
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend),
[Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#extend),
with one exception:
- When you set `extend` to a list, the list is expected to contain Component classes (or other classes that have a nested `Media` class).
@ -207,7 +207,7 @@ To access the files that you defined under [`Component.Media`](../../../referenc
use [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase).
This is consistent behavior with
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition).
[Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition).
```py
class MyComponent(Component):
@ -396,11 +396,11 @@ class SimpleComponent(Component):
### Paths as objects
In the example [above](#supported-types), you can see that when we used Django's
[`mark_safe()`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.mark_safe)
to mark a string as a [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
[`mark_safe()`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe)
to mark a string as a [`SafeString`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.SafeString),
we had to define the URL / path as an HTML `<script>`/`<link>` elements.
This is an extension of Django's [Paths as objects](https://docs.djangoproject.com/en/5.0/topics/forms/media/#paths-as-objects)
This is an extension of Django's [Paths as objects](https://docs.djangoproject.com/en/5.2/topics/forms/media/#paths-as-objects)
feature, where "safe" strings are taken as is, and are accessed only at render time.
Because of that, the paths defined as "safe" strings are NEVER resolved, neither relative to component's directory,
@ -446,7 +446,7 @@ In the [Paths as objects](#paths-as-objects) section, we saw that we can use tha
how the HTML tags are constructed.
However, if you need to change how ALL CSS and JS files are rendered for a given component,
you can provide your own subclass of [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media) to the [`Component.media_class`](../../reference/api.md#django_components.Component.media_class) attribute.
you can provide your own subclass of [Django's `Media` class](https://docs.djangoproject.com/en/5.2/topics/forms/media) to the [`Component.media_class`](../../reference/api.md#django_components.Component.media_class) attribute.
To change how the tags are constructed, you can override the [`Media.render_js()` and `Media.render_css()` methods](https://github.com/django/django/blob/fa7848146738a9fe1d415ee4808664e54739eeb7/django/forms/widgets.py#L102):

View file

@ -226,7 +226,7 @@ automatically embed the associated JS and CSS.
Your components may depend on third-party packages or styling, or other shared logic.
To load these additional dependencies, you can use a nested [`Media` class](../../reference/api#django_components.Component.Media).
This `Media` class behaves similarly to [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition),
This `Media` class behaves similarly to [Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition),
with a few differences:
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list, or (CSS-only) a dictonary (see below).
@ -275,7 +275,7 @@ class Calendar(Component):
!!! info
The `Media` nested class is shaped based on [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/).
The `Media` nested class is shaped based on [Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/).
As such, django-components allows multiple formats to define the nested Media class:

View file

@ -114,7 +114,7 @@ the component inputs, and massage them into a shape that's most appropriate for
what the template needs. And it also allows us to pass in static data into the template.
Imagine our component receives data from the database that looks like below
([taken from Django](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#regroup)).
([taken from Django](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#regroup)).
```py
cities = [

View file

@ -39,7 +39,7 @@ If you have embedded the component in a Django template using the
You can simply render the template with the Django tooling:
#### With [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
#### With [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
```python
from django.shortcuts import render
@ -48,9 +48,9 @@ context = {"date": "2024-12-13"}
rendered_template = render(request, "my_template.html", context)
```
#### With [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render)
#### With [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render)
Either loading the template with [`get_template()`](https://docs.djangoproject.com/en/5.1/topics/templates/#django.template.loader.get_template):
Either loading the template with [`get_template()`](https://docs.djangoproject.com/en/5.2/topics/templates/#django.template.loader.get_template):
```python
from django.template.loader import get_template
@ -60,7 +60,7 @@ context = {"date": "2024-12-13"}
rendered_template = template.render(context)
```
Or creating a new [`Template`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template) instance:
Or creating a new [`Template`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template) instance:
```python
from django.template import Template
@ -113,7 +113,7 @@ rendered_component = calendar.render(
rendered_component = calendar.render(request=request)
```
The `request` object is required for some of the component's features, like using [Django's context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext).
The `request` object is required for some of the component's features, like using [Django's context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext).
### 3. Render the component to HttpResponse
@ -121,7 +121,7 @@ A common pattern in Django is to render the component and then return the result
For this, you can use the [`Component.render_to_response()`](../../reference/api#django_components.Component.render_to_response) convenience method.
`render_to_response()` accepts the same args, kwargs, slots, and more, as [`Component.render()`](../../reference/api#django_components.Component.render), but wraps the result in an [`HttpResponse`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpResponse).
`render_to_response()` accepts the same args, kwargs, slots, and more, as [`Component.render()`](../../reference/api#django_components.Component.render), but wraps the result in an [`HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse).
```python
from components.calendar import Calendar
@ -144,7 +144,7 @@ def my_view(request):
**Response class of `render_to_response`**
While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is [`django.http.HttpResponse`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpResponse).
While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is [`django.http.HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse).
If you want to use a different Response class in `render_to_response`, set the [`Component.response_class`](../../reference/api#django_components.Component.response_class) attribute:
@ -164,7 +164,7 @@ def my_view(request):
Slots content are automatically escaped by default to prevent XSS attacks.
In other words, it's as if you would be using Django's [`mark_safe()`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe) function on the slot content:
In other words, it's as if you would be using Django's [`mark_safe()`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe) function on the slot content:
```python
from django.utils.safestring import mark_safe
@ -196,8 +196,8 @@ methods.
!!! info
If you're planning on passing an HTML string, check Django's use of
[`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html)
and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
[`format_html`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.html.format_html)
and [`mark_safe`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe).
### Component views and URLs
@ -247,11 +247,11 @@ class Calendar(Component):
!!! info
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
`get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()`, `trace()`
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest) object as the first argument.
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument.
Next, you need to set the URL for the component.
@ -284,7 +284,7 @@ You can either:
And with that, you're all set! When you visit the URL, the component will be rendered and the content will be returned.
The `get()`, `post()`, etc methods will receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest) object as the first argument. So you can parametrize how the component is rendered for example by passing extra query parameters to the URL:
The `get()`, `post()`, etc methods will receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument. So you can parametrize how the component is rendered for example by passing extra query parameters to the URL:
```
http://localhost:8000/calendar/?date=2024-12-13

View file

@ -63,10 +63,10 @@ KeyError: "An error occured while rendering components my_page > layout > layout
## Debug and trace logging
Django components supports [logging with Django](https://docs.djangoproject.com/en/5.0/howto/logging/#logging-how-to).
Django components supports [logging with Django](https://docs.djangoproject.com/en/5.2/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)
[`LOGGING`](https://docs.djangoproject.com/en/5.2/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.

View file

@ -30,7 +30,7 @@ COMPONENTS = {
}
```
The value should be the name of one of your configured cache backends from Django's [`CACHES`](https://docs.djangoproject.com/en/stable/ref/settings/#std-setting-CACHES) setting.
The value should be the name of one of your configured cache backends from Django's [`CACHES`](https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-CACHES) setting.
For example, to use Redis for caching component assets:

View file

@ -1,4 +1,4 @@
Django-components supports all supported combinations versions of [Django](https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django) and [Python](https://devguide.python.org/versions/#versions).
Django-components supports all supported combinations versions of [Django](https://docs.djangoproject.com/en/5.2/faq/install/#what-python-version-can-i-use-with-django) and [Python](https://devguide.python.org/versions/#versions).
| Python version | Django version |
| -------------- | -------------- |

View file

@ -156,6 +156,37 @@ twine upload --repository pypi dist/* -u __token__ -p <PyPI_TOKEN>
[See the full workflow here.](https://github.com/django-components/django-components/discussions/557#discussioncomment-10179141)
## Maintenance
### Updating supported versions
The `scripts/supported_versions.py` script can be used to update the supported versions.
```sh
python scripts/supported_versions.py
```
This will check the current versions of Django and Python, and will print to the terminal
all the places that need updating and what to set them to.
### Updating link references
The `scripts/validate_links.py` script can be used to update the link references.
```sh
python scripts/validate_links.py
```
When new version of Django is released, you can use the script to update the URLs pointing to the Django documentation.
First, you need to update the `URL_REWRITE_MAP` in the script to point to the new version of Django.
Then, you can run the script to update the URLs in the codebase.
```sh
python scripts/validate_links.py --rewrite
```
## Development guides
Head over to [Dev guides](../guides/devguides/dependency_mgmt.md) for a deep dive into how django_components' features are implemented.

View file

@ -29,7 +29,7 @@
- _Remove `'APP_DIRS': True,`_
- NOTE: Instead of `APP_DIRS: True`, we will use
[`django.template.loaders.app_directories.Loader`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.loaders.app_directories.Loader),
[`django.template.loaders.app_directories.Loader`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.loaders.app_directories.Loader),
which has the same effect.
- Add `loaders` to `OPTIONS` list and set it to following value:
@ -176,7 +176,7 @@ COMPONENTS = ComponentsSettings(
The input to [`COMPONENTS.dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
is the same as for `STATICFILES_DIRS`, and the paths must be full paths.
[See Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-dirs).
[See Django docs](https://docs.djangoproject.com/en/5.2/ref/settings/#staticfiles-dirs).
---

View file

@ -50,15 +50,15 @@ If you are on an pre-v0.27 version of django-components, your alternatives are:
- a) passing `--ignore <pattern>` options to the _collecstatic_ CLI command,
- b) defining a subclass of StaticFilesConfig.
Both routes are described in the official [docs of the _staticfiles_ app](https://docs.djangoproject.com/en/4.2/ref/contrib/staticfiles/#customizing-the-ignored-pattern-list).
Both routes are described in the official [docs of the _staticfiles_ app](https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#customizing-the-ignored-pattern-list).
Note that `safer_staticfiles` excludes the `.py` and `.html` files for [collectstatic command](https://docs.djangoproject.com/en/5.0/ref/contrib/staticfiles/#collectstatic):
Note that `safer_staticfiles` excludes the `.py` and `.html` files for [collectstatic command](https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#collectstatic):
```sh
python manage.py collectstatic
```
but it is ignored on the [development server](https://docs.djangoproject.com/en/5.0/ref/django-admin/#runserver):
but it is ignored on the [development server](https://docs.djangoproject.com/en/5.2/ref/django-admin/#runserver):
```sh
python manage.py runserver

View file

@ -111,7 +111,7 @@ class Calendar(Component):
# Additional JS and CSS
class Media:
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2.1.1/dist/htmx.min.js"]
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"]
css = ["bootstrap/dist/css/bootstrap.min.css"]
# Variables available in the template

View file

@ -787,7 +787,7 @@ def _format_hook_type(type_str: str) -> str:
elif "Component" in type_str:
type_str = f"[{type_str}](../api#django_components.Component)"
elif "Context" in type_str:
type_str = f"[{type_str}](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)"
type_str = f"[{type_str}](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)"
return type_str
@ -879,7 +879,7 @@ def _extract_property_docstrings(cls: Type) -> Dict[str, str]:
# as Python code). Instead, we manually list all signals that are sent by django-components.
def gen_reference_signals():
"""
Generate documentation for all [Django Signals](https://docs.djangoproject.com/en/5.1/ref/signals) that are
Generate documentation for all [Django Signals](https://docs.djangoproject.com/en/5.2/ref/signals) that are
send by or during the use of django-components.
"""
preface = "<!-- Autogenerated by reference.py -->\n\n"

View file

@ -1,4 +1,4 @@
# Commands
These are all the [Django management commands](https://docs.djangoproject.com/en/5.1/ref/django-admin)
These are all the [Django management commands](https://docs.djangoproject.com/en/5.2/ref/django-admin)
that will be added by installing `django_components`:

View file

@ -4,7 +4,7 @@ Below are the signals that are sent by or during the use of django-components.
## template_rendered
Django's [`template_rendered`](https://docs.djangoproject.com/en/5.1/ref/signals/#template-rendered) signal.
Django's [`template_rendered`](https://docs.djangoproject.com/en/5.2/ref/signals/#template-rendered) signal.
This signal is sent when a template is rendered.
Django-components triggers this signal when a component is rendered. If there are nested components,

View file

@ -158,8 +158,8 @@ plugins:
python:
import:
- https://docs.python.org/3.12/objects.inv
- url: https://docs.djangoproject.com/en/5.0/_objects/
base: https://docs.djangoproject.com/en/5.0/
- url: https://docs.djangoproject.com/en/5.2/_objects/
base: https://docs.djangoproject.com/en/5.2/
domains: [std, py]
paths: [src] # search packages in the src folder
options:

View file

@ -10,4 +10,5 @@ asv
virtualenv==20.30
pytest-asyncio
pytest-django
typing-extensions>=4.12.2
typing-extensions>=4.12.2
pathspec

View file

@ -46,6 +46,8 @@ packaging==24.2
# pyproject-api
# pytest
# tox
pathspec==0.12.1
# via -r requirements-ci.in
platformdirs==4.3.6
# via
# tox

View file

@ -21,4 +21,5 @@ asv
# NOTE: pin virtualenv to <20.31 until asv fixes integration
# See https://github.com/airspeed-velocity/asv/issues/1484
virtualenv==20.30
typing-extensions>=4.12.2
typing-extensions>=4.12.2
pathspec

View file

@ -80,7 +80,7 @@ packaging==24.2
# pytest
# tox
pathspec==0.12.1
# via black
# via -r requirements-ci.in
platformdirs==4.3.6
# via
# black

View file

@ -4,7 +4,7 @@ ASGI config for sampleproject project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os

View file

@ -12,7 +12,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
@ -100,7 +100,7 @@ COMPONENTS = ComponentsSettings(
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
"default": {
@ -111,7 +111,7 @@ DATABASES = {
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
@ -130,7 +130,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = "en-us"
@ -142,12 +142,12 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = "static/"
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

View file

@ -4,7 +4,7 @@ WSGI config for sampleproject project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os

406
scripts/validate_links.py Normal file
View file

@ -0,0 +1,406 @@
"""
validate_links.py - URL checker and rewriter for the codebase.
This script scans all files in the repository (respecting .gitignore and IGNORED_PATHS),
finds all URLs, validates them (including checking for HTML fragments), and can optionally
rewrite URLs in-place using a configurable mapping.
Features:
- Finds all URLs in code, markdown, and docstrings.
- Validates URLs by making GET requests (with caching and rate limiting).
- Uses BeautifulSoup to check for HTML fragments (e.g., #section) in the target page.
- Outputs a summary table of all issues (invalid, broken, missing fragment, etc).
- Can output the summary table to a file with `-o`/`--output`.
- Can rewrite URLs in-place using URL_REWRITE_MAP (supports both prefix and regex mapping).
- Supports dry-run mode for rewrites with `--dry-run`.
Usage:
# Validate all links and print summary to stdout
python scripts/validate_links.py
# Output summary table to a file
python scripts/validate_links.py -o link_report.txt
# Rewrite URLs using URL_REWRITE_MAP (in-place)
python scripts/validate_links.py --rewrite
# Show what would be rewritten, but do not write files
python scripts/validate_links.py --rewrite --dry-run
Configuration:
- IGNORED_PATHS: List of files/dirs to skip (in addition to .gitignore)
- URL_REWRITE_MAP: Dict of {prefix or regex: replacement} for rewriting URLs
See the code for more details and examples.
"""
import argparse
import os
import re
import requests
import sys
import time
from collections import defaultdict, deque
from pathlib import Path
from typing import DefaultDict, Deque, Dict, List, Tuple, Union
from urllib.parse import urlparse
from bs4 import BeautifulSoup
import pathspec
from django_components.util.misc import format_as_ascii_table
# This script relies on .gitignore to know which files to search for URLs,
# and which files to ignore.
#
# If there are files / dirs that you need to ignore, but they are not (or cannot be)
# included in .gitignore, you can add them here.
IGNORED_PATHS = [
"package-lock.json",
"package.json",
"yarn.lock",
"mdn_complete_page.html",
"supported_versions.py",
# Ignore auto-generated files
"node_modules",
"node_modules/",
".asv/",
"__snapshots__/",
"docs/benchmarks/",
".git/",
"*.min.js",
"*.min.css",
]
# Domains that are not real and should be ignored.
IGNORE_DOMAINS = [
"127.0.0.1",
"localhost",
"0.0.0.0",
"example.com",
]
# This allows us to rewrite URLs across the codebase.
# - If key is a str, it's a prefix and the value is the new prefix.
# - If key is a re.Pattern, it's a regex and the value is the replacement string.
URL_REWRITE_MAP: Dict[Union[str, re.Pattern], str] = {
# Example with regex and capture groups
# re.compile(r"https://github.com/old-org/([^/]+)/"): r"https://github.com/new-org/\1/",
# Update all Django docs URLs to 5.2
re.compile(r"https://docs.djangoproject.com/en/([^/]+)/"): "https://docs.djangoproject.com/en/5.2/",
}
REQUEST_TIMEOUT = 8 # seconds
REQUEST_DELAY = 0.5 # seconds between requests
# Simple regex for URLs to scan for
URL_REGEX = re.compile(r'https?://[^\s\'"\)\]]+')
# Detailed regex for URLs to validate
# See https://stackoverflow.com/a/7160778/9788634
URL_VALIDATOR_REGEX = re.compile(
r"^(?:http|ftp)s?://" # http:// or https://
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" # domain...
r"localhost|" # localhost...
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
r"(?::\d+)?" # optional port
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
def is_binary_file(filepath: Path) -> bool:
try:
with open(filepath, "rb") as f:
chunk = f.read(1024)
if b"\0" in chunk:
return True
except Exception:
return True
return False
def load_gitignore(root: Path) -> pathspec.PathSpec:
gitignore = root / ".gitignore"
patterns = []
if gitignore.exists():
with open(gitignore) as f:
patterns = f.read().splitlines()
# Add additional ignored paths
patterns += IGNORED_PATHS
return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
# Recursively find all files not ignored by .gitignore
def find_files(root: Path, spec: pathspec.PathSpec) -> List[Path]:
files = []
for dirpath, dirnames, filenames in os.walk(root):
# Remove ignored dirs in-place
rel_dir = os.path.relpath(dirpath, root)
if rel_dir == ".":
rel_dir = ""
ignored_dirs = [d for d in dirnames if spec.match_file(os.path.join(rel_dir, d))]
for d in ignored_dirs:
dirnames.remove(d)
for filename in filenames:
rel_file = os.path.join(rel_dir, filename)
if not spec.match_file(rel_file):
files.append(Path(dirpath) / filename)
return files
# Extract URLs from a file
def extract_urls_from_file(filepath: Path) -> List[Tuple[str, int, str, str]]:
urls = []
try:
with open(filepath, encoding="utf-8", errors="replace") as f:
for i, line in enumerate(f, 1):
for match in URL_REGEX.finditer(line):
url = match.group(0)
urls.append((str(filepath), i, line.rstrip(), url))
except Exception as e:
print(f"[WARN] Could not read {filepath}: {e}", file=sys.stderr)
return urls
def get_base_url(url: str) -> str:
"""Return the URL without the fragment."""
return url.split("#", 1)[0]
def pick_next_url(domains, domain_to_urls, last_request_time):
"""
Pick the next (domain, url) to fetch, respecting REQUEST_DELAY per domain.
Returns (domain, url) or None if all are on cooldown or empty.
"""
now = time.time()
for domain in domains:
if not domain_to_urls[domain]:
continue
since_last = now - last_request_time[domain]
if since_last >= REQUEST_DELAY:
url = domain_to_urls[domain].popleft()
return domain, url
return None
def validate_urls(all_urls):
"""
For each unique base URL, make a GET request (with caching).
Print progress for each request (including cache hits).
If a URL is invalid, print a warning and skip fetching.
Skip URLs whose netloc matches IGNORE_DOMAINS.
Use round-robin scheduling per domain, with cooldown.
"""
url_cache: Dict[str, Union[requests.Response, Exception, str]] = {}
unique_base_urls = sorted(set(get_base_url(url) for _, _, _, url in all_urls))
# NOTE: Originally we fetched the URLs one after another. But the issue with this was that
# there is a few large domains like Github, MDN, Djagno docs, etc. And there's a lot of URLs
# point to them. So we ended up with a lot of 429 errors.
#
# The current approach is to group the URLs by domain, and then fetch them in parallel,
# preferentially fetching from domains with most URLs (if not on cooldown).
# This way we can spread the load over the domains, and avoid hitting the rate limits.
# Group URLs by domain
domain_to_urls: DefaultDict[str, Deque[str]] = defaultdict(deque)
for url in unique_base_urls:
parsed = urlparse(url)
if parsed.hostname and any(parsed.hostname == d for d in IGNORE_DOMAINS):
url_cache[url] = "SKIPPED"
continue
domain_to_urls[parsed.netloc].append(url)
# Sort domains by number of URLs (descending)
domains = sorted(domain_to_urls, key=lambda d: -len(domain_to_urls[d]))
last_request_time = {domain: 0.0 for domain in domains}
total_urls = sum(len(q) for q in domain_to_urls.values())
done_count = 0
print(f"\nValidating {total_urls} unique base URLs (round-robin by domain)...")
while any(domain_to_urls.values()):
pick = pick_next_url(domains, domain_to_urls, last_request_time)
if pick is None:
# All domains are on cooldown, sleep until the soonest one is ready
soonest = min(
(last_request_time[d] + REQUEST_DELAY for d in domains if domain_to_urls[d]),
default=time.time() + REQUEST_DELAY,
)
sleep_time = max(soonest - time.time(), 0.05)
time.sleep(sleep_time)
continue
domain, url = pick
# Classify and fetch
if url in url_cache:
print(f"[done {done_count + 1}/{total_urls}] {url} (cache hit)")
done_count += 1
continue
if not URL_VALIDATOR_REGEX.match(url):
url_cache[url] = "INVALID_URL"
print(f"[done {done_count + 1}/{total_urls}] {url} WARNING: Invalid URL format, not fetched.")
done_count += 1
continue
print(f"[done {done_count + 1}/{total_urls}] {url} ...", end=" ")
try:
resp = requests.get(
url, timeout=REQUEST_TIMEOUT, headers={"User-Agent": "django-components-link-checker/0.1"}
)
url_cache[url] = resp
print(f"{resp.status_code}")
except Exception as err:
url_cache[url] = err
print(f"ERROR: {err}")
last_request_time[domain] = time.time()
done_count += 1
return url_cache
def check_fragment_in_html(html: str, fragment: str) -> bool:
"""Return True if id=fragment exists in the HTML."""
print(f"Checking fragment {fragment} in HTML...")
soup = BeautifulSoup(html, "html.parser")
return bool(soup.find(id=fragment))
def rewrite_url(url: str) -> Union[Tuple[None, None], Tuple[str, Union[str, re.Pattern]]]:
"""Return (new_url, mapping_key) if a mapping applies, else (None, None)."""
for key, repl in URL_REWRITE_MAP.items():
if isinstance(key, str):
if url.startswith(key):
return url.replace(key, repl, 1), key
elif isinstance(key, re.Pattern):
if key.search(url):
return key.sub(repl, url), key
else:
raise ValueError(f"Invalid key type: {type(key)}")
return None, None
def output_summary(errors: List[Tuple[str, int, str, str, str]], output: str):
# Format the errors into a table
headers = ["Type", "Details", "File", "URL"]
data = [
{"File": file + "#" + str(lineno), "Type": errtype, "URL": url, "Details": details}
for file, lineno, errtype, url, details in errors
]
table = format_as_ascii_table(data, headers, include_headers=True)
# Output summary to file if specified
if output:
output_path = Path(output)
output_path.write_text(table + "\n", encoding="utf-8")
else:
print(table + "\n")
# TODO: Run this as a test in CI?
# NOTE: At v0.140 there was ~800 URL instances total, ~300 unique URLs, and the script took 4 min.
def main():
parser = argparse.ArgumentParser(description="Validate links and fragments in the codebase.")
parser.add_argument(
"-o", "--output", type=str, help="Output summary table to file (suppress stdout except errors)"
)
parser.add_argument("--rewrite", action="store_true", help="Rewrite URLs using URL_REWRITE_MAP and update files")
parser.add_argument(
"--dry-run", action="store_true", help="Show what would be changed by --rewrite, but do not write files"
)
args = parser.parse_args()
root = Path(os.getcwd())
spec = load_gitignore(root)
files = find_files(root, spec)
print(f"Scanning {len(files)} files...")
all_urls: List[Tuple[str, int, str, str]] = []
for f in files:
if is_binary_file(f):
continue
all_urls.extend(extract_urls_from_file(f))
# HTTP request and caching step
url_cache = validate_urls(all_urls)
# --- URL rewriting logic ---
if args.rewrite:
# Group by file for efficient rewriting
file_to_lines: Dict[str, List[str]] = {}
for f in files:
try:
with open(f, encoding="utf-8", errors="replace") as fh:
file_to_lines[str(f)] = fh.readlines()
except Exception:
continue
rewrites = []
for file, lineno, line, url in all_urls:
new_url, mapping_key = rewrite_url(url)
if not new_url or new_url == url:
continue
# Rewrite in memory, so we can have dry-run mode
lines = file_to_lines[file]
idx = lineno - 1
old_line = lines[idx]
new_line = old_line.replace(url, new_url)
if old_line != new_line:
lines[idx] = new_line
rewrites.append((file, lineno, url, new_url, mapping_key))
# Write back or dry-run
if args.dry_run:
for file, lineno, old, new, _ in rewrites:
print(f"[DRY-RUN] {file}#{lineno}: {old} -> {new}")
else:
for file, _, _, _, _ in rewrites:
# Write only once per file
lines = file_to_lines[file]
Path(file).write_text("".join(lines), encoding="utf-8")
for file, lineno, old, new, _ in rewrites:
print(f"[REWRITE] {file}#{lineno}: {old} -> {new}")
return # After rewriting, skip error reporting
# --- Categorize the results / errors ---
errors = []
for file, lineno, line, url in all_urls:
base_url = get_base_url(url)
fragment = url.split("#", 1)[1] if "#" in url else None
cache_val = url_cache.get(base_url)
if cache_val == "SKIPPED":
continue
elif cache_val == "INVALID_URL":
errors.append((file, lineno, "INVALID", url, "Invalid URL format"))
continue
elif isinstance(cache_val, Exception):
errors.append((file, lineno, "ERROR", url, str(cache_val)))
continue
elif hasattr(cache_val, "status_code") and getattr(cache_val, "status_code", 0) != 200:
errors.append((file, lineno, "ERROR_HTTP", url, f"Status {getattr(cache_val, 'status_code', '?')}"))
continue
elif fragment and hasattr(cache_val, "text"):
content_type = cache_val.headers.get("Content-Type", "")
if "html" not in content_type:
errors.append((file, lineno, "ERROR_FRAGMENT", url, "Not HTML content"))
continue
if not check_fragment_in_html(cache_val.text, fragment):
errors.append((file, lineno, "ERROR_FRAGMENT", url, f"Fragment '#{fragment}' not found"))
if not errors:
print("\nAll links and fragments are valid!")
return
# Format the errors into a table
output_summary(errors, args.output)
if __name__ == "__main__":
main()

View file

@ -188,7 +188,7 @@ class ComponentsSettings(NamedTuple):
Defaults to `[Path(settings.BASE_DIR) / "components"]`. That is, the root `components/` app.
Directories must be full paths, same as with
[STATICFILES_DIRS](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-STATICFILES_DIRS).
[STATICFILES_DIRS](https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS).
These locations are searched during [autodiscovery](../../concepts/fundamentals/autodiscovery),
or when you [define HTML, JS, or CSS as separate files](../../concepts/fundamentals/defining_js_css_html_files).
@ -238,10 +238,10 @@ class ComponentsSettings(NamedTuple):
cache: Optional[str] = None
"""
Name of the [Django cache](https://docs.djangoproject.com/en/5.1/topics/cache/)
Name of the [Django cache](https://docs.djangoproject.com/en/5.2/topics/cache/)
to be used for storing component's JS and CSS files.
If `None`, a [`LocMemCache`](https://docs.djangoproject.com/en/5.1/topics/cache/#local-memory-caching)
If `None`, a [`LocMemCache`](https://docs.djangoproject.com/en/5.2/topics/cache/#local-memory-caching)
is used with default settings.
Defaults to `None`.
@ -359,7 +359,7 @@ class ComponentsSettings(NamedTuple):
```
This would be the equivalent of importing these modules from within Django's
[`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready):
[`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready):
```python
class MyAppConfig(AppConfig):
@ -441,12 +441,12 @@ class ComponentsSettings(NamedTuple):
[`COMPONENTS.dirs`](../settings/#django_components.app_settings.ComponentsSettings.dirs)
or
[`COMPONENTS.app_dirs`](../settings/#django_components.app_settings.ComponentsSettings.app_dirs)
are treated as [static files](https://docs.djangoproject.com/en/5.1/howto/static-files/).
are treated as [static files](https://docs.djangoproject.com/en/5.2/howto/static-files/).
If a file is matched against any of the patterns, it's considered a static file. Such files are collected
when running [`collectstatic`](https://docs.djangoproject.com/en/5.1/ref/contrib/staticfiles/#collectstatic),
when running [`collectstatic`](https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#collectstatic),
and can be accessed under the
[static file endpoint](https://docs.djangoproject.com/en/5.1/ref/settings/#static-url).
[static file endpoint](https://docs.djangoproject.com/en/5.2/ref/settings/#static-url).
You can also pass in compiled regexes ([`re.Pattern`](https://docs.python.org/3/library/re.html#re.Pattern))
for more advanced patterns.
@ -486,7 +486,7 @@ class ComponentsSettings(NamedTuple):
[`COMPONENTS.dirs`](../settings/#django_components.app_settings.ComponentsSettings.dirs)
or
[`COMPONENTS.app_dirs`](../settings/#django_components.app_settings.ComponentsSettings.app_dirs)
will NEVER be treated as [static files](https://docs.djangoproject.com/en/5.1/howto/static-files/).
will NEVER be treated as [static files](https://docs.djangoproject.com/en/5.2/howto/static-files/).
If a file is matched against any of the patterns, it will never be considered a static file,
even if the file matches a pattern in
@ -589,7 +589,7 @@ class ComponentsSettings(NamedTuple):
Defaults to `128`.
Each time a [Django template](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template)
Each time a [Django template](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
is rendered, it is cached to a global in-memory cache (using Python's
[`lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache)
decorator). This speeds up the next render of the component.

View file

@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Type
from django_components.component import all_components
from django_components.util.command import CommandArg, ComponentCommand
from django_components.util.misc import get_import_path, get_module_info
from django_components.util.misc import format_as_ascii_table, get_import_path, get_module_info
# This descriptor generates the list of command arguments (e.g. `--all`), such that
@ -87,58 +87,6 @@ class ListCommand(ComponentCommand):
print(table)
def format_as_ascii_table(data: List[Dict[str, Any]], headers: List[str], include_headers: bool = True) -> str:
"""
Format a list of dictionaries as an ASCII table.
Example:
```python
data = [
{"name": "ProjectPage", "full_name": "project.pages.project.ProjectPage", "path": "./project/pages/project"},
{"name": "ProjectDashboard", "full_name": "project.components.dashboard.ProjectDashboard", "path": "./project/components/dashboard"},
{"name": "ProjectDashboardAction", "full_name": "project.components.dashboard_action.ProjectDashboardAction", "path": "./project/components/dashboard_action"},
]
headers = ["name", "full_name", "path"]
print(format_as_ascii_table(data, headers))
```
Which prints:
```txt
name full_name path
==================================================================================================
ProjectPage project.pages.project.ProjectPage ./project/pages/project
ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
```
""" # noqa: E501
# Calculate the width of each column
column_widths = {header: len(header) for header in headers}
for row in data:
for header in headers:
row_value = str(row.get(header, ""))
column_widths[header] = max(column_widths[header], len(row_value))
# Create the header row
header_row = " ".join(f"{header:<{column_widths[header]}}" for header in headers)
separator = "=" * len(header_row)
# Create the data rows
data_rows = []
for row in data:
row_values = [str(row.get(header, "")) for header in headers]
data_row = " ".join(f"{value:<{column_widths[header]}}" for value, header in zip(row_values, headers))
data_rows.append(data_row)
# Combine all parts into the final table
if include_headers:
table = "\n".join([header_row, separator] + data_rows)
else:
table = "\n".join(data_rows)
return table
class ComponentListCommand(ListCommand):
"""
List all components.

View file

@ -522,7 +522,7 @@ class Component(metaclass=ComponentMeta):
Alias for [`template_file`](../api#django_components.Component.template_file).
For historical reasons, django-components used `template_name` to align with Django's
[TemplateView](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.TemplateView).
[TemplateView](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.TemplateView).
`template_file` was introduced to align with `js/js_file` and `css/css_file`.
@ -640,7 +640,7 @@ class Component(metaclass=ComponentMeta):
- `args`: Positional arguments passed to the component.
- `kwargs`: Keyword arguments passed to the component.
- `slots`: Slots passed to the component.
- `context`: [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
- `context`: [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
used for rendering the component template.
**Pass-through kwargs:**
@ -911,7 +911,7 @@ class Component(metaclass=ComponentMeta):
- `args`: Positional arguments passed to the component.
- `kwargs`: Keyword arguments passed to the component.
- `slots`: Slots passed to the component.
- `context`: [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
- `context`: [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
used for rendering the component template.
**Pass-through kwargs:**
@ -1182,7 +1182,7 @@ class Component(metaclass=ComponentMeta):
- `args`: Positional arguments passed to the component.
- `kwargs`: Keyword arguments passed to the component.
- `slots`: Slots passed to the component.
- `context`: [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
- `context`: [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
used for rendering the component template.
**Pass-through kwargs:**
@ -1383,7 +1383,7 @@ class Component(metaclass=ComponentMeta):
media_class: Type[MediaCls] = MediaCls
"""
Set the [Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition)
Set the [Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition)
that will be instantiated with the JS and CSS media files from
[`Component.Media`](../api#django_components.Component.Media).
@ -1409,7 +1409,7 @@ class Component(metaclass=ComponentMeta):
Defines JS and CSS media files associated with this component.
This `Media` class behaves similarly to
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition):
[Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition):
- Paths are generally handled as static file paths, and resolved URLs are rendered to HTML with
`media_class.render_js()` or `media_class.render_css()`.
@ -1667,7 +1667,6 @@ class Component(metaclass=ComponentMeta):
class MyComponent(Component):
def get_template_data(self, args, kwargs, slots, context):
print(f"Rendering '{self.id}'")
return {}
MyComponent.render()
# Rendering 'ab3c4d'
@ -1713,8 +1712,6 @@ class Component(metaclass=ComponentMeta):
footer_slot = self.input.slots["footer"]
some_var = self.input.context["some_var"]
return {}
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
@ -1756,11 +1753,11 @@ class Component(metaclass=ComponentMeta):
@property
def request(self) -> Optional[HttpRequest]:
"""
[HTTPRequest](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest)
[HTTPRequest](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest)
object passed to this component.
In regular Django templates, you have to use
[`RequestContext`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext)
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
to pass the `HttpRequest` object to the template.
But in Components, you can either use `RequestContext`, or pass the `request` object
@ -1794,30 +1791,30 @@ class Component(metaclass=ComponentMeta):
def context_processors_data(self) -> Dict:
"""
Retrieve data injected by
[`context_processors`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#configuring-an-engine).
[`context_processors`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#configuring-an-engine).
This data is also available from within the component's template, without having to
return this data from
[`get_template_data()`](../api#django_components.Component.get_template_data).
In regular Django templates, you need to use
[`RequestContext`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext)
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
to apply context processors.
In Components, the context processors are applied to components either when:
- The component is rendered with
[`RequestContext`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext)
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
(Regular Django behavior)
- The component is rendered with a regular
[`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context) (or none),
[`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) (or none),
but the `request` kwarg of [`Component.render()`](../api#django_components.Component.render) is set.
- The component is nested in another component that matches any of these conditions.
See
[`Component.request`](../api#django_components.Component.request)
on how the `request`
([HTTPRequest](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpRequest))
([HTTPRequest](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest))
object is passed to and within the components.
Raises `RuntimeError` if accessed outside of rendering execution.
@ -2160,7 +2157,7 @@ class Component(metaclass=ComponentMeta):
You can pass
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
to the `context` argument, so that the component will gain access to the request object and will use
[context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
[context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#using-requestcontext).
Read more on [Working with HTTP requests](../../concepts/fundamentals/http_request).
```py
@ -2833,7 +2830,7 @@ class ComponentNode(BaseNode):
### Isolating components
By default, components behave similarly to Django's
[`{% include %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#include),
[`{% include %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#include),
and the template inside the component has access to the variables defined in the outer template.
You can selectively isolate a component, using the `only` flag, so that the inner template

View file

@ -172,14 +172,14 @@ class ComponentRegistry:
When you register a component to a registry, behind the scenes the registry
automatically adds the component's template tag (e.g. `{% component %}` to
the [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#code-layout).
the [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#code-layout).
And the opposite happens when you unregister a component - the tag is removed.
See [Registering components](../../concepts/advanced/component_registry).
Args:
library (Library, optional): Django\
[`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#code-layout)\
[`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#code-layout)\
associated with this registry. If omitted, the default Library instance from django_components is used.
settings (Union[RegistrySettings, Callable[[ComponentRegistry], RegistrySettings]], optional): Configure\
how the components registered with this registry will behave when rendered.\
@ -281,7 +281,7 @@ class ComponentRegistry:
@property
def library(self) -> Library:
"""
The template tag [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#code-layout)
The template tag [`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#code-layout)
that is associated with the registry.
"""
# Lazily use the default library if none was passed

View file

@ -11,9 +11,9 @@ class TagProtectedError(Exception):
The way the [`TagFormatter`](../../concepts/advanced/tag_formatter) works is that,
based on which start and end tags are used for rendering components,
the [`ComponentRegistry`](../api#django_components.ComponentRegistry) behind the scenes
[un-/registers the template tags](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#registering-the-tag)
[un-/registers the template tags](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#registering-the-tag)
with the associated instance of Django's
[`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#code-layout).
[`Library`](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#code-layout).
In other words, if I have registered a component `"table"`, and I use the shorthand
syntax:

View file

@ -340,7 +340,7 @@ class BaseNode(Node, metaclass=NodeMeta):
def parse(cls, parser: Parser, token: Token, **kwargs: Any) -> "BaseNode":
"""
This function is what is passed to Django's `Library.tag()` when
[registering the tag](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#registering-the-tag).
[registering the tag](https://docs.djangoproject.com/en/5.2/howto/custom-template-tags/#registering-the-tag).
In other words, this method is called by Django's template parser when we encounter
a tag that matches this node's tag, e.g. `{% component %}` or `{% slot %}`.
@ -434,7 +434,7 @@ def template_tag(
The function MUST accept at least two positional arguments: `node` and `context`
- `node` is the [`BaseNode`](../api#django_components.BaseNode) instance.
- `context` is the [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
- `context` is the [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
of the template.
Any extra parameters defined on this function will be part of the tag's input parameters.

View file

@ -21,11 +21,11 @@ def cached_template(
Args:
template_string (str): Template as a string, same as the first argument to Django's\
[`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template). Required.
[`Template`](https://docs.djangoproject.com/en/5.2/topics/templates/#template). Required.
template_cls (Type[Template], optional): Specify the Template class that should be instantiated.\
Defaults to Django's [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template) class.
Defaults to Django's [`Template`](https://docs.djangoproject.com/en/5.2/topics/templates/#template) class.
origin (Type[Origin], optional): Sets \
[`Template.Origin`](https://docs.djangoproject.com/en/5.1/howto/custom-template-backend/#origin-api-and-3rd-party-integration).
[`Template.Origin`](https://docs.djangoproject.com/en/5.2/howto/custom-template-backend/#origin-api-and-3rd-party-integration).
name (Type[str], optional): Sets `Template.name`
engine (Type[Any], optional): Sets `Template.engine`

View file

@ -7,7 +7,7 @@ from django_components.provide import ProvideNode
from django_components.slots import FillNode, SlotNode
# NOTE: Variable name `register` is required by Django to recognize this as a template tag library
# See https://docs.djangoproject.com/en/dev/howto/custom-template-tags
# See https://docs.djangoproject.com/en/5.2/howto/custom-template-tags
register = django.template.Library()

View file

@ -243,7 +243,7 @@ class ComponentCommand:
Definition of a CLI command.
This class is based on Python's [`argparse`](https://docs.python.org/3/library/argparse.html)
module and Django's [`BaseCommand`](https://docs.djangoproject.com/en/5.1/howto/custom-management-commands/)
module and Django's [`BaseCommand`](https://docs.djangoproject.com/en/5.2/howto/custom-management-commands/)
class. `ComponentCommand` allows you to define:
- Command name, description, and help text

View file

@ -87,7 +87,7 @@ def get_component_dirs(include_apps: bool = True) -> List[Path]:
# Validate and add other values from the config
for component_dir in component_dirs:
# Consider tuples for STATICFILES_DIRS (See #489)
# See https://docs.djangoproject.com/en/5.0/ref/settings/#prefixes-optional
# See https://docs.djangoproject.com/en/5.2/ref/settings/#prefixes-optional
if isinstance(component_dir, (tuple, list)):
component_dir = component_dir[1]
try:

View file

@ -168,3 +168,55 @@ def format_url(url: str, query: Optional[Dict] = None, fragment: Optional[str] =
encoded_qs = parse.urlencode(merged, safe="")
return parse.urlunsplit(parts._replace(query=encoded_qs, fragment=fragment_enc))
def format_as_ascii_table(data: List[Dict[str, Any]], headers: List[str], include_headers: bool = True) -> str:
"""
Format a list of dictionaries as an ASCII table.
Example:
```python
data = [
{"name": "ProjectPage", "full_name": "project.pages.project.ProjectPage", "path": "./project/pages/project"},
{"name": "ProjectDashboard", "full_name": "project.components.dashboard.ProjectDashboard", "path": "./project/components/dashboard"},
{"name": "ProjectDashboardAction", "full_name": "project.components.dashboard_action.ProjectDashboardAction", "path": "./project/components/dashboard_action"},
]
headers = ["name", "full_name", "path"]
print(format_as_ascii_table(data, headers))
```
Which prints:
```txt
name full_name path
==================================================================================================
ProjectPage project.pages.project.ProjectPage ./project/pages/project
ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
```
""" # noqa: E501
# Calculate the width of each column
column_widths = {header: len(header) for header in headers}
for row in data:
for header in headers:
row_value = str(row.get(header, ""))
column_widths[header] = max(column_widths[header], len(row_value))
# Create the header row
header_row = " ".join(f"{header:<{column_widths[header]}}" for header in headers)
separator = "=" * len(header_row)
# Create the data rows
data_rows = []
for row in data:
row_values = [str(row.get(header, "")) for header in headers]
data_row = " ".join(f"{value:<{column_widths[header]}}" for value, header in zip(row_values, headers))
data_rows.append(data_row)
# Combine all parts into the final table
if include_headers:
table = "\n".join([header_row, separator] + data_rows)
else:
table = "\n".join(data_rows)
return table

View file

@ -25,7 +25,7 @@ class URLRoute:
Framework-agnostic route definition.
This is similar to Django's `URLPattern` object created with
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path).
[`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path).
The `URLRoute` must either define a `handler` function or have a list of child routes `children`.
If both are defined, an error will be raised.

View file

@ -811,7 +811,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
# or here: ^
if is_next_token(["'", '"', "_("]):
# NOTE: Strings may be wrapped in `_()` to allow for translation.
# See https://docs.djangoproject.com/en/5.1/topics/i18n/translation/#string-literals-passed-to-tags-and-filters # noqa: E501
# See https://docs.djangoproject.com/en/5.2/topics/i18n/translation/#string-literals-passed-to-tags-and-filters # noqa: E501
# NOTE 2: We could potentially raise if this token is supposed to be a filter
# name (after `|`) and we got a translation or a quoted string instead. But we
# leave that up for Django.

View file

@ -175,7 +175,7 @@ def djc_test(
**Arguments:**
- `django_settings`: Django settings, a dictionary passed to Django's
[`@override_settings`](https://docs.djangoproject.com/en/5.1/topics/testing/tools/#django.test.override_settings).
[`@override_settings`](https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.override_settings).
The test runs within the context of these overridden settings.
If `django_settings` contains django-components settings (`COMPONENTS` field), these are merged.

View file

@ -62,10 +62,10 @@ class UrlComponent(Component):
class Media:
css = [
"https://cdnjs.cloudflare.com/example/style.min.css",
"http://cdnjs.cloudflare.com/example/style.min.css",
"https://example.com/example/style.min.css",
"http://example.com/example/style.min.css",
# :// is not a valid URL - will be resolved as static path
"://cdnjs.cloudflare.com/example/style.min.css",
"://example.com/example/style.min.css",
"/path/to/style.css",
]
js = [

View file

@ -4,7 +4,7 @@ ASGI config for sampleproject project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os

View file

@ -8,7 +8,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
WSGI_APPLICATION = "testserver.wsgi.application"
@ -77,7 +77,7 @@ TEMPLATES = [
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = "static/"
@ -92,7 +92,7 @@ STATICFILES_FINDERS = [
]
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
"default": {
@ -102,6 +102,6 @@ DATABASES = {
}
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

View file

@ -4,7 +4,7 @@ WSGI config for testserver project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os

View file

@ -1,7 +1,7 @@
{
"__COMMENT__": "This file is used by tests in `test_component_media.py` to test integration with Django's `staticfiles`",
"__COMMENT2__": "Under normal conditions, this JSON would be generated by running Django's `collectstatic` with `ManifestStaticFilesStorage`",
"__COMMENT3__": "See https://docs.djangoproject.com/en/5.0/ref/contrib/staticfiles/#manifeststaticfilesstorage",
"__COMMENT3__": "See https://docs.djangoproject.com/en/5.2/ref/contrib/staticfiles/#manifeststaticfilesstorage",
"paths": {
"calendar/script.js": "calendar/script.e1815e23e0ec.js",

View file

@ -85,12 +85,6 @@ class TestComponentMediaCache:
<div>Template only component</div>
"""
def get_js_data(self, args, kwargs, slots, context):
return {}
def get_css_data(self, args, kwargs, slots, context):
return {}
@register("test_media_no_vars")
class TestMediaNoVarsComponent(Component):
template = """
@ -100,12 +94,6 @@ class TestComponentMediaCache:
js = "console.log('Hello from JS');"
css = ".novars-component { color: blue; }"
def get_js_data(self, args, kwargs, slots, context):
return {}
def get_css_data(self, args, kwargs, slots, context):
return {}
class TestMediaAndVarsComponent(Component):
template = """
<div>Full component</div>

View file

@ -35,7 +35,6 @@ class TestComponentCache:
def get_template_data(self, args, kwargs, slots, context):
nonlocal did_call_get
did_call_get = True
return {}
# First render
component = TestComponent()
@ -70,7 +69,6 @@ class TestComponentCache:
def get_template_data(self, args, kwargs, slots, context):
nonlocal did_call_get
did_call_get = True
return {}
# First render
component = TestComponent()
@ -199,9 +197,6 @@ class TestComponentCache:
# Custom hash method for args and kwargs
return "custom-args-and-kwargs"
def get_template_data(self, args, kwargs, slots, context):
return {}
component = TestComponent()
component.render(args=(1, 2), kwargs={"key": "value"})

View file

@ -440,10 +440,10 @@ class TestComponentMedia:
from tests.components.glob.glob import UrlComponent
rendered = UrlComponent.render()
assertInHTML('<link href="https://cdnjs.cloudflare.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<link href="http://cdnjs.cloudflare.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<link href="https://example.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<link href="http://example.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
# `://` is escaped because Django's `Media.absolute_path()` doesn't consider `://` a valid URL
assertInHTML('<link href="%3A//cdnjs.cloudflare.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<link href="%3A//example.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<link href="/path/to/style.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered)
@ -460,7 +460,7 @@ class TestMediaPathAsObject:
Test that media work with paths defined as instances of classes that define
the `__html__` method.
See https://docs.djangoproject.com/en/5.0/topics/forms/media/#paths-as-objects
See https://docs.djangoproject.com/en/5.2/topics/forms/media/#paths-as-objects
"""
# NOTE: @html_safe adds __html__ method from __str__
@ -745,7 +745,7 @@ class TestMediaStaticfiles:
# Configure static files. The dummy files are set up in the `./static_root` dir.
# The URL should have path prefix /static/.
# NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic
# See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS
"STATIC_URL": "static/",
"STATIC_ROOT": os.path.join(Path(__file__).resolve().parent, "static_root"),
# `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work.
@ -793,11 +793,11 @@ class TestMediaStaticfiles:
# Configure static files. The dummy files are set up in the `./static_root` dir.
# The URL should have path prefix /static/.
# NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic
# See https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS
"STATIC_URL": "static/",
"STATIC_ROOT": os.path.join(Path(__file__).resolve().parent, "static_root"),
# NOTE: STATICFILES_STORAGE is deprecated since 5.1, use STORAGES instead
# See https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-storage
# See https://docs.djangoproject.com/en/5.2/ref/settings/#storages
"STORAGES": {
# This was NOT changed
"default": {

View file

@ -561,7 +561,6 @@ class TestContextProcessors:
nonlocal inner_request
context_processors_data = self.context_processors_data
inner_request = self.request
return {}
template_str: types.django_html = """
{% load component_tags %}
@ -597,7 +596,6 @@ class TestContextProcessors:
nonlocal parent_request
context_processors_data = self.context_processors_data
parent_request = self.request
return {}
@register("test_child")
class TestChildComponent(Component):
@ -608,7 +606,6 @@ class TestContextProcessors:
nonlocal child_request
context_processors_data_child = self.context_processors_data
child_request = self.request
return {}
template_str: types.django_html = """
{% load component_tags %}
@ -645,7 +642,6 @@ class TestContextProcessors:
nonlocal parent_request
context_processors_data = self.context_processors_data
parent_request = self.request
return {}
@register("test_child")
class TestChildComponent(Component):
@ -656,7 +652,6 @@ class TestContextProcessors:
nonlocal child_request
context_processors_data_child = self.context_processors_data
child_request = self.request
return {}
template_str: types.django_html = """
{% load component_tags %}
@ -690,7 +685,6 @@ class TestContextProcessors:
nonlocal inner_request
context_processors_data = self.context_processors_data
inner_request = self.request
return {}
request = HttpRequest()
request_context = RequestContext(request)
@ -719,7 +713,6 @@ class TestContextProcessors:
nonlocal parent_request
context_processors_data = self.context_processors_data
parent_request = self.request
return {}
@register("test_child")
class TestChildComponent(Component):
@ -730,7 +723,6 @@ class TestContextProcessors:
nonlocal child_request
context_processors_data_child = self.context_processors_data
child_request = self.request
return {}
request = HttpRequest()
request_context = RequestContext(request)
@ -756,7 +748,6 @@ class TestContextProcessors:
nonlocal inner_request
context_processors_data = self.context_processors_data
inner_request = self.request
return {}
request = HttpRequest()
rendered = TestComponent.render(request=request)
@ -784,7 +775,6 @@ class TestContextProcessors:
nonlocal parent_request
context_processors_data = self.context_processors_data
parent_request = self.request
return {}
@register("test_child")
class TestChildComponent(Component):
@ -795,7 +785,6 @@ class TestContextProcessors:
nonlocal child_request
context_processors_data_child = self.context_processors_data
child_request = self.request
return {}
request = HttpRequest()
rendered = TestParentComponent.render(request=request)
@ -821,7 +810,6 @@ class TestContextProcessors:
nonlocal inner_request
context_processors_data = self.context_processors_data
inner_request = self.request
return {}
rendered = TestComponent.render(context=Context())
@ -844,7 +832,6 @@ class TestContextProcessors:
nonlocal inner_request
context_processors_data = self.context_processors_data
inner_request = self.request
return {}
rendered = TestComponent.render()
@ -867,7 +854,6 @@ class TestContextProcessors:
nonlocal inner_request
context_processors_data = self.context_processors_data
inner_request = self.request
return {}
request = HttpRequest()
rendered = TestComponent.render(Context(), request=request)
@ -906,7 +892,6 @@ class TestContextProcessors:
def get_template_data(self, args, kwargs, slots, context):
nonlocal context_processors_data
context_processors_data = self.context_processors_data
return {}
@register("test_child")
class TestChildComponent(Component):
@ -915,7 +900,6 @@ class TestContextProcessors:
def get_template_data(self, args, kwargs, slots, context):
nonlocal context_processors_data_child
context_processors_data_child = self.context_processors_data
return {}
request = HttpRequest()
TestParentComponent.render(request=request)

View file

@ -693,9 +693,6 @@ class TestDependenciesStrategySimple:
console.log("Hello");
"""
def get_template_data(self, args, kwargs, slots, context):
return {}
class Media:
css = ["style.css", "style2.css"]
js = "script2.js"
@ -715,9 +712,6 @@ class TestDependenciesStrategySimple:
console.log("xyz");
"""
def get_template_data(self, args, kwargs, slots, context):
return {}
class Media:
css = "xyz1.css"
js = "xyz1.js"
@ -853,9 +847,6 @@ class TestDependenciesStrategyPrepend:
console.log("Hello");
"""
def get_template_data(self, args, kwargs, slots, context):
return {}
class Media:
css = ["style.css", "style2.css"]
js = "script2.js"
@ -875,9 +866,6 @@ class TestDependenciesStrategyPrepend:
console.log("xyz");
"""
def get_template_data(self, args, kwargs, slots, context):
return {}
class Media:
css = "xyz1.css"
js = "xyz1.js"
@ -1010,9 +998,6 @@ class TestDependenciesStrategyAppend:
console.log("Hello");
"""
def get_template_data(self, args, kwargs, slots, context):
return {}
class Media:
css = ["style.css", "style2.css"]
js = "script2.js"
@ -1032,9 +1017,6 @@ class TestDependenciesStrategyAppend:
console.log("xyz");
"""
def get_template_data(self, args, kwargs, slots, context):
return {}
class Media:
css = "xyz1.css"
js = "xyz1.js"

View file

@ -55,9 +55,6 @@ class SimpleComponentNested(Component):
console.log("Hello");
"""
def get_template_data(self, args, kwargs, slots, context):
return {}
class Media:
css = ["style.css", "style2.css"]
js = "script2.js"
@ -78,9 +75,6 @@ class OtherComponent(Component):
console.log("xyz");
"""
def get_template_data(self, args, kwargs, slots, context):
return {}
class Media:
css = "xyz1.css"
js = "xyz1.js"
@ -91,9 +85,6 @@ class SimpleComponentWithSharedDependency(Component):
Variable: <strong>{{ variable }}</strong>
"""
def get_template_data(self, args, kwargs, slots, context):
return {}
class Media:
css = ["style.css", "style2.css"]
js = ["script.js", "script2.js"]

View file

@ -751,7 +751,6 @@ class TestSpreadOperator:
def get_template_data(self, args, kwargs, slots, context):
nonlocal captured
captured = args, kwargs
return {}
template_str: types.django_html = (
"""
@ -815,7 +814,6 @@ class TestAggregateKwargs:
def get_template_data(self, args, kwargs, slots, context):
nonlocal captured
captured = args, kwargs
return {}
template_str: types.django_html = """
{% load component_tags %}

View file

@ -2709,7 +2709,6 @@ class TestResolver:
def get_template_data(self, args, kwargs, slots, context):
nonlocal captured
captured = kwargs
return {}
template_str: types.django_html = """
{% load component_tags %}
@ -2774,7 +2773,6 @@ class TestResolver:
def get_template_data(self, args, kwargs, slots, context):
nonlocal captured
captured = args, kwargs
return {}
template_str: types.django_html = """
{% load component_tags %}
@ -2795,7 +2793,6 @@ class TestResolver:
def get_template_data(self, args, kwargs, slots, context):
nonlocal captured
captured = args, kwargs
return {}
template_str: types.django_html = """
{% load component_tags %}

View file

@ -213,9 +213,6 @@ class TestProvideTemplateTag:
<div></div>
"""
def get_template_data(self, args, kwargs, slots, context):
return {}
template_str: types.django_html = """
{% load component_tags %}
{% provide "my_provide" key="hi" another=6 %}

View file

@ -2411,7 +2411,6 @@ class TestSlotInput:
def get_template_data(self, args, kwargs, slots, context):
nonlocal seen_slots
seen_slots = slots
return {}
assert seen_slots == {}

View file

@ -1,5 +1,5 @@
# This library strives to support all officially supported combinations of Python and Django:
# https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django
# https://docs.djangoproject.com/en/5.2/faq/install/#what-python-version-can-i-use-with-django
# https://devguide.python.org/versions/#versions
[tox]