From bfc730ce1982434e0000265807cdf9697571b2e4 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 2 Feb 2024 21:08:43 -0800 Subject: [PATCH 1/4] Tweaks `use_channel_layer` to allow custom group channel names --- .../python/use-channel-layer-group.py | 6 +- src/reactpy_django/hooks.py | 92 +++++++++++++------ tests/test_app/channel_layers/components.py | 4 +- 3 files changed, 68 insertions(+), 34 deletions(-) diff --git a/docs/examples/python/use-channel-layer-group.py b/docs/examples/python/use-channel-layer-group.py index 2fde6bab..85a54ed5 100644 --- a/docs/examples/python/use-channel-layer-group.py +++ b/docs/examples/python/use-channel-layer-group.py @@ -4,7 +4,7 @@ @component def my_sender_component(): - sender = use_channel_layer("my-channel-name", group=True) + sender = use_channel_layer(group_name="my-group-name") async def submit_event(event): if event["key"] == "Enter": @@ -23,7 +23,7 @@ def my_receiver_component_1(): async def receive_event(message): set_message(message["text"]) - use_channel_layer("my-channel-name", receiver=receive_event, group=True) + use_channel_layer(receiver=receive_event, group_name="my-group-name") return html.div(f"Message Receiver 1: {message}") @@ -35,6 +35,6 @@ def my_receiver_component_2(): async def receive_event(message): set_message(message["text"]) - use_channel_layer("my-channel-name", receiver=receive_event, group=True) + use_channel_layer(receiver=receive_event, group_name="my-group-name") return html.div(f"Message Receiver 2: {message}") diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index 96da2d10..fbb7f452 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -46,9 +46,9 @@ _logger = logging.getLogger(__name__) -_REFETCH_CALLBACKS: DefaultDict[ - Callable[..., Any], set[Callable[[], None]] -] = DefaultDict(set) +_REFETCH_CALLBACKS: DefaultDict[Callable[..., Any], set[Callable[[], None]]] = ( + DefaultDict(set) +) # TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.* @@ -109,8 +109,7 @@ def use_query( query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred], *args: FuncParams.args, **kwargs: FuncParams.kwargs, -) -> Query[Inferred]: - ... +) -> Query[Inferred]: ... @overload @@ -118,8 +117,7 @@ def use_query( query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred], *args: FuncParams.args, **kwargs: FuncParams.kwargs, -) -> Query[Inferred]: - ... +) -> Query[Inferred]: ... def use_query(*args, **kwargs) -> Query[Inferred]: @@ -221,20 +219,20 @@ def register_refetch_callback() -> Callable[[], None]: @overload def use_mutation( options: MutationOptions, - mutation: Callable[FuncParams, bool | None] - | Callable[FuncParams, Awaitable[bool | None]], + mutation: ( + Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]] + ), refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None, -) -> Mutation[FuncParams]: - ... +) -> Mutation[FuncParams]: ... @overload def use_mutation( - mutation: Callable[FuncParams, bool | None] - | Callable[FuncParams, Awaitable[bool | None]], + mutation: ( + Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]] + ), refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None, -) -> Mutation[FuncParams]: - ... +) -> Mutation[FuncParams]: ... def use_mutation(*args: Any, **kwargs: Any) -> Mutation[FuncParams]: @@ -327,8 +325,9 @@ def use_user() -> AbstractUser: def use_user_data( - default_data: None - | dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any] = None, + default_data: ( + None | dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any] + ) = None, save_default_data: bool = False, ) -> UserData: """Get or set user data stored within the REACTPY_DATABASE. @@ -367,28 +366,62 @@ async def _set_user_data(data: dict): return UserData(query, mutation) +@overload +def use_channel_layer( + name: None, + *, + group_name: str, + group_add: bool, + group_discard: bool, + receiver: AsyncMessageReceiver | None, + layer: str, +) -> AsyncMessageSender: ... + + +@overload def use_channel_layer( name: str, + *, + group_name: str | None, + group_add: bool, + group_discard: bool, + receiver: AsyncMessageReceiver | None, + layer: str, +) -> AsyncMessageSender: ... + + +def use_channel_layer( + name: str | None = None, + *, + group_name: str | None = None, + group_add: bool = True, + group_discard: bool = True, receiver: AsyncMessageReceiver | None = None, - group: bool = False, layer: str = DEFAULT_CHANNEL_LAYER, ) -> AsyncMessageSender: """ Subscribe to a Django Channels layer to send/receive messages. Args: - name: The name of the channel to subscribe to. - receiver: An async function that receives a `message: dict` from the channel layer. \ - If more than one receiver waits on the same channel, a random one \ - will get the result (unless `group=True` is defined). - group: If `True`, a "group channel" will be used. Messages sent within a \ - group are broadcasted to all receivers on that channel. + name: The name of the channel to subscribe to. If you define a `group_name`, you \ + can keep this undefined to generate a random name. + group_name: If configured, any messages sent within this hook will be broadcasted \ + to all channels in this group. + group_add: If `True`, the channel will automatically be added to the group \ + when the component mounts. + group_discard: If `True`, the channel will automatically be removed from the \ + group when the component dismounts. + receiver: An async function that receives a `message: dict` from a channel. \ + If more than one receiver waits on the same channel name, a random receiver \ + will get the result. layer: The channel layer to use. These layers must be defined in \ `settings.py:CHANNEL_LAYERS`. """ channel_layer: InMemoryChannelLayer | RedisChannelLayer = get_channel_layer(layer) - channel_name = use_memo(lambda: str(uuid4() if group else name)) - group_name = name if group else "" + channel_name = use_memo(lambda: str(name or uuid4())) + + if not name or not group_name: + raise ValueError("You must define a `name` or `group_name` for the channel.") if not channel_layer: raise ValueError( @@ -399,9 +432,10 @@ def use_channel_layer( # Add/remove a group's channel during component mount/dismount respectively. @use_effect(dependencies=[]) async def group_manager(): - if group: + if group_name and group_add: await channel_layer.group_add(group_name, channel_name) + if group_name and group_discard: return lambda: asyncio.run( channel_layer.group_discard(group_name, channel_name) ) @@ -409,7 +443,7 @@ async def group_manager(): # Listen for messages on the channel using the provided `receiver` function. @use_effect async def message_receiver(): - if not receiver or not channel_name: + if not receiver: return while True: @@ -418,7 +452,7 @@ async def message_receiver(): # User interface for sending messages to the channel async def message_sender(message: dict): - if group: + if group_name: await channel_layer.group_send(group_name, message) else: await channel_layer.send(channel_name, message) diff --git a/tests/test_app/channel_layers/components.py b/tests/test_app/channel_layers/components.py index d9174b47..9d79bd79 100644 --- a/tests/test_app/channel_layers/components.py +++ b/tests/test_app/channel_layers/components.py @@ -40,7 +40,7 @@ def group_receiver(id: int): async def receiver(message): set_state(message["text"]) - use_channel_layer("group-messenger", receiver=receiver, group=True) + use_channel_layer("group-messenger", receiver=receiver, group_name="group") return html.div( {"id": f"group-receiver-{id}", "data-message": state}, @@ -50,7 +50,7 @@ async def receiver(message): @component def group_sender(): - sender = use_channel_layer("group-messenger", group=True) + sender = use_channel_layer("group-messenger", group_name="group") async def submit_event(event): if event["key"] == "Enter": From e1339e24d0066f789b5af74d52a14432ddd0f854 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 2 Feb 2024 21:50:50 -0800 Subject: [PATCH 2/4] new docs --- .../python/use-channel-layer-group.py | 4 +-- .../python/use-channel-layer-signal-sender.py | 5 ++- docs/src/reference/hooks.md | 31 +++++++++++-------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/docs/examples/python/use-channel-layer-group.py b/docs/examples/python/use-channel-layer-group.py index 85a54ed5..bcbabee6 100644 --- a/docs/examples/python/use-channel-layer-group.py +++ b/docs/examples/python/use-channel-layer-group.py @@ -23,7 +23,7 @@ def my_receiver_component_1(): async def receive_event(message): set_message(message["text"]) - use_channel_layer(receiver=receive_event, group_name="my-group-name") + use_channel_layer(group_name="my-group-name", receiver=receive_event) return html.div(f"Message Receiver 1: {message}") @@ -35,6 +35,6 @@ def my_receiver_component_2(): async def receive_event(message): set_message(message["text"]) - use_channel_layer(receiver=receive_event, group_name="my-group-name") + use_channel_layer(group_name="my-group-name", receiver=receive_event) return html.div(f"Message Receiver 2: {message}") diff --git a/docs/examples/python/use-channel-layer-signal-sender.py b/docs/examples/python/use-channel-layer-signal-sender.py index 4e12fb12..a35a6c88 100644 --- a/docs/examples/python/use-channel-layer-signal-sender.py +++ b/docs/examples/python/use-channel-layer-signal-sender.py @@ -5,8 +5,7 @@ from django.dispatch import receiver -class ExampleModel(Model): - ... +class ExampleModel(Model): ... @receiver(pre_save, sender=ExampleModel) @@ -17,4 +16,4 @@ def my_sender_signal(sender, instance, **kwargs): async_to_sync(layer.send)("my-channel-name", {"text": "Hello World!"}) # Example of sending a message to a group channel - async_to_sync(layer.group_send)("my-channel-name", {"text": "Hello World!"}) + async_to_sync(layer.group_send)("my-group-name", {"text": "Hello World!"}) diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index 96fe0616..5ceb2fe6 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -340,16 +340,18 @@ This is often used to create chat systems, synchronize data between components, | Name | Type | Description | Default | | --- | --- | --- | --- | - | `#!python name` | `#!python str` | The name of the channel to subscribe to. | N/A | - | `#!python receiver` | `#!python AsyncMessageReceiver | None` | An async function that receives a `#!python message: dict` from the channel layer. If more than one receiver waits on the same channel, a random one will get the result (unless `#!python group=True` is defined). | `#!python None` | - | `#!python group` | `#!python bool` | If `#!python True`, a "group channel" will be used. Messages sent within a group are broadcasted to all receivers on that channel. | `#!python False` | - | `#!python layer` | `#!python str` | The channel layer to use. These layers must be defined in `#!python settings.py:CHANNEL_LAYERS`. | `#!python 'default'` | + | `#!python name` | `#!python str | None` | The name of the channel to subscribe to. If you define a `#!python group_name`, you can keep `#!python name` undefined to auto-generate a unique name. | `#!python None` | + | `#!python group_name` | `#!python str | None` | If configured, any messages sent within this hook will be broadcasted to all channels in this group. | `#!python None` | + | `#!python group_add` | `#!python bool` | If `#!python True`, the channel will automatically be added to the group when the component mounts. | `#!python True` | + | `#!python group_discard` | `#!python bool` | If `#!python True`, the channel will automatically be removed from the group when the component dismounts. | `#!python True` | + | `#!python receiver` | `#!python AsyncMessageReceiver | None` | An async function that receives a `#!python message: dict` from a channel. If more than one receiver waits on the same channel name, a random receiver will get the result. | `#!python None` | + | `#!python layer` | `#!python str` | The channel layer to use. This layer must be defined in `#!python settings.py:CHANNEL_LAYERS`. | `#!python 'default'` | **Returns** | Type | Description | | --- | --- | - | `#!python AsyncMessageSender` | An async callable that can send a `#!python message: dict` | + | `#!python AsyncMessageSender` | An async callable that can send a `#!python message: dict`. | ??? warning "Extra Django configuration required" @@ -359,13 +361,15 @@ This is often used to create chat systems, synchronize data between components, In summary, you will need to: - 1. Run the following command to install `channels-redis` in your Python environment. + 1. Install [`redis`](https://redis.io/download/) on your machine. + + 2. Run the following command to install `channels-redis` in your Python environment. ```bash linenums="0" pip install channels-redis ``` - 2. Configure your `settings.py` to use `RedisChannelLayer` as your layer backend. + 3. Configure your `settings.py` to use `RedisChannelLayer` as your layer backend. ```python linenums="0" CHANNEL_LAYERS = { @@ -380,9 +384,9 @@ This is often used to create chat systems, synchronize data between components, ??? question "How do I broadcast a message to multiple components?" - By default, if more than one receiver waits on the same channel, a random one will get the result. + If more than one receiver waits on the same channel, a random one will get the result. - However, by defining `#!python group=True` you can configure a "group channel", which will broadcast messages to all receivers. + To get around this, you can define a `#!python group_name` to broadcast messages to all channels within a specific group. If you do not define a channel `#!python name` while using groups, ReactPy will automatically generate a unique channel name for you. In the example below, all messages sent by the `#!python sender` component will be received by all `#!python receiver` components that exist (across every active client browser). @@ -400,15 +404,16 @@ This is often used to create chat systems, synchronize data between components, In the example below, the sender will send a signal every time `#!python ExampleModel` is saved. Then, when the receiver component gets this signal, it explicitly calls `#!python set_message(...)` to trigger a re-render. - === "components.py" + === "signals.py" ```python - {% include "../../examples/python/use-channel-layer-signal-receiver.py" %} + {% include "../../examples/python/use-channel-layer-signal-sender.py" %} ``` - === "signals.py" + + === "components.py" ```python - {% include "../../examples/python/use-channel-layer-signal-sender.py" %} + {% include "../../examples/python/use-channel-layer-signal-receiver.py" %} ``` --- From e3f005f2e0b19d2a7ac967f8a67e6d28c0f26574 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 2 Feb 2024 21:51:11 -0800 Subject: [PATCH 3/4] bug fixes --- src/reactpy_django/hooks.py | 6 +++--- tests/test_app/channel_layers/components.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index fbb7f452..c4b0b77a 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -404,7 +404,7 @@ def use_channel_layer( Args: name: The name of the channel to subscribe to. If you define a `group_name`, you \ - can keep this undefined to generate a random name. + can keep `name` undefined to auto-generate a unique name. group_name: If configured, any messages sent within this hook will be broadcasted \ to all channels in this group. group_add: If `True`, the channel will automatically be added to the group \ @@ -414,13 +414,13 @@ def use_channel_layer( receiver: An async function that receives a `message: dict` from a channel. \ If more than one receiver waits on the same channel name, a random receiver \ will get the result. - layer: The channel layer to use. These layers must be defined in \ + layer: The channel layer to use. This layer must be defined in \ `settings.py:CHANNEL_LAYERS`. """ channel_layer: InMemoryChannelLayer | RedisChannelLayer = get_channel_layer(layer) channel_name = use_memo(lambda: str(name or uuid4())) - if not name or not group_name: + if not name and not group_name: raise ValueError("You must define a `name` or `group_name` for the channel.") if not channel_layer: diff --git a/tests/test_app/channel_layers/components.py b/tests/test_app/channel_layers/components.py index 9d79bd79..4f40a248 100644 --- a/tests/test_app/channel_layers/components.py +++ b/tests/test_app/channel_layers/components.py @@ -40,7 +40,7 @@ def group_receiver(id: int): async def receiver(message): set_state(message["text"]) - use_channel_layer("group-messenger", receiver=receiver, group_name="group") + use_channel_layer(receiver=receiver, group_name="group-messenger") return html.div( {"id": f"group-receiver-{id}", "data-message": state}, @@ -50,7 +50,7 @@ async def receiver(message): @component def group_sender(): - sender = use_channel_layer("group-messenger", group_name="group") + sender = use_channel_layer(group_name="group-messenger") async def submit_event(event): if event["key"] == "Enter": From b3f94f961189b1c12ccbc72c56f06757634e19b8 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Fri, 2 Feb 2024 22:04:25 -0800 Subject: [PATCH 4/4] fix type hints --- src/reactpy_django/hooks.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index c4b0b77a..de33b8ea 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -366,30 +366,6 @@ async def _set_user_data(data: dict): return UserData(query, mutation) -@overload -def use_channel_layer( - name: None, - *, - group_name: str, - group_add: bool, - group_discard: bool, - receiver: AsyncMessageReceiver | None, - layer: str, -) -> AsyncMessageSender: ... - - -@overload -def use_channel_layer( - name: str, - *, - group_name: str | None, - group_add: bool, - group_discard: bool, - receiver: AsyncMessageReceiver | None, - layer: str, -) -> AsyncMessageSender: ... - - def use_channel_layer( name: str | None = None, *, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy