mirror of
https://github.com/django-components/django-components.git
synced 2025-08-09 16:57:59 +00:00
feat: extensions (#1009)
* feat: extensions * refactor: remove support for passing in extensions as instances
This commit is contained in:
parent
cff252c566
commit
4d35bc97a2
24 changed files with 1884 additions and 57 deletions
|
@ -8,5 +8,6 @@ nav:
|
|||
- Typing and validation: typing_and_validation.md
|
||||
- Custom template tags: template_tags.md
|
||||
- Tag formatters: tag_formatter.md
|
||||
- Extensions: extensions.md
|
||||
- Testing: testing.md
|
||||
- Authoring component libraries: authoring_component_libraries.md
|
||||
|
|
330
docs/concepts/advanced/extensions.md
Normal file
330
docs/concepts/advanced/extensions.md
Normal file
|
@ -0,0 +1,330 @@
|
|||
_New in version 0.131_
|
||||
|
||||
Django-components functionality can be extended with "extensions". Extensions allow for powerful customization and integrations. They can:
|
||||
|
||||
- Tap into lifecycle events, such as when a component is created, deleted, registered, or unregistered.
|
||||
- Add new attributes and methods to the components under an extension-specific nested class.
|
||||
|
||||
## Setting up extensions
|
||||
|
||||
Extensions are configured in the Django settings under [`COMPONENTS.extensions`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions).
|
||||
|
||||
Extensions can be set by either as an import string or by passing in a class:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
|
||||
class MyExtension(ComponentsExtension):
|
||||
name = "my_extension"
|
||||
|
||||
class ExtensionClass(BaseExtensionClass):
|
||||
...
|
||||
|
||||
COMPONENTS = ComponentsSettings(
|
||||
extensions=[
|
||||
MyExtension,
|
||||
"another_app.extensions.AnotherExtension",
|
||||
"my_app.extensions.ThirdExtension",
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## Lifecycle hooks
|
||||
|
||||
Extensions can define methods to hook into lifecycle events, such as:
|
||||
|
||||
- Component creation or deletion
|
||||
- Un/registering a component
|
||||
- Creating or deleting a registry
|
||||
- Pre-processing data passed to a component on render
|
||||
- Post-processing data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
|
||||
and others.
|
||||
|
||||
See the full list in [Extension Hooks Reference](../../../reference/extension_hooks).
|
||||
|
||||
## Configuring extensions per component
|
||||
|
||||
Each extension has a corresponding nested class within the [`Component`](../../../reference/api#django_components.Component) class. These allow
|
||||
to configure the extensions on a per-component basis.
|
||||
|
||||
!!! note
|
||||
|
||||
**Accessing the component instance from inside the nested classes:**
|
||||
|
||||
Each method of the nested classes has access to the `component` attribute,
|
||||
which points to the component instance.
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View:
|
||||
def get(self, request):
|
||||
# `self.component` points to the instance of `MyTable` Component.
|
||||
return self.component.get(request)
|
||||
```
|
||||
|
||||
### Example: Component as View
|
||||
|
||||
The [Components as Views](../../fundamentals/components_as_views) feature is actually implemented as an extension
|
||||
that is configured by a `View` nested class.
|
||||
|
||||
You can override the `get`, `post`, etc methods to customize the behavior of the component as a view:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component.get(request)
|
||||
|
||||
def post(self, request):
|
||||
return self.component.post(request)
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### Example: Storybook integration
|
||||
|
||||
The Storybook integration (work in progress) is an extension that is configured by a `Storybook` nested class.
|
||||
|
||||
You can override methods such as `title`, `parameters`, etc, to customize how to generate a Storybook
|
||||
JSON file from the component.
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class Storybook:
|
||||
def title(self):
|
||||
return self.component.__class__.__name__
|
||||
|
||||
def parameters(self) -> Parameters:
|
||||
return {
|
||||
"server": {
|
||||
"id": self.component.__class__.__name__,
|
||||
}
|
||||
}
|
||||
|
||||
def stories(self) -> List[StoryAnnotations]:
|
||||
return []
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
## Accessing extensions in components
|
||||
|
||||
Above, we've configured extensions `View` and `Storybook` for the `MyTable` component.
|
||||
|
||||
You can access the instances of these extension classes in the component instance.
|
||||
|
||||
For example, the View extension is available as `self.view`:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
def get_context_data(self, request):
|
||||
# `self.view` points to the instance of `View` extension.
|
||||
return {
|
||||
"view": self.view,
|
||||
}
|
||||
```
|
||||
|
||||
And the Storybook extension is available as `self.storybook`:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
def get_context_data(self, request):
|
||||
# `self.storybook` points to the instance of `Storybook` extension.
|
||||
return {
|
||||
"title": self.storybook.title(),
|
||||
}
|
||||
```
|
||||
|
||||
Thus, you can use extensions to add methods or attributes that will be available to all components
|
||||
in their component context.
|
||||
|
||||
## Writing extensions
|
||||
|
||||
Creating extensions in django-components involves defining a class that inherits from
|
||||
[`ComponentExtension`](../../../reference/api/#django_components.ComponentExtension).
|
||||
This class can implement various lifecycle hooks and define new attributes or methods to be added to components.
|
||||
|
||||
### Defining an extension
|
||||
|
||||
To create an extension, define a class that inherits from [`ComponentExtension`](../../../reference/api/#django_components.ComponentExtension)
|
||||
and implement the desired hooks.
|
||||
|
||||
- Each extension MUST have a `name` attribute. The name MUST be a valid Python identifier.
|
||||
- The extension MAY implement any of the [hook methods](../../../reference/extension_hooks).
|
||||
- Each hook method receives a context object with relevant data.
|
||||
|
||||
```python
|
||||
from django_components.extension import ComponentExtension, OnComponentClassCreatedContext
|
||||
|
||||
class MyExtension(ComponentExtension):
|
||||
name = "my_extension"
|
||||
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||
# Custom logic for when a component class is created
|
||||
ctx.component_cls.my_attr = "my_value"
|
||||
```
|
||||
|
||||
### Defining the extension class
|
||||
|
||||
In previous sections we've seen the `View` and `Storybook` extensions classes that were nested within the `Component` class:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class View:
|
||||
...
|
||||
|
||||
class Storybook:
|
||||
...
|
||||
```
|
||||
|
||||
These can be understood as component-specific overrides or configuration.
|
||||
|
||||
The nested extension classes like `View` or `Storybook` will actually subclass from a base extension
|
||||
class as defined on the [`ComponentExtension.ExtensionClass`](../../../reference/api/#django_components.ComponentExtension.ExtensionClass).
|
||||
|
||||
This is how extensions define the "default" behavior of their nested extension classes.
|
||||
|
||||
For example, the `View` base extension class defines the handlers for GET, POST, etc:
|
||||
|
||||
```python
|
||||
from django_components.extension import ComponentExtension, BaseExtensionClass
|
||||
|
||||
class ViewExtension(ComponentExtension):
|
||||
name = "view"
|
||||
|
||||
# The default behavior of the `View` extension class.
|
||||
class ExtensionClass(BaseExtensionClass):
|
||||
def get(self, request):
|
||||
return self.component.get(request)
|
||||
|
||||
def post(self, request):
|
||||
return self.component.post(request)
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
In any component that then defines a nested `View` extension class, the `View` extension class will actually
|
||||
subclass from the `ViewExtension.ExtensionClass` class.
|
||||
|
||||
In other words, when you define a component like this:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View:
|
||||
def get(self, request):
|
||||
# Do something
|
||||
...
|
||||
```
|
||||
|
||||
It will actually be implemented as if the `View` class subclassed from base class `ViewExtension.ExtensionClass`:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View(ViewExtension.ExtensionClass):
|
||||
def get(self, request):
|
||||
# Do something
|
||||
...
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
When writing an extension, the `ExtensionClass` MUST subclass the base class [`BaseExtensionClass`](../../../reference/api/#django_components.ComponentExtension.BaseExtensionClass).
|
||||
|
||||
This base class ensures that the extension class will have access to the component instance.
|
||||
|
||||
### Registering extensions
|
||||
|
||||
Once the extension is defined, it needs to be registered in the Django settings to be used by the application.
|
||||
|
||||
Extensions can be given either as an extension class, or its import string:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
COMPONENTS = {
|
||||
"extensions": [
|
||||
"my_app.extensions.MyExtension",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Or by reference:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
from my_app.extensions import MyExtension
|
||||
|
||||
COMPONENTS = {
|
||||
"extensions": [
|
||||
MyExtension,
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Full example: Custom logging extension
|
||||
|
||||
To tie it all together, here's an example of a custom logging extension that logs when components are created, deleted, or rendered:
|
||||
|
||||
- Each component can specify which color to use for the logging by setting `Component.ColorLogger.color`.
|
||||
- The extension will log the component name and color when the component is created, deleted, or rendered.
|
||||
|
||||
```python
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
OnComponentClassCreatedContext,
|
||||
OnComponentClassDeletedContext,
|
||||
OnComponentInputContext,
|
||||
)
|
||||
|
||||
class ColorLoggerExtensionClass(BaseExtensionClass):
|
||||
color: str
|
||||
|
||||
|
||||
class ColorLoggerExtension(ComponentExtension):
|
||||
name = "color_logger"
|
||||
|
||||
# All `Component.ColorLogger` classes will inherit from this class.
|
||||
ExtensionClass = ColorLoggerExtensionClass
|
||||
|
||||
# These hooks don't have access to the Component instance, only to the Component class,
|
||||
# so we access the color as `Component.ColorLogger.color`.
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||
log.info(
|
||||
f"Component {ctx.component_cls} created.",
|
||||
color=ctx.component_cls.ColorLogger.color,
|
||||
)
|
||||
|
||||
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
|
||||
log.info(
|
||||
f"Component {ctx.component_cls} deleted.",
|
||||
color=ctx.component_cls.ColorLogger.color,
|
||||
)
|
||||
|
||||
# This hook has access to the Component instance, so we access the color
|
||||
# as `self.component.color_logger.color`.
|
||||
def on_component_input(self, ctx: OnComponentInputContext) -> None:
|
||||
log.info(
|
||||
f"Rendering component {ctx.component_cls}.",
|
||||
color=ctx.component.color_logger.color,
|
||||
)
|
||||
```
|
||||
|
||||
To use the `ColorLoggerExtension`, add it to your settings:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
COMPONENTS = {
|
||||
"extensions": [
|
||||
ColorLoggerExtension,
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Once registered, in any component, you can define a `ColorLogger` attribute:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class ColorLogger:
|
||||
color = "red"
|
||||
```
|
||||
|
||||
This will log the component name and color when the component is created, deleted, or rendered.
|
|
@ -267,6 +267,43 @@ Button.render(
|
|||
)
|
||||
```
|
||||
|
||||
### Extensions
|
||||
|
||||
Django-components functionality can be extended with "extensions". Extensions allow for powerful customization and integrations. They can:
|
||||
|
||||
- Tap into lifecycle events, such as when a component is created, deleted, or registered.
|
||||
- Add new attributes and methods to the components under an extension-specific nested class.
|
||||
|
||||
Some of the planned extensions include:
|
||||
|
||||
- Caching
|
||||
- AlpineJS integration
|
||||
- Storybook integration
|
||||
- Pydantic validation
|
||||
- Component-level benchmarking with asv
|
||||
|
||||
### Simple testing
|
||||
|
||||
- Write tests for components with `@djc_test` decorator.
|
||||
- The decorator manages global state, ensuring that tests don't leak.
|
||||
- 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`.
|
||||
|
||||
```python
|
||||
from djc_test import djc_test
|
||||
|
||||
from components.my_component import MyTable
|
||||
|
||||
@djc_test
|
||||
def test_my_table():
|
||||
rendered = MyTable.render(
|
||||
kwargs={
|
||||
"title": "My table",
|
||||
},
|
||||
)
|
||||
assert rendered == "<table>My table</table>"
|
||||
```
|
||||
|
||||
### Handle large projects with ease
|
||||
|
||||
- Components can be infinitely nested.
|
||||
|
|
|
@ -4,6 +4,7 @@ nav:
|
|||
- Commands: commands.md
|
||||
- Components: components.md
|
||||
- Exceptions: exceptions.md
|
||||
- Extension hooks: extension_hooks.md
|
||||
- Middlewares: middlewares.md
|
||||
- Settings: settings.md
|
||||
- Signals: signals.md
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentExtension
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentFileEntry
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
@ -51,6 +55,38 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.OnComponentClassCreatedContext
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.OnComponentClassDeletedContext
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.OnComponentDataContext
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.OnComponentInputContext
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.OnComponentRegisteredContext
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.OnComponentUnregisteredContext
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.OnRegistryCreatedContext
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.OnRegistryDeletedContext
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.RegistrySettings
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
|
152
docs/reference/extension_hooks.md
Normal file
152
docs/reference/extension_hooks.md
Normal file
|
@ -0,0 +1,152 @@
|
|||
<!-- Autogenerated by reference.py -->
|
||||
|
||||
# Extension Hooks
|
||||
|
||||
Overview of all the extension hooks available in Django Components.
|
||||
|
||||
Read more on [Extensions](../../concepts/advanced/extensions).
|
||||
|
||||
|
||||
::: django_components.extension.ComponentExtension.on_component_class_created
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
**Available data:**
|
||||
|
||||
name | type | description
|
||||
--|--|--
|
||||
`component_cls` | [`Type[Component]`](../api#django_components.Component) | The created Component class
|
||||
|
||||
::: django_components.extension.ComponentExtension.on_component_class_deleted
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
**Available data:**
|
||||
|
||||
name | type | description
|
||||
--|--|--
|
||||
`component_cls` | [`Type[Component]`](../api#django_components.Component) | The to-be-deleted Component class
|
||||
|
||||
::: django_components.extension.ComponentExtension.on_component_data
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
**Available data:**
|
||||
|
||||
name | type | description
|
||||
--|--|--
|
||||
`component` | [`Component`](../api#django_components.Component) | The Component instance that is being rendered
|
||||
`component_cls` | [`Type[Component]`](../api#django_components.Component) | The Component class
|
||||
`component_id` | `str` | The unique identifier for this component instance
|
||||
`context_data` | `Dict` | Dictionary of context data from `Component.get_context_data()`
|
||||
`css_data` | `Dict` | Dictionary of CSS data from `Component.get_css_data()`
|
||||
`js_data` | `Dict` | Dictionary of JavaScript data from `Component.get_js_data()`
|
||||
|
||||
::: django_components.extension.ComponentExtension.on_component_input
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
**Available data:**
|
||||
|
||||
name | type | description
|
||||
--|--|--
|
||||
`args` | `List` | List of positional arguments passed to the component
|
||||
`component` | [`Component`](../api#django_components.Component) | The Component instance that received the input and is being rendered
|
||||
`component_cls` | [`Type[Component]`](../api#django_components.Component) | The Component class
|
||||
`component_id` | `str` | The unique identifier for this component instance
|
||||
`context` | [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context) | The Django template Context object
|
||||
`kwargs` | `Dict` | Dictionary of keyword arguments passed to the component
|
||||
`slots` | `Dict` | Dictionary of slot definitions
|
||||
|
||||
::: django_components.extension.ComponentExtension.on_component_registered
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
**Available data:**
|
||||
|
||||
name | type | description
|
||||
--|--|--
|
||||
`component_cls` | [`Type[Component]`](../api#django_components.Component) | The registered Component class
|
||||
`name` | `str` | The name the component was registered under
|
||||
`registry` | [`ComponentRegistry`](../api#django_components.ComponentRegistry) | The registry the component was registered to
|
||||
|
||||
::: django_components.extension.ComponentExtension.on_component_unregistered
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
**Available data:**
|
||||
|
||||
name | type | description
|
||||
--|--|--
|
||||
`component_cls` | [`Type[Component]`](../api#django_components.Component) | The unregistered Component class
|
||||
`name` | `str` | The name the component was registered under
|
||||
`registry` | [`ComponentRegistry`](../api#django_components.ComponentRegistry) | The registry the component was unregistered from
|
||||
|
||||
::: django_components.extension.ComponentExtension.on_registry_created
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
**Available data:**
|
||||
|
||||
name | type | description
|
||||
--|--|--
|
||||
`registry` | [`ComponentRegistry`](../api#django_components.ComponentRegistry) | The created ComponentRegistry instance
|
||||
|
||||
::: django_components.extension.ComponentExtension.on_registry_deleted
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
**Available data:**
|
||||
|
||||
name | type | description
|
||||
--|--|--
|
||||
`registry` | [`ComponentRegistry`](../api#django_components.ComponentRegistry) | The to-be-deleted ComponentRegistry instance
|
||||
|
|
@ -42,6 +42,7 @@ defaults = ComponentsSettings(
|
|||
debug_highlight_components=False,
|
||||
debug_highlight_slots=False,
|
||||
dynamic_component_name="dynamic",
|
||||
extensions=[],
|
||||
libraries=[], # E.g. ["mysite.components.forms", ...]
|
||||
multiline_tags=True,
|
||||
reload_on_file_change=False,
|
||||
|
@ -146,6 +147,16 @@ defaults = ComponentsSettings(
|
|||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
::: django_components.app_settings.ComponentsSettings.extensions
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
show_symbol_type_heading: false
|
||||
show_symbol_type_toc: false
|
||||
show_if_no_docstring: true
|
||||
show_labels: false
|
||||
|
||||
::: django_components.app_settings.ComponentsSettings.forbidden_static_files
|
||||
options:
|
||||
show_root_heading: true
|
||||
|
|
|
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1549" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1568" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -125,10 +125,7 @@ def gen_reference_testing_api():
|
|||
f.write(preface + "\n\n")
|
||||
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if (
|
||||
name.startswith("_")
|
||||
or inspect.ismodule(obj)
|
||||
):
|
||||
if name.startswith("_") or inspect.ismodule(obj):
|
||||
continue
|
||||
|
||||
# For each entry, generate a mkdocstrings entry, e.g.
|
||||
|
@ -586,6 +583,178 @@ def gen_reference_templatevars():
|
|||
f.write(f"::: {ComponentVars.__module__}.{ComponentVars.__name__}.{field}\n\n")
|
||||
|
||||
|
||||
def gen_reference_extension_hooks():
|
||||
"""
|
||||
Generate documentation for the hooks that are available to the extensions.
|
||||
"""
|
||||
module = import_module("django_components.extension")
|
||||
|
||||
preface = "<!-- Autogenerated by reference.py -->\n\n"
|
||||
preface += (root / "docs/templates/reference_extension_hooks.md").read_text()
|
||||
out_file = root / "docs/reference/extension_hooks.md"
|
||||
|
||||
out_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with out_file.open("w", encoding="utf-8") as f:
|
||||
# 1. Insert section from `reference_extension_hooks.md`
|
||||
f.write(preface + "\n\n")
|
||||
|
||||
# 2. Print each hook and their descriptions
|
||||
extension_cls = module.ComponentExtension
|
||||
class_name = get_import_path(extension_cls)
|
||||
|
||||
# NOTE: If no unique methods, just document the class itself without methods
|
||||
unique_methods = _get_unique_methods(NamedTuple, extension_cls)
|
||||
# All hooks start with `on_`, so filter out the rest
|
||||
unique_methods = [name for name in unique_methods if name.startswith("on_")]
|
||||
|
||||
for name in sorted(unique_methods):
|
||||
# Programmatically get the data available inside the hook, so we can generate
|
||||
# a table of available data.
|
||||
# Each hook receives a second "ctx" argument, so we access the typing of this "ctx",
|
||||
# and get its fields.
|
||||
method = getattr(extension_cls, name)
|
||||
ctx_type = method.__annotations__["ctx"]
|
||||
# The Context data class is defined in the same module as the hook, so we can
|
||||
# import it dynamically.
|
||||
ctx_class = getattr(module, ctx_type.__name__)
|
||||
fields = ctx_class._fields
|
||||
field_docstrings = _extract_property_docstrings(ctx_class)
|
||||
|
||||
# Generate the available data table
|
||||
available_data = "**Available data:**\n\n"
|
||||
available_data += "name | type | description\n"
|
||||
available_data += "--|--|--\n"
|
||||
for field in sorted(fields):
|
||||
field_type = _format_hook_type(str(ctx_class.__annotations__[field]))
|
||||
field_desc = field_docstrings[field]
|
||||
available_data += f"`{field}` | {field_type} | {field_desc}\n"
|
||||
|
||||
# For each entry, generate a mkdocstrings entry, e.g.
|
||||
# ```
|
||||
# ::: django_components.extension.ComponentExtension.on_component_registered
|
||||
# options:
|
||||
# ...
|
||||
# ```
|
||||
f.write(
|
||||
f"::: {class_name}.{name}\n"
|
||||
f" options:\n"
|
||||
f" show_root_heading: true\n"
|
||||
f" show_signature: true\n"
|
||||
f" separate_signature: true\n"
|
||||
f" show_symbol_type_heading: false\n"
|
||||
f" show_symbol_type_toc: false\n"
|
||||
f" show_if_no_docstring: true\n"
|
||||
f" show_labels: false\n"
|
||||
)
|
||||
f.write("\n")
|
||||
f.write(available_data)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
forward_ref_pattern = re.compile(r"ForwardRef\('(.+?)'\)")
|
||||
class_repr_pattern = re.compile(r"<class '(.+?)'>")
|
||||
typing_pattern = re.compile(r"typing\.(.+?)")
|
||||
|
||||
|
||||
def _format_hook_type(type_str: str) -> str:
|
||||
# Clean up the type string
|
||||
type_str = forward_ref_pattern.sub(r"\1", type_str)
|
||||
type_str = class_repr_pattern.sub(r"\1", type_str)
|
||||
type_str = typing_pattern.sub(r"\1", type_str)
|
||||
type_str = type_str.replace("django.template.context.Context", "Context")
|
||||
type_str = "`" + type_str + "`"
|
||||
|
||||
# Add links to non-builtin types
|
||||
if "ComponentRegistry" in type_str:
|
||||
type_str = f"[{type_str}](../api#django_components.ComponentRegistry)"
|
||||
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)"
|
||||
|
||||
return type_str
|
||||
|
||||
|
||||
def _extract_property_docstrings(cls: Type) -> Dict[str, str]:
|
||||
"""
|
||||
Python doesn't provide a way to access docstrings of properties, e.g.:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
my_property: str = "Hello, world!"
|
||||
'''
|
||||
My property docstring
|
||||
'''
|
||||
```
|
||||
|
||||
This function extracts the docstrings of properties from the source code.
|
||||
|
||||
Returns a dictionary with the property name as the key and the docstring as the value.
|
||||
|
||||
```python
|
||||
{
|
||||
"my_property": "My property docstring"
|
||||
}
|
||||
```
|
||||
|
||||
NOTE: This is a naive implementation and may not work for all cases:
|
||||
|
||||
- The function expects NO colons (`:`) inside class bases definition.
|
||||
- The function assumes that the docstring is defined with `\"\"\"` or `'''`, and that
|
||||
the docstring begins on a separate line.
|
||||
- The function assumes that the class is defined at the global scope (module level)
|
||||
and that the body is indented with 4 spaces.
|
||||
"""
|
||||
lines, start_line_index = inspect.getsourcelines(cls)
|
||||
attrs_lines = []
|
||||
ignore = True
|
||||
for line in lines:
|
||||
if ignore:
|
||||
if line.endswith("):\n"):
|
||||
ignore = False
|
||||
continue
|
||||
else:
|
||||
attrs_lines.append(line)
|
||||
|
||||
attrs_docstrings = {}
|
||||
curr_attr = None
|
||||
docstring_delimiter = None
|
||||
state = "before_attr"
|
||||
|
||||
while attrs_lines:
|
||||
line = attrs_lines.pop(0)
|
||||
# Exactly 1 indentation and not empty line
|
||||
is_one_indent = line.startswith(" " * 4) and not line.startswith(" " * 5) and line.strip()
|
||||
line = line.strip()
|
||||
|
||||
if state == "before_attr":
|
||||
if not is_one_indent:
|
||||
continue
|
||||
curr_attr = line.split(":", maxsplit=1)[0].strip()
|
||||
attrs_docstrings[curr_attr] = ""
|
||||
state = "before_attr_docstring"
|
||||
elif state == "before_attr_docstring":
|
||||
if not is_one_indent or not (line.startswith("'''") or line.startswith('"""')):
|
||||
continue
|
||||
# Found start of docstring
|
||||
docstring_delimiter = line[0:3]
|
||||
line = line[3:]
|
||||
attrs_lines.insert(0, line)
|
||||
state = "attr_docstring"
|
||||
elif state == "attr_docstring":
|
||||
# Not end of docstring
|
||||
if docstring_delimiter not in line: # type: ignore[operator]
|
||||
attrs_docstrings[curr_attr] += line # type: ignore[index]
|
||||
continue
|
||||
# Found end of docstring
|
||||
last_docstring_line, _ = line.split(docstring_delimiter, maxsplit=1)
|
||||
attrs_docstrings[curr_attr] += last_docstring_line # type: ignore[index]
|
||||
attrs_docstrings[curr_attr] = dedent(attrs_docstrings[curr_attr]) # type: ignore[index]
|
||||
state = "before_attr"
|
||||
|
||||
return attrs_docstrings
|
||||
|
||||
|
||||
# NOTE: Unlike other references, the API of Signals is not yet codified (AKA source of truth defined
|
||||
# as Python code). Instead, we manually list all signals that are sent by django-components.
|
||||
def gen_reference_signals():
|
||||
|
@ -774,6 +943,7 @@ def gen_reference():
|
|||
gen_reference_templatevars()
|
||||
gen_reference_signals()
|
||||
gen_reference_testing_api()
|
||||
gen_reference_extension_hooks()
|
||||
|
||||
|
||||
# This is run when `gen-files` plugin is run in mkdocs.yml
|
||||
|
|
5
docs/templates/reference_extension_hooks.md
vendored
Normal file
5
docs/templates/reference_extension_hooks.md
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Extension Hooks
|
||||
|
||||
Overview of all the extension hooks available in Django Components.
|
||||
|
||||
Read more on [Extensions](../../concepts/advanced/extensions).
|
Loading…
Add table
Add a link
Reference in a new issue