Skip to content

Commit 93f918a

Browse files
committed
documentation
1 parent a88bee4 commit 93f918a

File tree

5 files changed

+85
-26
lines changed

5 files changed

+85
-26
lines changed

docs/src/dictionary.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ unstyled
2222
py
2323
idom
2424
asgi
25+
postfixed
26+
postprocessing

docs/src/features/hooks.md

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ The function you provide into this hook must return either a `Model` or `QuerySe
5454
| Name | Type | Description | Default |
5555
| --- | --- | --- | --- |
5656
| `query` | `Callable[_Params, _Result | None]` | A callable that returns a Django `Model` or `QuerySet`. | N/A |
57+
| `options` | `QueryOptions | None` | An optional `QueryOptions` object that can modify how the query is executed. | None |
5758
| `*args` | `_Params.args` | Positional arguments to pass into `query`. | N/A |
5859
| `**kwargs` | `_Params.kwargs` | Keyword arguments to pass into `query`. | N/A |
5960

@@ -67,19 +68,83 @@ The function you provide into this hook must return either a `Model` or `QuerySe
6768

6869
Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a `SynchronousOnlyOperation` exception.
6970

70-
This may be resolved in a future version of Django containing an asynchronous ORM.
71+
This compatibility may be resolved in a future version of Django containing an asynchronous ORM. However, it is still best practice to always perform ORM calls in the background via `use_query`.
7172

72-
??? question "Why does the example `get_items` function return a `Model` or `QuerySet`?"
73+
??? question "Can this hook be used for things other than the Django ORM?"
7374

