From 1278729a662a29572a0bf3182a993fad01dc70f9 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 1 Jul 2022 20:15:52 -0700 Subject: [PATCH 1/4] Draft of use_sync_to_async --- docs/features/hooks.md | 37 +++++++++++++++++++--- src/django_idom/hooks.py | 67 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/docs/features/hooks.md b/docs/features/hooks.md index e1cc4d04..650b19d4 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -1,9 +1,39 @@ -# Django Hooks - ???+ tip "Looking for more hooks?" Check out the [IDOM Core docs](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html?highlight=hooks) on hooks! +## Use Sync to Async + +This is the suggested method of performing ORM queries when using Django IDOM. + +```python title="components.py" +from example_project.my_app.models import Category +from channels.db import database_sync_to_async +from idom import component, html +from django_idom import hooks + +@component +def simple_list(): + categories, set_categories = hooks.use_state(None) + + @hooks.use_sync_to_async + def get_categories(): + if categories: + return + set_categories(Category.objects.all()) + + if not categories: + return html.h2("Loading...") + + return html.ul( + [html.li(category.name, key=category.name) for category in categories] + ) +``` + +??? question "Why can't I make ORM calls without hooks?" + + Due to Django's ORM design, database queries must be deferred using hooks. Otherwise, you will see a `SynchronousOnlyOperation` exception. + ## Use Websocket You can fetch the Django Channels websocket at any time by using `use_websocket`. @@ -18,8 +48,6 @@ def MyComponent(): return html.div(my_websocket) ``` - - ## Use Scope This is a shortcut that returns the Websocket's `scope`. @@ -34,7 +62,6 @@ def MyComponent(): return html.div(my_scope) ``` - ## Use Location ??? info "This hook's behavior will be changed in a future update" diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 5d088223..25de3879 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -1,9 +1,26 @@ from dataclasses import dataclass -from typing import Awaitable, Callable, Dict, Optional, Type, Union +from typing import ( + Any, + Awaitable, + Callable, + Dict, + Optional, + Sequence, + Type, + Union, + overload, +) from idom.backend.types import Location -from idom.core.hooks import Context, create_context, use_context - +from idom.core.hooks import ( + Context, + _EffectApplyFunc, + create_context, + use_context, + use_effect, +) +from channels.db import database_sync_to_async +from inspect import iscoroutinefunction @dataclass class IdomWebsocket: @@ -37,3 +54,47 @@ def use_websocket() -> IdomWebsocket: if websocket is None: raise RuntimeError("No websocket. Are you running with a Django server?") return websocket + + +@overload +def use_sync_to_async( + function: None = None, + dependencies: Sequence[Any] | ellipsis | None = ..., +) -> Callable[[_EffectApplyFunc], None]: + ... + + +@overload +def use_sync_to_async( + function: _EffectApplyFunc, + dependencies: Sequence[Any] | ellipsis | None = ..., +) -> None: + ... + + +def use_sync_to_async( + function: Optional[_EffectApplyFunc] = None, + dependencies: Sequence[Any] | ellipsis | None = ..., +) -> Optional[Callable[[_EffectApplyFunc], None]]: + """This is a sync_to_async wrapper for `idom.hooks.use_effect`. + See the full :ref:`Use Effect` docs for details + + Parameters: + function: + Applies the effect and can return a clean-up function + dependencies: + Dependencies for the effect. The effect will only trigger if the identity + of any value in the given sequence changes (i.e. their :func:`id` is + different). By default these are inferred based on local variables that are + referenced by the given function. + + Returns: + If a function is not provided: + A decorator. + Otherwise: + `None` + """ + if function and iscoroutinefunction(function): + raise ValueError("use_sync_to_async cannot be used with async functions") + sync_to_async_function = database_sync_to_async(function) if function else None + return use_effect(sync_to_async_function, dependencies) From 44e76b43cc47be34a0547959fd23133040a0cd09 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 1 Jul 2022 20:28:14 -0700 Subject: [PATCH 2/4] fix flake8 warnings --- src/django_idom/hooks.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index 25de3879..c68cbca1 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -1,5 +1,7 @@ from dataclasses import dataclass +from inspect import iscoroutinefunction from typing import ( + TYPE_CHECKING, Any, Awaitable, Callable, @@ -11,6 +13,7 @@ overload, ) +from channels.db import database_sync_to_async from idom.backend.types import Location from idom.core.hooks import ( Context, @@ -19,8 +22,12 @@ use_context, use_effect, ) -from channels.db import database_sync_to_async -from inspect import iscoroutinefunction + + +if not TYPE_CHECKING: + # make flake8 think that this variable exists + ellipsis = type(...) + @dataclass class IdomWebsocket: From 6bb1ed877e3863e0085da63d9f61edcab1b9b20e Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 1 Jul 2022 20:33:03 -0700 Subject: [PATCH 3/4] Union Jack --- src/django_idom/hooks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/django_idom/hooks.py b/src/django_idom/hooks.py index c68cbca1..cb58dadb 100644 --- a/src/django_idom/hooks.py +++ b/src/django_idom/hooks.py @@ -66,7 +66,7 @@ def use_websocket() -> IdomWebsocket: @overload def use_sync_to_async( function: None = None, - dependencies: Sequence[Any] | ellipsis | None = ..., + dependencies: Union[Sequence[Any], ellipsis, None] = ..., ) -> Callable[[_EffectApplyFunc], None]: ... @@ -74,14 +74,14 @@ def use_sync_to_async( @overload def use_sync_to_async( function: _EffectApplyFunc, - dependencies: Sequence[Any] | ellipsis | None = ..., + dependencies: Union[Sequence[Any], ellipsis, None] = ..., ) -> None: ... def use_sync_to_async( function: Optional[_EffectApplyFunc] = None, - dependencies: Sequence[Any] | ellipsis | None = ..., + dependencies: Union[Sequence[Any], ellipsis, None] = ..., ) -> Optional[Callable[[_EffectApplyFunc], None]]: """This is a sync_to_async wrapper for `idom.hooks.use_effect`. See the full :ref:`Use Effect` docs for details From 32b089502c3100e6e8160c87a49cfe6a66c0dabb Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 1 Jul 2022 20:43:53 -0700 Subject: [PATCH 4/4] add use_effect note --- docs/features/hooks.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/features/hooks.md b/docs/features/hooks.md index 650b19d4..8318ee87 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -4,7 +4,9 @@ ## Use Sync to Async -This is the suggested method of performing ORM queries when using Django IDOM. +This is the suggested method of performing ORM queries when using Django IDOM. + +Fundamentally, this hook is an ORM-safe version of [use_effect](https://idom-docs.herokuapp.com/docs/reference/hooks-api.html#use-effect). ```python title="components.py" from example_project.my_app.models import Category