docs: docstrings, fundamentals, and minor changes (#1145)

* docs: docstrings, fundamentals, and minor changes

* refactor: fix tests + linter errors
This commit is contained in:
Juro Oravec 2025-04-24 12:47:04 +02:00 committed by GitHub
parent 89db10a643
commit 59f82307ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 2239 additions and 673 deletions

View file

@ -11,7 +11,6 @@ from typing import (
Dict,
Generator,
List,
Literal,
Mapping,
NamedTuple,
Optional,
@ -582,6 +581,8 @@ class Component(metaclass=ComponentMeta):
This method has access to the [Render API](../../concepts/fundamentals/render_api).
Read more about [Template variables](../../concepts/fundamentals/html_js_css_variables).
**Example:**
```py
@ -601,6 +602,8 @@ class Component(metaclass=ComponentMeta):
`get_context_data()` and [`get_template_data()`](../api#django_components.Component.get_template_data)
are mutually exclusive.
If both methods return non-empty dictionaries, an error will be raised.
"""
return None
@ -610,6 +613,8 @@ class Component(metaclass=ComponentMeta):
This method has access to the [Render API](../../concepts/fundamentals/render_api).
Read more about [Template variables](../../concepts/fundamentals/html_js_css_variables).
**Example:**
```py
@ -730,6 +735,8 @@ class Component(metaclass=ComponentMeta):
`get_template_data()` and [`get_context_data()`](../api#django_components.Component.get_context_data)
are mutually exclusive.
If both methods return non-empty dictionaries, an error will be raised.
"""
return None
@ -873,6 +880,8 @@ class Component(metaclass=ComponentMeta):
The data returned from this method will be serialized to JSON.
Read more about [JavaScript variables](../../concepts/fundamentals/html_js_css_variables).
**Example:**
```py
@ -1143,6 +1152,8 @@ class Component(metaclass=ComponentMeta):
The data returned from this method will be serialized to string.
Read more about [CSS variables](../../concepts/fundamentals/html_js_css_variables).
**Example:**
```py
@ -1150,7 +1161,6 @@ class Component(metaclass=ComponentMeta):
def get_css_data(self, args, kwargs, slots, context):
return {
"color": kwargs["color"],
"id": self.id,
}
css = '''
@ -1226,7 +1236,6 @@ class Component(metaclass=ComponentMeta):
return {
"color": args.color,
"size": kwargs.size,
"id": self.id,
}
```
@ -1405,7 +1414,7 @@ class Component(metaclass=ComponentMeta):
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
- You can set [`extend`](../api#django_components.ComponentMediaInput.extend) to configure
whether to inherit JS / CSS from parent components. See
[Controlling Media Inheritance](../../concepts/fundamentals/defining_js_css_html_files/#controlling-media-inheritance).
[Media inheritance](../../concepts/fundamentals/secondary_js_css_files/#media-inheritance).
However, there's a few differences from Django's Media class:
@ -1435,7 +1444,31 @@ class Component(metaclass=ComponentMeta):
""" # noqa: E501
response_class = HttpResponse
"""This allows to configure what class is used to generate response from `render_to_response`"""
"""
This attribute configures what class is used to generate response from
[`Component.render_to_response()`](../api/#django_components.Component.render_to_response).
The response class should accept a string as the first argument.
Defaults to
[`django.http.HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#httpresponse-objects).
**Example:**
```py
from django.http import HttpResponse
from django_components import Component
class MyHttpResponse(HttpResponse):
...
class MyComponent(Component):
response_class = MyHttpResponse
response = MyComponent.render_to_response()
assert isinstance(response, MyHttpResponse)
```
"""
# #####################################
# PUBLIC API - HOOKS (Configurable by users)
@ -1580,8 +1613,6 @@ class Component(metaclass=ComponentMeta):
self.outer_context: Optional[Context] = outer_context
self.registry = registry or registry_
self._metadata_stack: Deque[MetadataItem] = deque()
# None == uninitialized, False == No types, Tuple == types
self._types: Optional[Union[Tuple[Any, Any, Any, Any, Any, Any], Literal[False]]] = None
extensions._init_component_instance(self)
@ -1769,13 +1800,18 @@ class Component(metaclass=ComponentMeta):
return this data from
[`get_context_data()`](../api#django_components.Component.get_context_data).
In regular Django templates, you need to use `RequestContext` to apply context processors.
In regular Django templates, you need to use
[`RequestContext`](https://docs.djangoproject.com/en/5.1/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` (Regular Django behavior)
- The component is rendered with a regular `Context` (or none), but the `request` kwarg
of [`Component.render()`](../api#django_components.Component.render) is set.
- The component is rendered with
[`RequestContext`](https://docs.djangoproject.com/en/5.1/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),
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
@ -1786,7 +1822,7 @@ class Component(metaclass=ComponentMeta):
Raises `RuntimeError` if accessed outside of rendering execution.
NOTE: This object is generated dynamically, so changes to it are not persisted.
NOTE: This dictionary is generated dynamically, so any changes to it will not be persisted.
**Example:**
@ -1873,25 +1909,26 @@ class Component(metaclass=ComponentMeta):
def inject(self, key: str, default: Optional[Any] = None) -> Any:
"""
Use this method to retrieve the data that was passed to a `{% provide %}` tag
Use this method to retrieve the data that was passed to a [`{% provide %}`](../template_tags#provide) tag
with the corresponding key.
To retrieve the data, `inject()` must be called inside a component that's
inside the `{% provide %}` tag.
inside the [`{% provide %}`](../template_tags#provide) tag.
You may also pass a default that will be used if the `provide` tag with given
key was NOT found.
You may also pass a default that will be used if the [`{% provide %}`](../template_tags#provide) tag
with given key was NOT found.
This method mut be used inside the `get_context_data()` method and raises
an error if called elsewhere.
This method is part of the [Render API](../../concepts/fundamentals/render_api), and
raises an error if called from outside the rendering execution.
Example:
Read more about [Provide / Inject](../../concepts/advanced/provide_inject).
**Example:**
Given this template:
```django
{% provide "provider" hello="world" %}
{% component "my_comp" %}
{% endcomponent %}
{% provide "my_provide" message="hello" %}
{% component "my_comp" / %}
{% endprovide %}
```
@ -1901,18 +1938,20 @@ class Component(metaclass=ComponentMeta):
@register("my_comp")
class MyComp(Component):
template = "hi {{ data.hello }}!"
def get_context_data(self):
data = self.inject("provider")
return {"data": data}
template = "hi {{ message }}!"
def get_template_data(self, args, kwargs, slots, context):
data = self.inject("my_provide")
message = data.message
return {"message": message}
```
This renders into:
```
hi world!
hi hello!
```
As the `{{ data.hello }}` is taken from the "provider".
As the `{{ message }}` is taken from the "my_provide" provider.
"""
if not len(self._metadata_stack):
raise RuntimeError(
@ -1960,61 +1999,60 @@ class Component(metaclass=ComponentMeta):
slots: Optional[Any] = None,
escape_slots_content: bool = True,
type: RenderType = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
*response_args: Any,
**response_kwargs: Any,
) -> HttpResponse:
"""
Render the component and wrap the content in the response class.
Render the component and wrap the content in an HTTP response class.
The response class is taken from `Component.response_class`. Defaults to `django.http.HttpResponse`.
`render_to_response()` takes the same inputs as
[`Component.render()`](../api/#django_components.Component.render).
See that method for more information.
This is the interface for the `django.views.View` class which allows us to
use components as Django views with `component.as_view()`.
After the component is rendered, the HTTP response class is instantiated with the rendered content.
Inputs:
Any additional kwargs are passed to the response class.
- `args` - Positional args for the component. This is the same as calling the component
as `{% component "my_comp" arg1 arg2 ... %}`
- `kwargs` - Kwargs for the component. This is the same as calling the component
as `{% component "my_comp" key1=val1 key2=val2 ... %}`
- `slots` - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
or render function.
- `escape_slots_content` - Whether the content from `slots` should be escaped.
- `context` - A context (dictionary or Django's Context) within which the component
is rendered. The keys on the context can be accessed from within the template.
- NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
component's args and kwargs.
- `type` - Configure how to handle JS and CSS dependencies.
- `"document"` (default) - JS dependencies are inserted into `{% component_js_dependencies %}`,
or to the end of the `<body>` tag. CSS dependencies are inserted into
`{% component_css_dependencies %}`, or the end of the `<head>` tag.
- `"fragment"` - `{% component_js_dependencies %}` and `{% component_css_dependencies %}`,
are ignored, and a script that loads the JS and CSS dependencies is appended to the HTML.
- `request` - The request object. This is only required when needing to use RequestContext,
e.g. to enable template `context_processors`.
**Example:**
Any additional args and kwargs are passed to the `response_class`.
Example:
```py
MyComponent.render_to_response(
args=[1, "two", {}],
```python
Button.render_to_response(
args=["John"],
kwargs={
"key": 123,
"surname": "Doe",
"age": 30,
},
slots={
"header": 'STATIC TEXT HERE',
"footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
"footer": "i AM A SLOT",
},
escape_slots_content=False,
# HttpResponse input
# HttpResponse kwargs
status=201,
headers={...},
)
# HttpResponse(content=..., status=201, headers=...)
```
**Custom response class:**
You can set a custom response class on the component via
[`Component.response_class`](../api/#django_components.Component.response_class).
Defaults to
[`django.http.HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#httpresponse-objects).
```python
from django.http import HttpResponse
from django_components import Component
class MyHttpResponse(HttpResponse):
...
class MyComponent(Component):
response_class = MyHttpResponse
response = MyComponent.render_to_response()
assert isinstance(response, MyHttpResponse)
```
"""
content = cls.render(
args=args,
@ -2023,10 +2061,10 @@ class Component(metaclass=ComponentMeta):
slots=slots,
escape_slots_content=escape_slots_content,
type=type,
render_dependencies=True,
render_dependencies=render_dependencies,
request=request,
)
return cls.response_class(content, *response_args, **response_kwargs)
return cls.response_class(content, **response_kwargs)
@classmethod
def render(
@ -2041,46 +2079,212 @@ class Component(metaclass=ComponentMeta):
request: Optional[HttpRequest] = None,
) -> str:
"""
Render the component into a string.
Render the component into a string. This is the equivalent of calling
the [`{% component %}`](../template_tags#component) tag.
Inputs:
- `args` - Positional args for the component. This is the same as calling the component
as `{% component "my_comp" arg1 arg2 ... %}`
- `kwargs` - Kwargs for the component. This is the same as calling the component
as `{% component "my_comp" key1=val1 key2=val2 ... %}`
- `slots` - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
or render function.
- `escape_slots_content` - Whether the content from `slots` should be escaped.
- `context` - A context (dictionary or Django's Context) within which the component
is rendered. The keys on the context can be accessed from within the template.
- NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
component's args and kwargs.
- `type` - Configure how to handle JS and CSS dependencies.
- `"document"` (default) - JS dependencies are inserted into `{% component_js_dependencies %}`,
or to the end of the `<body>` tag. CSS dependencies are inserted into
`{% component_css_dependencies %}`, or the end of the `<head>` tag.
- `"fragment"` - `{% component_js_dependencies %}` and `{% component_css_dependencies %}`,
are ignored, and a script that loads the JS and CSS dependencies is appended to the HTML.
- `render_dependencies` - Set this to `False` if you want to insert the resulting HTML into another component.
- `request` - The request object. This is only required when needing to use RequestContext,
e.g. to enable template `context_processors`.
Example:
```py
MyComponent.render(
args=[1, "two", {}],
```python
Button.render(
args=["John"],
kwargs={
"key": 123,
"surname": "Doe",
"age": 30,
},
slots={
"header": 'STATIC TEXT HERE',
"footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
"footer": "i AM A SLOT",
},
escape_slots_content=False,
)
```
"""
**Inputs:**
- `args` - Optional. A list of positional args for the component. This is the same as calling the component
as:
```django
{% component "button" arg1 arg2 ... %}
```
- `kwargs` - Optional. A dictionary of keyword arguments for the component. This is the same as calling
the component as:
```django
{% component "button" key1=val1 key2=val2 ... %}
```
- `slots` - Optional. A dictionary of slot fills. This is the same as passing [`{% fill %}`](../template_tags#fill)
tags to the component.
```django
{% component "button" %}
{% fill "content" %}
Click me!
{% endfill %}
{% endcomponent %}
```
Dictionary keys are the slot names. Dictionary values are the slot fills.
Slot fills can be strings, render functions, or [`Slot`](../api/#django_components.Slot) instances:
```python
Button.render(
slots={
"content": "Click me!"
"content2": lambda *a, **kwa: "Click me!",
"content3": Slot(lambda *a, **kwa: "Click me!"),
},
)
```
- `context` - Optional. Plain dictionary or Django's
[Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context).
The context within which the component is rendered.
When a component is rendered within a template with the [`{% component %}`](../template_tags#component)
tag, this will be set to the
[Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
instance that is used for rendering the template.
When you call `Component.render()` directly from Python, you can ignore this input most of the time.
Instead use `args`, `kwargs`, and `slots` to pass data to the component.
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).
Read more on [Working with HTTP requests](../../concepts/fundamentals/http_request).
```py
Button.render(
context=RequestContext(request),
)
```
For advanced use cases, you can use `context` argument to "pre-render" the component in Python, and then
pass the rendered output as plain string to the template. With this, the inner component is rendered as if
it was within the template with [`{% component %}`](../template_tags#component).
```py
class Button(Component):
def render(self, context, template):
# Pass `context` to Icon component so it is rendered
# as if nested within Button.
icon = Icon.render(
context=context,
args=["icon-name"],
render_dependencies=False,
)
# Update context with icon
with context.update({"icon": icon}):
return template.render(context)
```
Whether the variables defined in `context` are available to the template depends on the
[context behavior mode](../settings#django_components.app_settings.ComponentsSettings.context_behavior):
- In `"django"` context behavior mode, the template will have access to the keys of this context.
- In `"isolated"` context behavior mode, the template will NOT have access to this context,
and data MUST be passed via component's args and kwargs.
- `type` - Optional. Configure how to handle JS and CSS dependencies. Read more about
[Render types](../../concepts/fundamentals/rendering_components#render-types).
Options:
- `"document"` (default) - Use this if you are rendering a whole page, or if no other option suits better.
If it is possible to insert JS and/or CSS into the rendered HTML, then:
- JS and CSS from [`Component.js`](../api/#django_components.Component.js)
and [`Component.css`](../api/#django_components.Component.css) are inlined into the rendered HTML.
- JS and CSS from [`Component.Media`](../api/#django_components.Component.Media) are inserted
into the rendered HTML only as links.
- Extra JS script to manage component dependencies is inserted into the HTML.
- `"fragment"` - Use this if you plan to insert this HTML into a page that was rendered as `"document"`.
- No JS / CSS is inserted. Instead, a JSON `<script>` is inserted. This JSON
tells the dependency manager to load the component's JS and CSS dependencies.
- No extra scripts are inserted.
- `"inline"` - Use this for non-browser use cases like emails, or when you don't want to use
django-component's dependency manager.
This is the same as `"document"`, except no extra scripts are inserted:
- JS and CSS from [`Component.js`](../api/#django_components.Component.js)
and [`Component.css`](../api/#django_components.Component.css) are inlined into the rendered HTML.
- JS and CSS from [`Component.Media`](../api/#django_components.Component.Media) are inserted
into the rendered HTML only as links.
- No extra scripts are inserted.
- `request` - Optional. HTTPRequest object. Pass a request object directly to the component to apply
[context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context.update).
Read more about [Working with HTTP requests](../../concepts/fundamentals/http_request).
- `escape_slots_content` - Optional. Whether the content from `slots` should be escaped with Django's
[`escape`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#std-templatefilter-escape).
Defaults to `True`.
- `render_dependencies` - Optional. Whether the output should be processed to finalize JS and CSS dependencies.
Defaults to `True`.
Set this to `False` if you want to insert the resulting HTML into another component:
```py
html = Button.render(
render_dependencies=False,
)
# Insert the resulting HTML into another component
MyOtherComponent.render(
content=html,
)
```
**Type hints:**
`Component.render()` is NOT typed. To add type hints, you can wrap the inputs
in component's [`Args`](../api/#django_components.Component.Args),
[`Kwargs`](../api/#django_components.Component.Kwargs),
and [`Slots`](../api/#django_components.Component.Slots) classes.
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
```python
from typing import NamedTuple, Optional
from django_components import Component, Slot, SlotInput
# Define the component with the types
class Button(Component):
class Args(NamedTuple):
name: str
class Kwargs(NamedTuple):
surname: str
age: int
class Slots(NamedTuple):
my_slot: Optional[SlotInput] = None
footer: SlotInput
# Add type hints to the render call
Button.render(
args=Button.Args(
name="John",
),
kwargs=Button.Kwargs(
surname="Doe",
age=30,
),
slots=Button.Slots(
footer=Slot(lambda *a, **kwa: "Click me!"),
),
)
```
""" # noqa: 501
# This method may be called as class method or as instance method.
# If called as class method, create a new instance.
if isinstance(cls, Component):
@ -2587,84 +2791,48 @@ class ComponentNode(BaseNode):
[`@register()`](./api.md#django_components.register)
decorator.
**Args:**
The `{% component %}` tag takes:
- `name` (str, required): Registered name of the component to render
- All other args and kwargs are defined based on the component itself.
If you defined a component `"my_table"`
```python
from django_component import Component, register
@register("my_table")
class MyTable(Component):
template = \"\"\"
<table>
<thead>
{% for header in headers %}
<th>{{ header }}</th>
{% endfor %}
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
<tbody>
</table>
\"\"\"
def get_context_data(self, rows: List, headers: List):
return {
"rows": rows,
"headers": headers,
}
```
Then you can render this component by referring to `MyTable` via its
registered name `"my_table"`:
- Component's registered name as the first positional argument,
- Followed by any number of positional and keyword arguments.
```django
{% component "my_table" rows=rows headers=headers ... / %}
{% load component_tags %}
<div>
{% component "button" name="John" job="Developer" / %}
</div>
```
### Component input
The component name must be a string literal.
Positional and keyword arguments can be literals or template variables.
The component name must be a single- or double-quotes string and must
be either:
- The first positional argument after `component`:
```django
{% component "my_table" rows=rows headers=headers ... / %}
```
- Passed as kwarg `name`:
```django
{% component rows=rows headers=headers name="my_table" ... / %}
```
### Inserting into slots
### Inserting slot fills
If the component defined any [slots](../concepts/fundamentals/slots.md), you can
pass in the content to be placed inside those slots by inserting [`{% fill %}`](#fill) tags,
directly within the `{% component %}` tag:
"fill" these slots by placing the [`{% fill %}`](#fill) tags within the `{% component %}` tag:
```django
{% component "my_table" rows=rows headers=headers ... / %}
{% component "my_table" rows=rows headers=headers %}
{% fill "pagination" %}
< 1 | 2 | 3 >
{% endfill %}
{% endcomponent %}
```
You can even nest [`{% fill %}`](#fill) tags within
[`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#if),
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
and other tags:
```django
{% component "my_table" rows=rows headers=headers %}
{% if rows %}
{% fill "pagination" %}
< 1 | 2 | 3 >
{% endfill %}
{% endif %}
{% endcomponent %}
```
### Isolating components
By default, components behave similarly to Django's
@ -2677,6 +2845,36 @@ class ComponentNode(BaseNode):
```django
{% component "name" positional_arg keyword_arg=value ... only %}
```
Alternatively, you can set all components to be isolated by default, by setting
[`context_behavior`](../settings#django_components.app_settings.ComponentsSettings.context_behavior)
to `"isolated"` in your settings:
```python
# settings.py
COMPONENTS = {
"context_behavior": "isolated",
}
```
### Omitting the `component` keyword
If you would like to omit the `component` keyword, and simply refer to your
components by their registered names:
```django
{% button name="John" job="Developer" / %}
```
You can do so by setting the "shorthand" [Tag formatter](../../concepts/advanced/tag_formatters)
in the settings:
```python
# settings.py
COMPONENTS = {
"tag_formatter": "django_components.component_shorthand_formatter",
}
```
"""
tag = "component"