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, *,