Skip to content

Commit d11b39a

Browse files
authored
Async ORM mutations and queries (#134)
- Disable `thread_sensitive` where it's not needed - Perform queries and mutations using async - `use_query` now supports async functions. - `use_mutation` now supports async functions. - `django_idom.types.QueryOptions.thread_sensitive` option to customize how sync queries are executed. - `django_idom.hooks.use_mutation` now accepts `django_idom.types.MutationOptions` option to customize how mutations are executed. - Use Python's arg/kwarg handlers to properly interpret and differentiate between `kwarg` and `args`. This allow things such as `query=my_function` to be properly handled. - The `mutate` argument on `django_idom.hooks.use_mutation` has been renamed to `mutation`. - Add tests for our database routing feature - Reduce runtime for tests from ~181 seconds to ~11 seconds by only starting the Django server once - Fix bug where ReactPy utilizes Django's default cache timeout, which can prematurely expire our cache entries.
1 parent 1bea69f commit d11b39a

33 files changed

+855
-184
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ logs
88
*.pyc
99
.dccachea
1010
__pycache__
11-
db.sqlite3
11+
*.sqlite3
12+
*.sqlite3-journal
1213
media
1314
cache
1415
static-deploy

CHANGELOG.md

+15-2
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,22 @@ Using the following categories, list your changes in this order:
3434

3535
## [Unreleased]
3636

37-
- Nothing (yet)!
37+
### Added
38+
39+
- `use_query` now supports async functions.
40+
- `use_mutation` now supports async functions.
41+
- `reactpy_django.types.QueryOptions.thread_sensitive` option to customize how sync queries are executed.
42+
- `reactpy_django.hooks.use_mutation` now accepts `reactpy_django.types.MutationOptions` option to customize how mutations are executed.
43+
44+
### Changed
45+
46+
- The `mutate` argument on `reactpy_django.hooks.use_mutation` has been renamed to `mutation`.
47+
48+
### Fixed
49+
50+
- Fix bug where ReactPy utilizes Django's default cache timeout, which can prematurely expire the component cache.
3851

39-
## [3.0.1] - 2023-03-31
52+
## [3.0.1] - 2023-04-06
4053

4154
### Changed
4255

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
</td>
2828
<td>
2929
<a href="https://github.com/reactive-python/reactpy-django">Django</a>,
30-
<a href="https://github.com/idom-team/idom-jupyter">Jupyter</a>,
30+
<a href="https://github.com/reactive-python/reactpy-jupyter">Jupyter</a>,
3131
<a href="https://github.com/idom-team/idom-dash">Plotly-Dash</a>
3232
</td>
3333
</tr>

docs/python/use-query-async.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from channels.db import database_sync_to_async
2+
from example.models import TodoItem
3+
from reactpy import component, html
4+
5+
from reactpy_django.hooks import use_query
6+
7+
8+
async def get_items():
9+
return await database_sync_to_async(TodoItem.objects.all)()
10+
11+
12+
@component
13+
def todo_list():
14+
item_query = use_query(get_items)
15+
16+
if item_query.loading:
17+
rendered_items = html.h2("Loading...")
18+
elif item_query.error or not item_query.data:
19+
rendered_items = html.h2("Error when loading!")
20+
else:
21+
rendered_items = html.ul([html.li(item, key=item) for item in item_query.data])
22+
23+
return html.div("Rendered items: ", rendered_items)

docs/python/use-query-postprocessor-change.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def execute_io_intensive_operation():
1717

1818

1919
@component
20-
def todo_list():
20+
def my_component():
2121
query = use_query(
2222
QueryOptions(
2323
postprocessor=my_postprocessor,

docs/python/use-query-postprocessor-disable.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def execute_io_intensive_operation():
1010

1111

1212
@component
13-
def todo_list():
13+
def my_component():
1414
query = use_query(
1515
QueryOptions(postprocessor=None),
1616
execute_io_intensive_operation,

docs/python/use-query-postprocessor-kwargs.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def get_model_with_relationships():
1616

1717

1818
@component
19-
def todo_list():
19+
def my_component():
2020
query = use_query(
2121
QueryOptions(
2222
postprocessor_kwargs={"many_to_many": False, "many_to_one": False}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from reactpy import component
2+
3+
from reactpy_django.hooks import use_query
4+
from reactpy_django.types import QueryOptions
5+
6+
7+
def execute_thread_safe_operation():
8+
"""This is an example query function that does some thread-safe operation."""
9+
pass
10+
11+
12+
@component
13+
def my_component():
14+
query = use_query(
15+
QueryOptions(thread_sensitive=False),
16+
execute_thread_safe_operation,
17+
)
18+
19+
if query.loading or query.error:
20+
return None
21+
22+
return str(query.data)

docs/python/use-query.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ def todo_list():
1717
elif item_query.error or not item_query.data:
1818
rendered_items = html.h2("Error when loading!")
1919
else:
20-
rendered_items = html.ul(html.li(item, key=item) for item in item_query.data)
20+
rendered_items = html.ul([html.li(item, key=item) for item in item_query.data])
2121

2222
return html.div("Rendered items: ", rendered_items)

docs/src/dictionary.txt

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ postprocessing
3030
serializable
3131
postprocessor
3232
preprocessor
33+
middleware
3334
backends
3435
backend
3536
frontend

docs/src/features/hooks.md

+34-4
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,29 @@ The function you provide into this hook must return either a `Model` or `QuerySe
5757
{% include "../../python/use-query-args.py" %}
5858
```
5959

60-
??? question "Why does the example `get_items` function return `TodoItem.objects.all()`?"
60+
??? question "Why does `get_items` in the example return `TodoItem.objects.all()`?"
6161

6262
This was a technical design decision to based on [Apollo's `useQuery` hook](https://www.apollographql.com/docs/react/data/queries/), but ultimately helps avoid Django's `SynchronousOnlyOperation` exceptions.
6363

6464
The `use_query` hook ensures the provided `Model` or `QuerySet` executes all [deferred](https://docs.djangoproject.com/en/dev/ref/models/instances/#django.db.models.Model.get_deferred_fields)/[lazy queries](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy) safely prior to reaching your components.
6565

66-
??? question "Can this hook be used for things other than the Django ORM?"
66+
??? question "How can I use `QueryOptions` to customize fetching behavior?"
67+
68+
<font size="4">**`thread_sensitive`**</font>
69+
70+
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.
71+
72+
This setting only applies to sync query functions, and will be ignored for async functions.
73+
74+
=== "components.py"
75+
76+
```python
77+
{% include "../../python/use-query-thread-sensitive.py" %}
78+
```
79+
80+
---
81+
82+
<font size="4">**`postprocessor`**</font>
6783

6884
{% include-markdown "../../includes/orm.md" start="<!--orm-fetch-start-->" end="<!--orm-fetch-end-->" %}
6985

@@ -72,7 +88,7 @@ The function you provide into this hook must return either a `Model` or `QuerySe
7288
1. Want to use this hook to defer IO intensive tasks to be computed in the background
7389
2. Want to to utilize `use_query` with a different ORM
7490

75-
... then you can disable all postprocessing behavior by modifying the `QueryOptions.postprocessor` parameter. In the example below, we will set the `postprocessor` to `None` to disable postprocessing behavior.
91+
... then you can either set a custom `postprocessor`, or disable all postprocessing behavior by modifying the `QueryOptions.postprocessor` parameter. In the example below, we will set the `postprocessor` to `None` to disable postprocessing behavior.
7692

7793
=== "components.py"
7894

@@ -92,7 +108,9 @@ The function you provide into this hook must return either a `Model` or `QuerySe
92108
{% include "../../python/use-query-postprocessor-change.py" %}
93109
```
94110

95-
??? question "How can I prevent this hook from recursively fetching `ManyToMany` fields or `ForeignKey` relationships?"
111+
---
112+
113+
<font size="4">**`postprocessor_kwargs`**</font>
96114

97115
{% include-markdown "../../includes/orm.md" start="<!--orm-fetch-start-->" end="<!--orm-fetch-end-->" %}
98116

@@ -108,6 +126,18 @@ The function you provide into this hook must return either a `Model` or `QuerySe
108126

109127
_Note: In Django's ORM design, the field name to access foreign keys is [postfixed with `_set`](https://docs.djangoproject.com/en/dev/topics/db/examples/many_to_one/) by default._
110128

129+
??? question "Can I define async query functions?"
130+
131+
Async functions are supported by `use_query`. You can use them in the same way as a sync query function.
132+
133+
However, be mindful of Django async ORM restrictions.
134+
135+
=== "components.py"
136+
137+
```python
138+
{% include "../../python/use-query-async.py" %}
139+
```
140+
111141
??? question "Can I make ORM calls without hooks?"
112142

113143
{% include-markdown "../../includes/orm.md" start="<!--orm-excp-start-->" end="<!--orm-excp-end-->" %}

docs/src/get-started/render-view.md renamed to docs/src/get-started/register-view.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
---
88

9-
## Render Your View
9+
## Register a View
1010

1111
We will assume you have [created a Django View](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) before, but here's a simple example below.
1212

@@ -28,8 +28,6 @@ We will add this new view into your [`urls.py`](https://docs.djangoproject.com/e
2828
{% include "../../python/example/urls.py" %}
2929
```
3030

31-
Now, navigate to `http://127.0.0.1:8000/example/`. If you copy-pasted the component from the previous example, you will now see your component display "Hello World".
32-
3331
??? question "Which urls.py do I add my views to?"
3432

3533
For simple **Django projects**, you can easily add all of your views directly into the **Django project's** `urls.py`. However, as you start increase your project's complexity you might end up with way too much within one file.

docs/src/get-started/run-webserver.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## Overview
2+
3+
!!! summary
4+
5+
Run a webserver to display your Django view.
6+
7+
---
8+
9+
## Run the Webserver
10+
11+
To test your new Django view, run the following command to start up a development webserver.
12+
13+
```bash linenums="0"
14+
python manage.py runserver
15+
```
16+
17+
Now you can navigate to your **Django project** URL that contains an ReactPy component, such as `http://127.0.0.1:8000/example/` (_from the previous step_).
18+
19+
If you copy-pasted our example component, you will now see your component display "Hello World".
20+
21+
??? warning "Do not use `manage.py runserver` for production."
22+
23+
The webserver contained within `manage.py runserver` is only intended for development and testing purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/dev/howto/deployment/).

mkdocs.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ nav:
66
- Choose a Django App: get-started/choose-django-app.md
77
- Create a Component: get-started/create-component.md
88
- Use the Template Tag: get-started/use-template-tag.md
9-
- Render Your View: get-started/render-view.md
9+
- Register a View: get-started/register-view.md
10+
- Run the Webserver: get-started/run-webserver.md
1011
- Learn More: get-started/learn-more.md
1112
- Reference:
1213
- Components: features/components.md

noxfile.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_suite(session: Session) -> None:
5656
posargs.append("--debug-mode")
5757

5858
session.run("playwright", "install", "chromium")
59-
session.run("python", "manage.py", "test", *posargs)
59+
session.run("python", "manage.py", "test", *posargs, "-v 2")
6060

6161

6262
@nox.session

requirements/test-env.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ django
22
playwright
33
twisted
44
channels[daphne]>=4.0.0
5+
tblib

src/reactpy_django/config.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
from reactpy.config import REACTPY_DEBUG_MODE
77
from reactpy.core.types import ComponentConstructor
88

9-
from reactpy_django.types import Postprocessor, ViewComponentIframe
9+
from reactpy_django.types import (
10+
AsyncPostprocessor,
11+
SyncPostprocessor,
12+
ViewComponentIframe,
13+
)
1014
from reactpy_django.utils import import_dotted_path
1115

1216

@@ -37,10 +41,12 @@
3741
"REACTPY_DATABASE",
3842
DEFAULT_DB_ALIAS,
3943
)
40-
REACTPY_DEFAULT_QUERY_POSTPROCESSOR: Postprocessor | None = import_dotted_path(
41-
getattr(
42-
settings,
43-
"REACTPY_DEFAULT_QUERY_POSTPROCESSOR",
44-
"reactpy_django.utils.django_query_postprocessor",
44+
REACTPY_DEFAULT_QUERY_POSTPROCESSOR: AsyncPostprocessor | SyncPostprocessor | None = (
45+
import_dotted_path(
46+
getattr(
47+
settings,
48+
"REACTPY_DEFAULT_QUERY_POSTPROCESSOR",
49+
"reactpy_django.utils.django_query_postprocessor",
50+
)
4551
)
4652
)

0 commit comments

Comments
 (0)