Skip to content

use_query and use_mutation #86

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 60 commits into from
Sep 15, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
c01152a
wip use_query and use_mutation
rmorshea Jul 2, 2022
86b6575
fetch deferred attrs
rmorshea Jul 15, 2022
780251c
update comment
rmorshea Jul 15, 2022
e465055
use dataclass instead of namedtuple
rmorshea Jul 15, 2022
adc69b6
sort imports
rmorshea Jul 15, 2022
84f8332
move undefined to types module
Archmonger Jul 26, 2022
e618868
formatting
Archmonger Jul 26, 2022
2d8d9e9
attempt fix for checkout warning
Archmonger Jul 26, 2022
70e3c91
fix docs typo
Archmonger Jul 26, 2022
f3b1d9e
add to changelog
Archmonger Jul 26, 2022
f920ae6
change event function name
Archmonger Jul 26, 2022
2c2e4e1
separate query/mutation docs
Archmonger Jul 26, 2022
725995b
enable link checking
Archmonger Jul 26, 2022
196c7ba
verbose link checking
Archmonger Jul 26, 2022
6180719
bump setup python version
Archmonger Jul 26, 2022
ae7c754
bump setup node
Archmonger Jul 26, 2022
b1b1552
fix task name
Archmonger Jul 26, 2022
01c427f
Update src/django_idom/hooks.py
Archmonger Jul 26, 2022
5c81104
Merge branch 'use_database' of https://github.com/idom-team/django-id…
Archmonger Jul 26, 2022
da9f4ce
Can `use_mutation` trigger refetch of a `use_query`
Archmonger Jul 26, 2022
08950d9
wordsmith
Archmonger Jul 26, 2022
d67f9b4
event["target"]["value"]
Archmonger Jul 29, 2022
6cb01fa
Ignore some type hints
Archmonger Jul 30, 2022
1317701
add ORM clarification
Archmonger Jul 30, 2022
308b5d1
misc fixes + remove fetch_deferred_fields
rmorshea Jul 30, 2022
7327106
remove unused code
Archmonger Jul 30, 2022
668ffc3
More typehint cleanup
Archmonger Jul 30, 2022
4c3de35
put deferred fetch back
rmorshea Aug 2, 2022
8f73020
switch from selenium to playwright
rmorshea Aug 2, 2022
7b6ced3
add basic test
rmorshea Aug 2, 2022
0b5c1fd
fix style
rmorshea Aug 2, 2022
02936e3
headless by default
rmorshea Aug 2, 2022
f437106
increase DB timeout
Archmonger Aug 2, 2022
acfe4e5
add todo item to admin site
Archmonger Aug 2, 2022
aa6139a
fix item done toggle
Archmonger Aug 2, 2022
e56255e
format
Archmonger Aug 2, 2022
8595c4e
attempt using onChange as devtools suggests
Archmonger Aug 2, 2022
a007566
remove accidental sleep
Archmonger Aug 2, 2022
093fc36
try to fix tests
rmorshea Aug 9, 2022
83b2d01
fix setup.py for deprecated distutils
rmorshea Aug 14, 2022
6a754cf
use propper skip error
rmorshea Aug 14, 2022
3d336b3
bump idom dep
rmorshea Aug 14, 2022
38de554
Fix context type hint
Archmonger Aug 14, 2022
5203f0d
bump idom
Archmonger Sep 12, 2022
1f2ed01
remove unused import
rmorshea Sep 13, 2022
ab07327
Merge branch 'main' into use_database
rmorshea Sep 13, 2022
0b8013c
no mypy on tests
rmorshea Sep 13, 2022
fd09362
bump idom-client-react
Archmonger Sep 13, 2022
1e1d979
bump idom version
rmorshea Sep 13, 2022
a53ece4
add delay to typing
rmorshea Sep 13, 2022
ca289d0
revert idom.html changes in tests
Archmonger Sep 14, 2022
281c5af
clean up noxfile
Archmonger Sep 14, 2022
949e815
bump idom client
Archmonger Sep 14, 2022
80b1471
Merge branch 'use_database' of https://github.com/idom-team/django-id…
Archmonger Sep 14, 2022
ba42a8b
Move mutation and query to types.py
Archmonger Sep 14, 2022
b174a80
Python < 3.10 compatibility
Archmonger Sep 14, 2022
0324b06
Revert "Python < 3.10 compatibility"
Archmonger Sep 14, 2022
19806cb
from __future__ import annotations
Archmonger Sep 14, 2022
f672798
remove TransactionTestCase
Archmonger Sep 15, 2022
81928fb
revert channels login changes
Archmonger Sep 15, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/features/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,55 @@

Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html?highlight=hooks) on hooks!

## Use Query and Use Mutation

<!-- TODO: Add description -->

```python
from example_project.my_app.models import TodoItem
from idom import component, html
from django_idom.hooks import use_query, use_mutation


def get_items():
return TodoItem.objects.all()

def add_item(text: str):
TodoItem(text=text).save()


@component
def todo_list():
items_query = use_query(get_items)
add_item_mutation = use_mutation(add_item, refetch=get_items)
item_draft, set_item_draft = use_state("")

if items_query.loading:
items_view = html.h2("Loading...")
elif items_query.error:
items_view = html.h2(f"Error when loading: {items.error}")
else:
items_view = html.ul(html.li(item, key=item) for item in items_query.data)

if add_item_mutation.loading:
add_item_status = html.h2("Adding...")
elif add_item_mutation.error:
add_item_status = html.h2(f"Error when adding: {add_item_mutation.error}")
else:
add_item_status = ""

def handle_add_item(event):
set_item_draft("")
add_item_mutation.execute(text=item_draft)

return html.div(
html.label("Add an item:")
html.input({"type": "text", "onClick": handle_add_item})
add_item_status,
items_view,
)
```