74-
This was a technical design decision to [based on Apollo](https://www.apollographql.com/docs/react/data/mutations/#usemutation-api), but ultimately helps avoid Django's `SynchronousOnlyOperation` exceptions.
75+
If you...
7576

76-
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.
77+
1. Want to use this hook to defer IO intensive tasks to be computed in the background
78+
2. Want to to utilize `use_query` with a different ORM
79+
80+
... then you can disable all postprocessing behavior by modifying the `postprocessor` parameter in `QueryOptions`.
81+
82+
```python
83+
from django_idom.types import QueryOptions
84+
from django_idom.hooks import use_query
85+
86+
def io_intensive_operation():
87+
"""This is an example function call that does something IO intensive, but can
88+
potentially fail to execute."""
89+
...
90+
91+
@component
92+
def todo_list():
93+
query = use_query(
94+
io_intensive_operation,
95+
QueryOptions(
96+
# By setting the postprocessor to a function that takes one argument
97+
# and returns None, we can disable postprocessing behavior.
98+
postprocessor=lambda data: None,
99+
),
100+
)
101+
102+
if query.loading or query.error:
103+
return None
104+
105+
return str(query.data)
106+
```
107+
108+
??? question "Can this hook automatically fetch `ManyToMany` fields or `ForeignKey` relationships?"
109+
110+
By default, automatic recursive fetching of `ManyToMany` or `ForeignKey` fields is disabled for performance reasons.
111+
112+
If you wish you enable this feature, you can modify the `postprocessor_kwargs` parameter in `QueryOptions`.
77113

78-
??? question "What is an "ORM"?"
114+
```python
115+
from example_project.my_app.models import MyModel
116+
from django_idom.types import QueryOptions
117+
from django_idom.hooks import use_query
118+
119+
def model_with_relationships():
120+
"""This is an example function that gets `MyModel` that has a ManyToMany field, and
121+
additionally other models that have formed a ForeignKey association to `MyModel`.
122+
123+
ManyToMany Field: `many_to_many_field`
124+
ForeignKey Field: `foreign_key_field_set`
125+
"""
126+
return MyModel.objects.get(id=1)
127+
128+
@component
129+
def todo_list():
130+
query = use_query(
131+
io_intensive_operation,
132+
QueryOptions(postprocessor_kwargs={"many_to_many": True, "many_to_one": True}),
133+
)
134+
135+
if query.loading or query.error:
136+
return None
137+
138+
return f"{query.data.many_to_many_field} {query.data.foriegn_key_field_set}"
139+
```
79140

80-
A Python **Object Relational Mapper** is an API for your code to access a database.
141+
Please note that due Django's ORM design, the field name to access foreign keys is [always be postfixed with `_set`](https://docs.djangoproject.com/en/dev/topics/db/examples/many_to_one/).
81142

82-
See the [Django ORM documentation](https://docs.djangoproject.com/en/dev/topics/db/queries/) for more information.
143+
??? question "Why does the example `get_items` function return a `Model` or `QuerySet`?"
144+
145+
This was a technical design decision to [based on Apollo](https://www.apollographql.com/docs/react/data/mutations/#usemutation-api), but ultimately helps avoid Django's `SynchronousOnlyOperation` exceptions.
146+
147+
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.
83148

84149
## Use Mutation
85150

@@ -244,12 +309,6 @@ The function you provide into this hook will have no return value.
244309

245310
However, even when resolved it is best practice to perform ORM queries within the `use_query` in order to handle `loading` and `error` states.
246311

247-
??? question "What is an "ORM"?"
248-
249-
A Python **Object Relational Mapper** is an API for your code to access a database.
250-
251-
See the [Django ORM documentation](https://docs.djangoproject.com/en/dev/topics/db/queries/) for more information.
252-
253312
## Use Websocket
254313

255314
You can fetch the Django Channels [websocket](https://channels.readthedocs.io/en/stable/topics/consumers.html#asyncjsonwebsocketconsumer) at any time by using `use_websocket`.

src/django_idom/hooks.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def use_query(
112112
113113
Args:
114114
query: A callable that returns a Django `Model` or `QuerySet`.
115-
options: An optional `QueryOptions` object that can modify how the query is excuted.
115+
options: An optional `QueryOptions` object that can modify how the query is executed.
116116
*args: Positional arguments to pass into `query`.
117117
118118
Keyword Args:
@@ -159,11 +159,11 @@ def execute_query() -> None:
159159

160160
# Use a custom postprocessor, if provided
161161
if query_options.postprocessor:
162-
query_options.postprocessor(data, **query_options.postprocessor_options)
162+
query_options.postprocessor(data, **query_options.postprocessor_kwargs)
163163

164164
# Use the default postprocessor
165165
else:
166-
_postprocess_django_query(data, **query_options.postprocessor_options)
166+
postprocess_django_query(data, **query_options.postprocessor_kwargs)
167167
except Exception as e:
168168
set_data(None)
169169
set_loading(False)
@@ -235,7 +235,7 @@ def reset() -> None:
235235
return Mutation(call, loading, error, reset)
236236

237237

238-
def _postprocess_django_query(
238+
def postprocess_django_query(
239239
data: QuerySet | Model, /, many_to_many: bool = False, many_to_one: bool = False
240240
) -> None:
241241
"""Recursively fetch all fields within a `Model` or `QuerySet` to ensure they are not performed lazily.
@@ -246,7 +246,7 @@ def _postprocess_django_query(
246246
# https://github.com/typeddjango/django-stubs/issues/704
247247
if isinstance(data, QuerySet): # type: ignore[misc]
248248
for model in data:
249-
_postprocess_django_query(
249+
postprocess_django_query(
250250
model,
251251
many_to_many=many_to_many,
252252
many_to_one=many_to_one,
@@ -266,7 +266,7 @@ def _postprocess_django_query(
266266

267267
elif many_to_many and isinstance(field, ManyToManyField):
268268
prefetch_fields.append(field.name)
269-
_postprocess_django_query(
269+
postprocess_django_query(
270270
getattr(data, field.name).get_queryset(),
271271
many_to_many=many_to_many,
272272
many_to_one=many_to_one,

src/django_idom/types.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,14 @@ def __call__(self, data: Any, **kwargs: Any) -> None:
7272
class QueryOptions:
7373
"""Configuration options that can be provided to `use_query`."""
7474

75-
postprocessor_options: dict[str, Any] = field(default_factory=lambda: {})
76-
"""Configuration values usable by the `postprocessor`."""
77-
7875
postprocessor: Postprocessor | None = None
7976
"""A post processing callable that can read/modify the query `data`.
8077
81-
`postprocessor_options` are provided to this `postprocessor` as keyword arguments.
82-
8378
If `None`, the default postprocessor is used.
8479
8580
This default Django query postprocessor prevents Django's lazy query execution, and
86-
additionally can be configured via `postprocessor_options` to recursively fetch
81+
additionally can be configured via `postprocessor_kwargs` to recursively fetch
8782
`many_to_many` and `many_to_one` fields."""
83+
84+
postprocessor_kwargs: dict[str, Any] = field(default_factory=lambda: {})
85+
"""Keyworded arguments directly passed into the `postprocessor` for configuration."""

tests/test_app/components.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def get_foriegn_child_query():
185185
def relational_query():
186186
relational_parent = use_query(
187187
get_relational_parent_query,
188-
QueryOptions(postprocessor_options={"many_to_many": True, "many_to_one": True}),
188+
QueryOptions(postprocessor_kwargs={"many_to_many": True, "many_to_one": True}),
189189
)
190190
foriegn_child = use_query(get_foriegn_child_query)
191191

0 commit comments

Comments
 (0)