diff --git a/CHANGELOG.md b/CHANGELOG.md
index aa53f765..5c9b047f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,7 +34,21 @@ Using the following categories, list your changes in this order:
## [Unreleased]
-- Nothing (yet)!
+### Changed
+
+- New syntax for `use_query` and `use_mutation` hooks. Here's a quick comparison of the changes:
+
+ ```python
+ query = use_query(QueryOptions(thread_sensitive=True), get_items, value=123456, foo="bar") # Old
+ query = use_query(get_items, {"value":12356, "foo":"bar"}, thread_sensitive=True) # New
+
+ mutation = use_mutation(MutationOptions(thread_sensitive=True), remove_item) # Old
+ mutation = use_mutation(remove_item, thread_sensitive=True) # New
+ ```
+
+### Removed
+
+- `QueryOptions` and `MutationOptions` have been removed. Their values are now passed direct into the hook.
## [3.8.1] - 2024-05-07
diff --git a/docs/examples/python/django-query-postprocessor.py b/docs/examples/python/django-query-postprocessor.py
index ff7419b5..da33c362 100644
--- a/docs/examples/python/django-query-postprocessor.py
+++ b/docs/examples/python/django-query-postprocessor.py
@@ -1,7 +1,6 @@
from example.models import TodoItem
from reactpy import component
from reactpy_django.hooks import use_query
-from reactpy_django.types import QueryOptions
from reactpy_django.utils import django_query_postprocessor
@@ -11,13 +10,11 @@ def get_items():
@component
def todo_list():
- # These `QueryOptions` are functionally equivalent to ReactPy-Django's default values
+ # These postprocessor options are functionally equivalent to ReactPy-Django's default values
item_query = use_query(
- QueryOptions(
- postprocessor=django_query_postprocessor,
- postprocessor_kwargs={"many_to_many": True, "many_to_one": True},
- ),
get_items,
+ postprocessor=django_query_postprocessor,
+ postprocessor_kwargs={"many_to_many": True, "many_to_one": True},
)
return item_query.data
diff --git a/docs/examples/python/use-mutation-query-refetch.py b/docs/examples/python/use-mutation-query-refetch.py
index b0817713..227ab1a7 100644
--- a/docs/examples/python/use-mutation-query-refetch.py
+++ b/docs/examples/python/use-mutation-query-refetch.py
@@ -26,7 +26,9 @@ def submit_event(event):
elif item_query.error or not item_query.data:
rendered_items = html.h2("Error when loading!")
else:
- rendered_items = html.ul(html.li(item, key=item.pk) for item in item_query.data)
+ rendered_items = html.ul(
+ html.li(item.text, key=item.pk) for item in item_query.data
+ )
# Handle all possible mutation states
if item_mutation.loading:
diff --git a/docs/examples/python/use-mutation-thread-sensitive.py b/docs/examples/python/use-mutation-thread-sensitive.py
index 0242e41b..85046dc0 100644
--- a/docs/examples/python/use-mutation-thread-sensitive.py
+++ b/docs/examples/python/use-mutation-thread-sensitive.py
@@ -1,6 +1,5 @@
from reactpy import component, html
from reactpy_django.hooks import use_mutation
-from reactpy_django.types import MutationOptions
def execute_thread_safe_mutation(text):
@@ -11,8 +10,8 @@ def execute_thread_safe_mutation(text):
@component
def my_component():
item_mutation = use_mutation(
- MutationOptions(thread_sensitive=False),
execute_thread_safe_mutation,
+ thread_sensitive=False,
)
def submit_event(event):
diff --git a/docs/examples/python/use-query-args.py b/docs/examples/python/use-query-args.py
index 60e8851a..8deb549a 100644
--- a/docs/examples/python/use-query-args.py
+++ b/docs/examples/python/use-query-args.py
@@ -2,16 +2,11 @@
from reactpy_django.hooks import use_query
-def example_query(value: int, other_value: bool = False):
- ...
+def example_query(value: int, other_value: bool = False): ...
@component
def my_component():
- query = use_query(
- example_query,
- 123,
- other_value=True,
- )
+ query = use_query(example_query, {"value": 123, "other_value": True})
return str(query.data)
diff --git a/docs/examples/python/use-query-postprocessor-change.py b/docs/examples/python/use-query-postprocessor-change.py
index c331285e..5685956d 100644
--- a/docs/examples/python/use-query-postprocessor-change.py
+++ b/docs/examples/python/use-query-postprocessor-change.py
@@ -1,6 +1,5 @@
from reactpy import component
from reactpy_django.hooks import use_query
-from reactpy_django.types import QueryOptions
def my_postprocessor(data, example_kwarg=True):
@@ -18,11 +17,9 @@ def execute_io_intensive_operation():
@component
def my_component():
query = use_query(
- QueryOptions(
- postprocessor=my_postprocessor,
- postprocessor_kwargs={"example_kwarg": False},
- ),
execute_io_intensive_operation,
+ postprocessor=my_postprocessor,
+ postprocessor_kwargs={"example_kwarg": False},
)
if query.loading or query.error:
diff --git a/docs/examples/python/use-query-postprocessor-disable.py b/docs/examples/python/use-query-postprocessor-disable.py
index 32e981f1..e9541924 100644
--- a/docs/examples/python/use-query-postprocessor-disable.py
+++ b/docs/examples/python/use-query-postprocessor-disable.py
@@ -1,6 +1,5 @@
from reactpy import component
from reactpy_django.hooks import use_query
-from reactpy_django.types import QueryOptions
def execute_io_intensive_operation():
@@ -11,8 +10,8 @@ def execute_io_intensive_operation():
@component
def my_component():
query = use_query(
- QueryOptions(postprocessor=None),
execute_io_intensive_operation,
+ postprocessor=None,
)
if query.loading or query.error:
diff --git a/docs/examples/python/use-query-postprocessor-kwargs.py b/docs/examples/python/use-query-postprocessor-kwargs.py
index b11eb2b5..4ed108af 100644
--- a/docs/examples/python/use-query-postprocessor-kwargs.py
+++ b/docs/examples/python/use-query-postprocessor-kwargs.py
@@ -1,7 +1,6 @@
from example.models import TodoItem
from reactpy import component
from reactpy_django.hooks import use_query
-from reactpy_django.types import QueryOptions
def get_model_with_relationships():
@@ -17,10 +16,8 @@ def get_model_with_relationships():
@component
def my_component():
query = use_query(
- QueryOptions(
- postprocessor_kwargs={"many_to_many": False, "many_to_one": False}
- ),
get_model_with_relationships,
+ postprocessor_kwargs={"many_to_many": False, "many_to_one": False},
)
if query.loading or query.error or not query.data:
diff --git a/docs/examples/python/use-query-thread-sensitive.py b/docs/examples/python/use-query-thread-sensitive.py
index 9af19f2b..d657be6b 100644
--- a/docs/examples/python/use-query-thread-sensitive.py
+++ b/docs/examples/python/use-query-thread-sensitive.py
@@ -1,6 +1,5 @@
from reactpy import component
from reactpy_django.hooks import use_query
-from reactpy_django.types import QueryOptions
def execute_thread_safe_operation():
@@ -10,10 +9,7 @@ def execute_thread_safe_operation():
@component
def my_component():
- query = use_query(
- QueryOptions(thread_sensitive=False),
- execute_thread_safe_operation,
- )
+ query = use_query(execute_thread_safe_operation, thread_sensitive=False)
if query.loading or query.error:
return None
diff --git a/docs/examples/python/use-query.py b/docs/examples/python/use-query.py
index cd8d171b..5688765b 100644
--- a/docs/examples/python/use-query.py
+++ b/docs/examples/python/use-query.py
@@ -18,7 +18,7 @@ def todo_list():
rendered_items = html.h2("Error when loading!")
else:
rendered_items = html.ul(
- [html.li(item, key=item.pk) for item in item_query.data]
+ [html.li(item.text, key=item.pk) for item in item_query.data]
)
return html.div("Rendered items: ", rendered_items)
diff --git a/docs/includes/orm.md b/docs/includes/orm.md
index 58f45477..a2a18dfe 100644
--- a/docs/includes/orm.md
+++ b/docs/includes/orm.md
@@ -8,6 +8,6 @@ These `#!python SynchronousOnlyOperation` exceptions may be removed in a future
-By default, automatic recursive fetching of `#!python ManyToMany` or `#!python ForeignKey` fields is enabled within the `django_query_postprocessor`. This is needed to prevent `#!python SynchronousOnlyOperation` exceptions when accessing these fields within your ReactPy components.
+By default, automatic recursive fetching of `#!python ManyToMany` or `#!python ForeignKey` fields is enabled within the `#!python django_query_postprocessor`. This is needed to prevent `#!python SynchronousOnlyOperation` exceptions when accessing these fields within your ReactPy components.
diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md
index b3ec7d6e..6d7da90f 100644
--- a/docs/src/reference/hooks.md
+++ b/docs/src/reference/hooks.md
@@ -44,20 +44,21 @@ Query functions can be sync or async.
| Name | Type | Description | Default |
| --- | --- | --- | --- |
- | `#!python options` | `#!python QueryOptions | None` | An optional `#!python QueryOptions` object that can modify how the query is executed. | `#!python None` |
- | `#!python query` | `#!python Callable[_Params, _Result | None]` | A callable that returns a Django `#!python Model` or `#!python QuerySet`. | N/A |
- | `#!python *args` | `#!python _Params.args` | Positional arguments to pass into `#!python query`. | N/A |
- | `#!python **kwargs` | `#!python _Params.kwargs` | Keyword arguments to pass into `#!python query`. | N/A |
+ | `#!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 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` |
**Returns**
| Type | Description |
| --- | --- |
- | `#!python Query[_Result | None]` | An object containing `#!python loading`/`#!python error` states, your `#!python data` (if the query has successfully executed), and a `#!python refetch` callable that can be used to re-run the query. |
+ | `#!python Query[Inferred]` | An object containing `#!python loading`/`#!python error` states, your `#!python data` (if the query has successfully executed), and a `#!python refetch` callable that can be used to re-run the query. |
??? question "How can I provide arguments to my query function?"
- `#!python *args` and `#!python **kwargs` can be provided to your query function via `#!python use_query` parameters.
+ `#!python kwargs` can be provided to your query function via the `#!python kwargs=...` parameter.
=== "components.py"
@@ -67,15 +68,15 @@ Query functions can be sync or async.
??? question "How can I customize this hook's behavior?"
- This hook accepts a `#!python options: QueryOptions` parameter that can be used to customize behavior.
+ This hook has several parameters that can be used to customize behavior.
- Below are the settings that can be modified via these `#!python QueryOptions`.
+ Below are examples of values that can be modified.
---
**`#!python thread_sensitive`**
- Whether to run your synchronous query function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information.
+ Whether to run your synchronous query function in thread sensitive mode. Thread sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information.
This setting only applies to sync query functions, and will be ignored for async functions.
@@ -96,7 +97,7 @@ Query functions can be sync or async.
1. Want to use this hook to defer IO intensive tasks to be computed in the background
2. Want to to utilize `#!python use_query` with a different ORM
- ... then you can either set a custom `#!python postprocessor`, or disable all postprocessing behavior by modifying the `#!python QueryOptions.postprocessor` parameter. In the example below, we will set the `#!python postprocessor` to `#!python None` to disable postprocessing behavior.
+ ... then you can either set a custom `#!python postprocessor`, or disable all postprocessing behavior by modifying the `#!python postprocessor=...` parameter. In the example below, we will set the `#!python postprocessor` to `#!python None` to disable postprocessing behavior.
=== "components.py"
@@ -104,11 +105,7 @@ Query functions can be sync or async.
{% include "../../examples/python/use-query-postprocessor-disable.py" %}
```
- If you wish to create a custom `#!python postprocessor`, you will need to create a callable.
-
- The first argument of `#!python postprocessor` must be the query `#!python data`. All proceeding arguments
- are optional `#!python postprocessor_kwargs` (see below). This `#!python postprocessor` must return
- the modified `#!python data`.
+ If you wish to create a custom `#!python postprocessor`, you will need to create a function where the first must be the query `#!python data`. All proceeding arguments are optional `#!python postprocessor_kwargs` (see below). This `#!python postprocessor` function must return the modified `#!python data`.
=== "components.py"
@@ -124,7 +121,7 @@ Query functions can be sync or async.
However, if you have deep nested trees of relational data, this may not be a desirable behavior. In these scenarios, you may prefer to manually fetch these relational fields using a second `#!python use_query` hook.
- You can disable the prefetching behavior of the default `#!python postprocessor` (located at `#!python reactpy_django.utils.django_query_postprocessor`) via the `#!python QueryOptions.postprocessor_kwargs` parameter.
+ You can disable the prefetching behavior of the default `#!python postprocessor` (located at `#!python reactpy_django.utils.django_query_postprocessor`) via the `#!python postprocessor_kwargs=...` parameter.
=== "components.py"
@@ -140,7 +137,7 @@ Query functions can be sync or async.
??? question "Can I make a failed query try again?"
- Yes, a `#!python use_mutation` can be re-performed by calling `#!python reset()` on your `#!python use_mutation` instance.
+ Yes, `#!python use_mutation` can be re-executed by calling `#!python reset()` on your `#!python use_mutation` instance.
For example, take a look at `#!python reset_event` below.
@@ -190,14 +187,15 @@ Mutation functions can be sync or async.
| Name | Type | Description | Default |
| --- | --- | --- | --- |
- | `#!python mutation` | `#!python Callable[_Params, 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 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 `refetch` if the mutation succeeds. This is useful for refreshing data after a mutation has been performed. | `#!python None` |
+ | `#!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 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**
| Type | Description |
| --- | --- |
- | `#!python Mutation[_Params]` | An object containing `#!python loading`/`#!python error` states, a `#!python reset` callable that will set `#!python loading`/`#!python error` states to defaults, and a `#!python execute` callable that will run the query. |
+ | `#!python Mutation[FuncParams]` | An object containing `#!python loading`/`#!python error` states, and a `#!python reset` callable that will set `#!python loading`/`#!python error` states to defaults. This object can be called to run the query. |
??? question "How can I provide arguments to my mutation function?"
@@ -211,15 +209,15 @@ Mutation functions can be sync or async.
??? question "How can I customize this hook's behavior?"
- This hook accepts a `#!python options: MutationOptions` parameter that can be used to customize behavior.
+ This hook has several parameters that can be used to customize behavior.
- Below are the settings that can be modified via these `#!python MutationOptions`.
+ Below are examples of values that can be modified.
---
**`#!python thread_sensitive`**
- Whether to run your synchronous mutation function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information.
+ Whether to run your synchronous mutation function in thread sensitive mode. Thread sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information.
This setting only applies to sync query functions, and will be ignored for async functions.
@@ -235,7 +233,7 @@ Mutation functions can be sync or async.
??? question "Can I make a failed mutation try again?"
- Yes, a `#!python use_mutation` can be re-performed by calling `#!python reset()` on your `#!python use_mutation` instance.
+ Yes, `#!python use_mutation` can be re-executed by calling `#!python reset()` on your `#!python use_mutation` instance.
For example, take a look at `#!python reset_event` below.
@@ -257,7 +255,7 @@ Mutation functions can be sync or async.
The example below is a merge of the `#!python use_query` and `#!python use_mutation` examples above with the addition of a `#!python use_mutation(refetch=...)` argument.
- Please note that `refetch` will cause all `#!python use_query` hooks that use `#!python get_items` in the current component tree will be refetched.
+ Please note that `#!python refetch` will cause all `#!python use_query` hooks that use `#!python get_items` in the current component tree will be refetched.
=== "components.py"
diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py
index 8015a4ab..e13655c2 100644
--- a/src/reactpy_django/hooks.py
+++ b/src/reactpy_django/hooks.py
@@ -11,7 +11,6 @@
Sequence,
Union,
cast,
- overload,
)
from uuid import uuid4
@@ -29,16 +28,16 @@
from reactpy_django.types import (
AsyncMessageReceiver,
AsyncMessageSender,
+ AsyncPostprocessor,
ConnectionType,
FuncParams,
Inferred,
Mutation,
- MutationOptions,
Query,
- QueryOptions,
+ SyncPostprocessor,
UserData,
)
-from reactpy_django.utils import generate_obj_name, get_pk
+from reactpy_django.utils import django_query_postprocessor, generate_obj_name, get_pk
if TYPE_CHECKING:
from channels_redis.core import RedisChannelLayer
@@ -51,7 +50,6 @@
)
-# TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.*
def use_location() -> Location:
"""Get the current route as a `Location` object"""
return _use_location()
@@ -85,7 +83,6 @@ def use_origin() -> str | None:
return None
-# TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.*
def use_scope() -> dict[str, Any]:
"""Get the current ASGI scope dictionary"""
scope = _use_scope()
@@ -96,55 +93,55 @@ def use_scope() -> dict[str, Any]:
raise TypeError(f"Expected scope to be a dict, got {type(scope)}")
-# TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.*
def use_connection() -> ConnectionType:
"""Get the current `Connection` object"""
return _use_connection()
-@overload
def use_query(
- options: QueryOptions,
- /,
query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred],
- *args: FuncParams.args,
- **kwargs: FuncParams.kwargs,
-) -> Query[Inferred]: ...
-
-
-@overload
-def use_query(
- query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred],
- *args: FuncParams.args,
- **kwargs: FuncParams.kwargs,
-) -> Query[Inferred]: ...
-
-
-def use_query(*args, **kwargs) -> Query[Inferred]:
+ kwargs: dict[str, Any] | None = None,
+ *,
+ thread_sensitive: bool = True,
+ postprocessor: (
+ AsyncPostprocessor | SyncPostprocessor | None
+ ) = django_query_postprocessor,
+ postprocessor_kwargs: dict[str, Any] | None = None,
+) -> Query[Inferred]:
"""This hook is used to execute functions in the background and return the result, \
typically to read data the Django ORM.
Args:
- options: An optional `QueryOptions` object that can modify how the query is executed.
- query: A callable that returns a Django `Model` or `QuerySet`.
- *args: Positional arguments to pass into `query`.
+ query: A function that executes a query and returns some data.
- Keyword Args:
- **kwargs: Keyword arguments to pass into `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 \
+ 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 \
+ proceeding arguments are optional `postprocessor_kwargs`. This postprocessor \
+ function must return the modified `data`. \
+ \
+ If unset, `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` is used. By default, this \
+ is used to prevent Django's lazy query execution and supports `many_to_many` \
+ and `many_to_one` as `postprocessor_kwargs`.
+ postprocessor_kwargs: Keyworded arguments passed into the `postprocessor` function.
+
+ Returns:
+ An object containing `loading`/`#!python error` states, your `data` (if the query \
+ has successfully executed), and a `refetch` callable that can be used to re-run the query.
+ """
should_execute, set_should_execute = use_state(True)
data, set_data = use_state(cast(Inferred, None))
loading, set_loading = use_state(True)
error, set_error = use_state(cast(Union[Exception, None], None))
- if isinstance(args[0], QueryOptions):
- query_options, query, query_args, query_kwargs = _use_query_args_1(
- *args, **kwargs
- )
- else:
- query_options, query, query_args, query_kwargs = _use_query_args_2(
- *args, **kwargs
- )
query_ref = use_ref(query)
+ kwargs = kwargs or {}
+ postprocessor_kwargs = postprocessor_kwargs or {}
+
if query_ref.current is not query:
raise ValueError(f"Query function changed from {query_ref.current} to {query}.")
@@ -153,31 +150,27 @@ async def execute_query() -> None:
try:
# Run the query
if asyncio.iscoroutinefunction(query):
- new_data = await query(*query_args, **query_kwargs)
+ new_data = await query(**kwargs)
else:
new_data = await database_sync_to_async(
- query,
- thread_sensitive=query_options.thread_sensitive,
- )(*query_args, **query_kwargs)
+ query, thread_sensitive=thread_sensitive
+ )(**kwargs)
# Run the postprocessor
- if query_options.postprocessor:
- if asyncio.iscoroutinefunction(query_options.postprocessor):
- new_data = await query_options.postprocessor(
- new_data, **query_options.postprocessor_kwargs
- )
+ if postprocessor:
+ if asyncio.iscoroutinefunction(postprocessor):
+ new_data = await postprocessor(new_data, **postprocessor_kwargs)
else:
new_data = await database_sync_to_async(
- query_options.postprocessor,
- thread_sensitive=query_options.thread_sensitive,
- )(new_data, **query_options.postprocessor_kwargs)
+ postprocessor, thread_sensitive=thread_sensitive
+ )(new_data, **postprocessor_kwargs)
# Log any errors and set the error state
except Exception as e:
set_data(cast(Inferred, None))
set_loading(False)
set_error(e)
- _logger.exception(f"Failed to execute query: {generate_obj_name(query)}")
+ _logger.exception("Failed to execute query: %s", generate_obj_name(query))
return
# Query was successful
@@ -212,30 +205,18 @@ def register_refetch_callback() -> Callable[[], None]:
_REFETCH_CALLBACKS[query].add(refetch)
return lambda: _REFETCH_CALLBACKS[query].remove(refetch)
- # The query's user API
+ # Return Query user API
return Query(data, loading, error, refetch)
-@overload
-def use_mutation(
- options: MutationOptions,
- mutation: (
- Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]
- ),
- refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None,
-) -> Mutation[FuncParams]: ...
-
-
-@overload
def use_mutation(
mutation: (
Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]
),
+ *,
+ thread_sensitive: bool = True,
refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None,
-) -> Mutation[FuncParams]: ...
-
-
-def use_mutation(*args: Any, **kwargs: Any) -> Mutation[FuncParams]:
+) -> Mutation[FuncParams]:
"""This hook is used to modify data in the background, typically to create/update/delete \
data from the Django ORM.
@@ -246,18 +227,24 @@ def use_mutation(*args: Any, **kwargs: Any) -> Mutation[FuncParams]:
mutation: A callable that performs Django ORM create, update, or delete \
functionality. If this function returns `False`, then your `refetch` \
function will not be used.
+
+ 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 \
+ 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 \
mutation succeeds. This is useful for refreshing data after a mutation \
has been performed.
+
+ Returns:
+ An object containing `#!python loading`/`#!python error` states, and a \
+ `#!python reset` callable that will set `#!python loading`/`#!python error` \
+ states to defaults. This object can be called to run the query.
"""
loading, set_loading = use_state(False)
error, set_error = use_state(cast(Union[Exception, None], None))
- if isinstance(args[0], MutationOptions):
- mutation_options, mutation, refetch = _use_mutation_args_1(*args, **kwargs)
- else:
- mutation_options, mutation, refetch = _use_mutation_args_2(*args, **kwargs)
# The main "running" function for `use_mutation`
async def execute_mutation(exec_args, exec_kwargs) -> None:
@@ -267,7 +254,7 @@ async def execute_mutation(exec_args, exec_kwargs) -> None:
should_refetch = await mutation(*exec_args, **exec_kwargs)
else:
should_refetch = await database_sync_to_async(
- mutation, thread_sensitive=mutation_options.thread_sensitive
+ mutation, thread_sensitive=thread_sensitive
)(*exec_args, **exec_kwargs)
# Log any errors and set the error state
@@ -275,7 +262,7 @@ async def execute_mutation(exec_args, exec_kwargs) -> None:
set_loading(False)
set_error(e)
_logger.exception(
- f"Failed to execute mutation: {generate_obj_name(mutation)}"
+ "Failed to execute mutation: %s", generate_obj_name(mutation)
)
# Mutation was successful
@@ -311,7 +298,7 @@ def reset() -> None:
set_loading(False)
set_error(None)
- # The mutation's user API
+ # Return mutation user API
return Mutation(schedule_mutation, loading, error, reset)
@@ -355,11 +342,13 @@ async def _set_user_data(data: dict):
await model.asave()
query: Query[dict | None] = use_query(
- QueryOptions(postprocessor=None),
_get_user_data,
- user=user,
- default_data=default_data,
- save_default_data=save_default_data,
+ kwargs={
+ "user": user,
+ "default_data": default_data,
+ "save_default_data": save_default_data,
+ },
+ postprocessor=None,
)
mutation = use_mutation(_set_user_data, refetch=_get_user_data)
@@ -444,22 +433,6 @@ def use_root_id() -> str:
return scope["reactpy"]["id"]
-def _use_query_args_1(options: QueryOptions, /, query: Query, *args, **kwargs):
- return options, query, args, kwargs
-
-
-def _use_query_args_2(query: Query, *args, **kwargs):
- return QueryOptions(), query, args, kwargs
-
-
-def _use_mutation_args_1(options: MutationOptions, mutation: Mutation, refetch=None):
- return options, mutation, refetch
-
-
-def _use_mutation_args_2(mutation, refetch=None):
- return MutationOptions(), mutation, refetch
-
-
async def _get_user_data(
user: AbstractUser, default_data: None | dict, save_default_data: bool
) -> dict | None:
diff --git a/src/reactpy_django/types.py b/src/reactpy_django/types.py
index 8efda72f..91ffc319 100644
--- a/src/reactpy_django/types.py
+++ b/src/reactpy_django/types.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from dataclasses import dataclass, field
+from dataclasses import dataclass
from typing import (
TYPE_CHECKING,
Any,
@@ -52,49 +52,11 @@ def __call__(self, *args: FuncParams.args, **kwargs: FuncParams.kwargs) -> None:
class AsyncPostprocessor(Protocol):
- async def __call__(self, data: Any) -> Any:
- ...
+ async def __call__(self, data: Any) -> Any: ...
class SyncPostprocessor(Protocol):
- def __call__(self, data: Any) -> Any:
- ...
-
-
-@dataclass
-class QueryOptions:
- """Configuration options that can be provided to `use_query`."""
-
- from reactpy_django.config import REACTPY_DEFAULT_QUERY_POSTPROCESSOR
-
- postprocessor: AsyncPostprocessor | SyncPostprocessor | None = (
- REACTPY_DEFAULT_QUERY_POSTPROCESSOR
- )
- """A callable that can modify the query `data` after the query has been executed.
-
- The first argument of postprocessor must be the query `data`. All proceeding arguments
- are optional `postprocessor_kwargs` (see below). This postprocessor function must return
- the modified `data`.
-
- If unset, REACTPY_DEFAULT_QUERY_POSTPROCESSOR is used.
-
- ReactPy's default django_query_postprocessor prevents Django's lazy query execution, and
- additionally can be configured via `postprocessor_kwargs` to recursively fetch
- `many_to_many` and `many_to_one` fields."""
-
- postprocessor_kwargs: MutableMapping[str, Any] = field(default_factory=lambda: {})
- """Keyworded arguments directly passed into the `postprocessor` for configuration."""
-
- thread_sensitive: bool = True
- """Whether to run the query in thread-sensitive mode. This setting only applies to sync query functions."""
-
-
-@dataclass
-class MutationOptions:
- """Configuration options that can be provided to `use_mutation`."""
-
- thread_sensitive: bool = True
- """Whether to run the mutation in thread-sensitive mode. This setting only applies to sync mutation functions."""
+ def __call__(self, data: Any) -> Any: ...
@dataclass
@@ -112,10 +74,8 @@ class UserData(NamedTuple):
class AsyncMessageReceiver(Protocol):
- async def __call__(self, message: dict) -> None:
- ...
+ async def __call__(self, message: dict) -> None: ...
class AsyncMessageSender(Protocol):
- async def __call__(self, message: dict) -> None:
- ...
+ async def __call__(self, message: dict) -> None: ...
diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py
index 457caf47..73538ad7 100644
--- a/src/reactpy_django/utils.py
+++ b/src/reactpy_django/utils.py
@@ -29,19 +29,19 @@
)
_logger = logging.getLogger(__name__)
-_component_tag = r"(?Pcomponent)"
-_component_path = r"""(?P"[^"'\s]+"|'[^"'\s]+')"""
-_component_offline_kwarg = (
- rf"""(\s*offline\s*=\s*{_component_path.replace(r"", r"")})"""
+_TAG_PATTERN = r"(?Pcomponent)"
+_PATH_PATTERN = r"""(?P"[^"'\s]+"|'[^"'\s]+')"""
+_OFFLINE_KWARG_PATTERN = (
+ rf"""(\s*offline\s*=\s*{_PATH_PATTERN.replace(r"", r"")})"""
)
-_component_generic_kwarg = r"""(\s*.*?)"""
+_GENERIC_KWARG_PATTERN = r"""(\s*.*?)"""
COMMENT_REGEX = re.compile(r"")
COMPONENT_REGEX = re.compile(
r"{%\s*"
- + _component_tag
+ + _TAG_PATTERN
+ r"\s*"
- + _component_path
- + rf"({_component_offline_kwarg}|{_component_generic_kwarg})*?"
+ + _PATH_PATTERN
+ + rf"({_OFFLINE_KWARG_PATTERN}|{_GENERIC_KWARG_PATTERN})*?"
+ r"\s*%}"
)
@@ -262,7 +262,7 @@ def django_query_postprocessor(
) -> QuerySet | Model:
"""Recursively fetch all fields within a `Model` or `QuerySet` to ensure they are not performed lazily.
- Behaviors can be modified through `QueryOptions` within your `use_query` hook.
+ Behavior can be modified through `postprocessor_kwargs` within your `use_query` hook.
Args:
data: The `Model` or `QuerySet` to recursively fetch fields from.
@@ -275,8 +275,7 @@ def django_query_postprocessor(
The `Model` or `QuerySet` with all fields fetched.
"""
- # `QuerySet`, which is an iterable of `Model`/`QuerySet` instances
- # https://github.com/typeddjango/django-stubs/issues/704
+ # `QuerySet`, which is an iterable containing `Model`/`QuerySet` objects.
if isinstance(data, QuerySet):
for model in data:
django_query_postprocessor(
@@ -314,7 +313,7 @@ def django_query_postprocessor(
"One of the following may have occurred:\n"
" - You are using a non-Django ORM.\n"
" - You are attempting to use `use_query` to fetch non-ORM data.\n\n"
- "If these situations seem correct, you may want to consider disabling the postprocessor via `QueryOptions`."
+ "If these situations seem correct, you may want to consider disabling the postprocessor."
)
return data
@@ -381,4 +380,4 @@ def strtobool(val):
elif val in ("n", "no", "f", "false", "off", "0"):
return 0
else:
- raise ValueError("invalid truth value %r" % (val,))
+ raise ValueError(f"invalid truth value {val}")
diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py
index 0d4c62d1..345f399e 100644
--- a/src/reactpy_django/websocket/consumer.py
+++ b/src/reactpy_django/websocket/consumer.py
@@ -29,6 +29,8 @@
if TYPE_CHECKING:
from django.contrib.auth.models import AbstractUser
+ from reactpy_django import models
+
_logger = logging.getLogger(__name__)
BACKHAUL_LOOP = asyncio.new_event_loop()
@@ -47,9 +49,18 @@ def start_backhaul_loop():
class ReactpyAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer):
"""Communicates with the browser to perform actions on-demand."""
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # New WebsocketConsumer attributes created by ReactPy
+ self.dispatcher: Future | asyncio.Task
+ self.threaded: bool
+ self.recv_queue: asyncio.Queue
+ self.dotted_path: str
+ self.component_session: "models.ComponentSession" | None = None
+
async def connect(self) -> None:
"""The browser has connected."""
- from reactpy_django import models
from reactpy_django.config import (
REACTPY_AUTH_BACKEND,
REACTPY_AUTO_RELOGIN,
@@ -79,9 +90,7 @@ async def connect(self) -> None:
)
# Start the component dispatcher
- self.dispatcher: Future | asyncio.Task
self.threaded = REACTPY_BACKHAUL_THREAD
- self.component_session: models.ComponentSession | None = None
if self.threaded:
if not BACKHAUL_THREAD.is_alive():
await asyncio.to_thread(
@@ -149,14 +158,14 @@ async def run_dispatcher(self):
)
scope = self.scope
- self.dotted_path = dotted_path = scope["url_route"]["kwargs"]["dotted_path"]
+ self.dotted_path = scope["url_route"]["kwargs"]["dotted_path"]
uuid = scope["url_route"]["kwargs"].get("uuid")
has_args = scope["url_route"]["kwargs"].get("has_args")
scope["reactpy"] = {"id": str(uuid)}
query_string = parse_qs(scope["query_string"].decode(), strict_parsing=True)
http_pathname = query_string.get("http_pathname", [""])[0]
http_search = query_string.get("http_search", [""])[0]
- self.recv_queue: asyncio.Queue = asyncio.Queue()
+ self.recv_queue = asyncio.Queue()
connection = Connection( # For `use_connection`
scope=scope,
location=Location(pathname=http_pathname, search=http_search),
@@ -168,11 +177,11 @@ async def run_dispatcher(self):
# Verify the component has already been registered
try:
- root_component_constructor = REACTPY_REGISTERED_COMPONENTS[dotted_path]
+ root_component_constructor = REACTPY_REGISTERED_COMPONENTS[self.dotted_path]
except KeyError:
await asyncio.to_thread(
_logger.warning,
- f"Attempt to access invalid ReactPy component: {dotted_path!r}",
+ f"Attempt to access invalid ReactPy component: {self.dotted_path!r}",
)
return
@@ -194,7 +203,7 @@ async def run_dispatcher(self):
except models.ComponentSession.DoesNotExist:
await asyncio.to_thread(
_logger.warning,
- f"Component session for '{dotted_path}:{uuid}' not found. The "
+ f"Component session for '{self.dotted_path}:{uuid}' not found. The "
"session may have already expired beyond REACTPY_SESSION_MAX_AGE. "
"If you are using a custom `host`, you may have forgotten to provide "
"args/kwargs.",
diff --git a/tests/test_app/components.py b/tests/test_app/components.py
index dbe9bd8f..fe6df2f0 100644
--- a/tests/test_app/components.py
+++ b/tests/test_app/components.py
@@ -10,7 +10,6 @@
from django.shortcuts import render
from reactpy import component, hooks, html, web
from reactpy_django.components import view_to_component, view_to_iframe
-from reactpy_django.types import QueryOptions
from test_app.models import (
AsyncForiegnChild,
@@ -659,7 +658,7 @@ def custom_host(number=0):
@component
def broken_postprocessor_query():
relational_parent = reactpy_django.hooks.use_query(
- QueryOptions(postprocessor=None), get_relational_parent_query
+ get_relational_parent_query, postprocessor=None
)
if not relational_parent.data:
@@ -720,9 +719,9 @@ async def on_submit(event):
"data-fetch-error": bool(user_data_query.error),
"data-mutation-error": bool(user_data_mutation.error),
"data-loading": user_data_query.loading or user_data_mutation.loading,
- "data-username": "AnonymousUser"
- if current_user.is_anonymous
- else current_user.username,
+ "data-username": (
+ "AnonymousUser" if current_user.is_anonymous else current_user.username
+ ),
},
html.div("use_user_data"),
html.button({"class": "login-1", "on_click": login_user1}, "Login 1"),
@@ -788,9 +787,9 @@ async def on_submit(event):
"data-fetch-error": bool(user_data_query.error),
"data-mutation-error": bool(user_data_mutation.error),
"data-loading": user_data_query.loading or user_data_mutation.loading,
- "data-username": "AnonymousUser"
- if current_user.is_anonymous
- else current_user.username,
+ "data-username": (
+ "AnonymousUser" if current_user.is_anonymous else current_user.username
+ ),
},
html.div("use_user_data_with_default"),
html.button({"class": "login-3", "on_click": login_user3}, "Login 3"),