## Use Websocket

You can fetch the Django Channels websocket at any time by using `use_websocket`.
Expand Down
1 change: 1 addition & 0 deletions requirements/pkg-deps.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
channels >=3.0.0
idom >=0.39.0, <0.40.0
aiofile >=3.0
typing_extensions
158 changes: 155 additions & 3 deletions src/django_idom/hooks.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
from typing import Dict, Type, Union
from __future__ import annotations

from dataclasses import dataclass
from threading import Thread
from types import FunctionType
from typing import Any, Callable, DefaultDict, Generic, Sequence, Type, TypeVar, Union

from django.db.models.base import Model
from django.db.models.query import QuerySet
from idom import use_callback
from idom.backend.types import Location
from idom.core.hooks import Context, create_context, use_context
from idom.core.hooks import Context, create_context, use_context, use_effect, use_state
from typing_extensions import ParamSpec

from django_idom.types import IdomWebsocket
from django_idom.utils import UNDEFINED


WebsocketContext: Type[Context[Union[IdomWebsocket, None]]] = create_context(
Expand All @@ -19,7 +29,7 @@ def use_location() -> Location:
return Location(scope["path"], f"?{search}" if search else "")


def use_scope() -> Dict:
def use_scope() -> dict[str, Any]:
"""Get the current ASGI scope dictionary"""
return use_websocket().scope

Expand All @@ -30,3 +40,145 @@ def use_websocket() -> IdomWebsocket:
if websocket is None:
raise RuntimeError("No websocket. Are you running with a Django server?")
return websocket


_REFETCH_CALLBACKS: DefaultDict[FunctionType, set[Callable[[], None]]] = DefaultDict(
set
)


_Data = TypeVar("_Data")
_Params = ParamSpec("_Params")


def use_query(
query: Callable[_Params, _Data],
*args: _Params.args,
fetch_deferred_fields: bool = True,
**kwargs: _Params.kwargs,
) -> Query[_Data]:
given_query = query
query, _ = use_state(given_query)
if given_query is not query:
raise ValueError(f"Query function changed from {query} to {given_query}.")

data, set_data = use_state(UNDEFINED)
loading, set_loading = use_state(True)
error, set_error = use_state(None)

@use_callback
def refetch() -> None:
set_data(UNDEFINED)
set_loading(True)
set_error(None)

@use_effect(dependencies=[])
def add_refetch_callback():
# By tracking callbacks globally, any usage of the query function will be re-run
# if the user has told a mutation to refetch it.
_REFETCH_CALLBACKS[query].add(refetch)
return lambda: _REFETCH_CALLBACKS[query].remove(refetch)

@use_effect(dependencies=None)
def execute_query():
if data is not UNDEFINED:
return

def thread_target():
try:
query_result = query(*args, **kwargs)
except Exception as e:
set_data(UNDEFINED)
set_loading(False)
set_error(e)
return

if isinstance(query_result, QuerySet):
if fetch_deferred_fields:
for model in query_result:
_fetch_deferred_fields(model)
else:
# still force query set to execute
for _ in query_result:
pass
elif isinstance(query_result, Model):
if fetch_deferred_fields:
_fetch_deferred_fields(query_result)
elif fetch_deferred_fields:
raise ValueError(
f"Expected {query} to return Model or Query because "
f"{fetch_deferred_fields=}, got {query_result!r}"
)

set_data(query_result)
set_loading(False)
set_error(None)

# We need to run this in a thread so we don't prevent rendering when loading.
# We also can't do this async since Django's ORM doesn't support this yet.
Thread(target=thread_target, daemon=True).start()

return Query(data, loading, error, refetch)


@dataclass
class Query(Generic[_Data]):
data: _Data
loading: bool
error: Exception | None
refetch: Callable[[], None]


def use_mutation(
mutate: Callable[_Params, None],
refetch: Callable[..., Any] | Sequence[Callable[..., Any]],
) -> Mutation[_Params]:
loading, set_loading = use_state(True)
error, set_error = use_state(None)

@use_callback
def call(*args: _Params.args, **kwargs: _Params.kwargs) -> None:
set_loading(True)

def thread_target():
try:
mutate(*args, **kwargs)
except Exception as e:
set_loading(False)
set_error(e)
else:
set_loading(False)
set_error(None)
for query in (refetch,) if isinstance(refetch, Query) else refetch:
refetch_callback = _REFETCH_CALLBACKS.get(query)
if refetch_callback is not None:
refetch_callback()

# We need to run this in a thread so we don't prevent rendering when loading.
# We also can't do this async since Django's ORM doesn't support this yet.
Thread(target=thread_target, daemon=True).start()

@use_callback
def reset() -> None:
set_loading(False)
set_error(None)

return Query(call, loading, error, reset)


@dataclass
class Mutation(Generic[_Params]):
execute: Callable[_Params, None]
loading: bool
error: Exception | None
reset: Callable[[], None]


_Model = TypeVar("_Model", bound=Model)


def _fetch_deferred_fields(model: _Model) -> _Model:
for field in model.get_deferred_fields():
value = getattr(model, field)
if isinstance(value, Model):
_fetch_deferred_fields(value)
9 changes: 9 additions & 0 deletions src/django_idom/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,12 @@ def _register_components(self, components: Set) -> None:
"\033[0m",
component,
)


class _Undefined:
def __repr__(self):
return "UNDEFINED"


UNDEFINED = _Undefined()
"""Sentinel for undefined values"""