diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index bbf12c58..26feda67 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -409,11 +409,12 @@ Compatible with both [standard Django forms](https://docs.djangoproject.com/en/s | `#!python on_success` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when the form is successfully submitted. | `#!python None` | | `#!python on_error` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when the form submission fails. | `#!python None` | | `#!python on_receive_data` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called before newly submitted form data is rendered. | `#!python None` | - | `#!python on_change` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when the form is changed. | `#!python None` | + | `#!python on_change` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when a form field is modified by the user. | `#!python None` | | `#!python auto_save` | `#!python bool` | If `#!python True`, the form will automatically call `#!python save` on successful submission of a `#!python ModelForm`. This has no effect on regular `#!python Form` instances. | `#!python True` | | `#!python extra_props` | `#!python dict[str, Any] | None` | Additional properties to add to the `#!html
` element. | `#!python None` | | `#!python extra_transforms` | `#!python Sequence[Callable[[VdomDict], Any]] | None` | A list of functions that transforms the newly generated VDOM. The functions will be repeatedly called on each VDOM node. | `#!python None` | | `#!python form_template` | `#!python str | None` | The template to use for the form. If `#!python None`, Django's default template is used. | `#!python None` | + | `#!python thread_sensitive` | `#!python bool` | Whether to run event callback functions in thread sensitive mode. This mode only applies to sync functions, and is turned on by default due to Django ORM limitations. | `#!python True` | | `#!python top_children` | `#!python Sequence[Any]` | Additional elements to add to the top of the form. | `#!python tuple` | | `#!python bottom_children` | `#!python Sequence[Any]` | Additional elements to add to the bottom of the form. | `#!python tuple` | | `#!python key` | `#!python Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings. | `#!python None` | diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index 65bf1727..5826a7b0 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -46,7 +46,7 @@ Query functions can be sync or async. | --- | --- | --- | --- | | `#!python query` | `#!python Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred]` | A function that executes a query and returns some data. | N/A | | `#!python kwargs` | `#!python dict[str, Any] | None` | Keyword arguments to passed into the `#!python query` function. | `#!python None` | - | `#!python thread_sensitive` | `#!python bool` | Whether to run your query function in thread sensitive mode. This mode only applies to sync query functions, and is turned on by default due to Django ORM limitations. | `#!python True` | + | `#!python thread_sensitive` | `#!python bool` | Whether to run your query function in thread sensitive mode. This setting only applies to sync functions, and is turned on by default due to Django ORM limitations. | `#!python True` | | `#!python postprocessor` | `#!python AsyncPostprocessor | SyncPostprocessor | None` | A callable that processes the query `#!python data` before it is returned. The first argument of postprocessor function must be the query `#!python data`. All proceeding arguments are optional `#!python postprocessor_kwargs`. This postprocessor function must return the modified `#!python data`. | `#!python None` | | `#!python postprocessor_kwargs` | `#!python dict[str, Any] | None` | Keyworded arguments passed into the `#!python postprocessor` function. | `#!python None` | @@ -188,7 +188,7 @@ Mutation functions can be sync or async. | Name | Type | Description | Default | | --- | --- | --- | --- | | `#!python mutation` | `#!python Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `#!python False`, then your `#!python refetch` function will not be used. | N/A | - | `#!python thread_sensitive` | `#!python bool` | Whether to run the mutation in thread sensitive mode. This mode only applies to sync mutation functions, and is turned on by default due to Django ORM limitations. | `#!python True` | + | `#!python thread_sensitive` | `#!python bool` | Whether to run the mutation in thread sensitive mode. This setting only applies to sync functions, and is turned on by default due to Django ORM limitations. | `#!python True` | | `#!python refetch` | `#!python Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A query function (the function you provide to your `#!python use_query` hook) or a sequence of query functions that need a `#!python refetch` if the mutation succeeds. This is useful for refreshing data after a mutation has been performed. | `#!python None` | **Returns** diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py index d0db240c..7e821c1c 100644 --- a/src/reactpy_django/components.py +++ b/src/reactpy_django/components.py @@ -131,6 +131,7 @@ def django_form( extra_props: dict[str, Any] | None = None, extra_transforms: Sequence[Callable[[VdomDict], Any]] | None = None, form_template: str | None = None, + thread_sensitive: bool = True, top_children: Sequence[Any] = (), bottom_children: Sequence[Any] = (), key: Key | None = None, @@ -144,13 +145,16 @@ def django_form( on_success: A callback function that is called when the form is successfully submitted. on_error: A callback function that is called when the form submission fails. on_receive_data: A callback function that is called before newly submitted form data is rendered. - on_change: A callback function that is called when the form is changed. + on_change: A callback function that is called when a form field is modified by the user. auto_save: If `True`, the form will automatically call `save` on successful submission of \ a `ModelForm`. This has no effect on regular `Form` instances. extra_props: Additional properties to add to the `html.form` element. extra_transforms: A list of functions that transforms the newly generated VDOM. \ The functions will be repeatedly called on each VDOM node. form_template: The template to use for the form. If `None`, Django's default template is used. + thread_sensitive: Whether to run event callback functions in thread sensitive mode. \ + This mode only applies to sync functions, and is turned on by default due to Django \ + ORM limitations. top_children: Additional elements to add to the top of the form. bottom_children: Additional elements to add to the bottom of the form. key: A key to uniquely identify this component which is unique amongst a component's \ @@ -167,6 +171,7 @@ def django_form( extra_props=extra_props or {}, extra_transforms=extra_transforms or [], form_template=form_template, + thread_sensitive=thread_sensitive, top_children=top_children, bottom_children=bottom_children, key=key, diff --git a/src/reactpy_django/forms/components.py b/src/reactpy_django/forms/components.py index f0be5398..157bed23 100644 --- a/src/reactpy_django/forms/components.py +++ b/src/reactpy_django/forms/components.py @@ -43,6 +43,7 @@ def _django_form( extra_props: dict, extra_transforms: Sequence[Callable[[VdomDict], Any]], form_template: str | None, + thread_sensitive: bool, top_children: Sequence, bottom_children: Sequence, ): @@ -80,9 +81,9 @@ async def render_form(): await database_sync_to_async(initialized_form.full_clean)() success = not initialized_form.errors.as_data() if success and on_success: - await ensure_async(on_success)(form_event) + await ensure_async(on_success, thread_sensitive=thread_sensitive)(form_event) if not success and on_error: - await ensure_async(on_error)(form_event) + await ensure_async(on_error, thread_sensitive=thread_sensitive)(form_event) if success and auto_save and isinstance(initialized_form, ModelForm): await database_sync_to_async(initialized_form.save)() set_submitted_data(None) @@ -103,7 +104,7 @@ async def on_submit_callback(new_data: dict[str, Any]): new_form_event = FormEventData( form=initialized_form, submitted_data=new_data, set_submitted_data=set_submitted_data ) - await ensure_async(on_receive_data)(new_form_event) + await ensure_async(on_receive_data, thread_sensitive=thread_sensitive)(new_form_event) if submitted_data != new_data: set_submitted_data(new_data) @@ -111,7 +112,7 @@ async def on_submit_callback(new_data: dict[str, Any]): async def _on_change(_event): """Event that exist solely to allow the user to detect form changes.""" if on_change: - await ensure_async(on_change)(form_event) + await ensure_async(on_change, thread_sensitive=thread_sensitive)(form_event) if not rendered_form: return None diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index 9f76902f..ba3642f6 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -104,7 +104,7 @@ def use_query( Kwargs: kwargs: Keyword arguments to passed into the `query` function. thread_sensitive: Whether to run the query in thread sensitive mode. \ - This mode only applies to sync query functions, and is turned on by default \ + This setting only applies to sync functions, and is turned on by default \ due to Django ORM limitations. postprocessor: A callable that processes the query `data` before it is returned. \ The first argument of postprocessor function must be the query `data`. All \ @@ -219,7 +219,7 @@ def use_mutation( Kwargs: thread_sensitive: Whether to run the mutation in thread sensitive mode. \ - This mode only applies to sync mutation functions, and is turned on by default \ + This setting only applies to sync functions, and is turned on by default \ due to Django ORM limitations. refetch: A query function (the function you provide to your `use_query` \ hook) or a sequence of query functions that need a `refetch` if the \ diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index 88272867..77dec277 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -11,6 +11,7 @@ from concurrent.futures import ThreadPoolExecutor from copy import deepcopy from fnmatch import fnmatch +from functools import wraps from importlib import import_module from pathlib import Path from typing import TYPE_CHECKING, Any, Awaitable, Callable @@ -47,6 +48,9 @@ from django.views import View from reactpy.types import ComponentConstructor + from reactpy_django.types import FuncParams, Inferred + + _logger = logging.getLogger(__name__) _TAG_PATTERN = r"(?Pcomponent)" _PATH_PATTERN = r"""(?P"[^"'\s]+"|'[^"'\s]+')"""