Skip to content

Commit 2dde58c

Browse files
authored
Tweak use_channel_layer to allow custom group names (#225)
Minor follow up to #221 PR to tweak the user API.
1 parent 96a9f93 commit 2dde58c

File tree

5 files changed

+66
-52
lines changed

5 files changed

+66
-52
lines changed

Diff for: docs/examples/python/use-channel-layer-group.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
@component
66
def my_sender_component():
7-
sender = use_channel_layer("my-channel-name", group=True)
7+
sender = use_channel_layer(group_name="my-group-name")
88

99
async def submit_event(event):
1010
if event["key"] == "Enter":
@@ -23,7 +23,7 @@ def my_receiver_component_1():
2323
async def receive_event(message):
2424
set_message(message["text"])
2525

26-
use_channel_layer("my-channel-name", receiver=receive_event, group=True)
26+
use_channel_layer(group_name="my-group-name", receiver=receive_event)
2727

2828
return html.div(f"Message Receiver 1: {message}")
2929

@@ -35,6 +35,6 @@ def my_receiver_component_2():
3535
async def receive_event(message):
3636
set_message(message["text"])
3737

38-
use_channel_layer("my-channel-name", receiver=receive_event, group=True)
38+
use_channel_layer(group_name="my-group-name", receiver=receive_event)
3939

4040
return html.div(f"Message Receiver 2: {message}")

Diff for: docs/examples/python/use-channel-layer-signal-sender.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
from django.dispatch import receiver
66

77

8-
class ExampleModel(Model):
9-
...
8+
class ExampleModel(Model): ...
109

1110

1211
@receiver(pre_save, sender=ExampleModel)
@@ -17,4 +16,4 @@ def my_sender_signal(sender, instance, **kwargs):
1716
async_to_sync(layer.send)("my-channel-name", {"text": "Hello World!"})
1817

1918
# Example of sending a message to a group channel
20-
async_to_sync(layer.group_send)("my-channel-name", {"text": "Hello World!"})
19+
async_to_sync(layer.group_send)("my-group-name", {"text": "Hello World!"})

Diff for: docs/src/reference/hooks.md

+18-13
Original file line numberDiff line numberDiff line change
@@ -340,16 +340,18 @@ This is often used to create chat systems, synchronize data between components,
340340

341341
| Name | Type | Description | Default |
342342
| --- | --- | --- | --- |
343-
| `#!python name` | `#!python str` | The name of the channel to subscribe to. | N/A |
344-
| `#!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` |
345-
| `#!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` |
346-
| `#!python layer` | `#!python str` | The channel layer to use. These layers must be defined in `#!python settings.py:CHANNEL_LAYERS`. | `#!python 'default'` |
343+
| `#!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` |
344+
| `#!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` |
345+
| `#!python group_add` | `#!python bool` | If `#!python True`, the channel will automatically be added to the group when the component mounts. | `#!python True` |
346+
| `#!python group_discard` | `#!python bool` | If `#!python True`, the channel will automatically be removed from the group when the component dismounts. | `#!python True` |
347+
| `#!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` |
348+
| `#!python layer` | `#!python str` | The channel layer to use. This layer must be defined in `#!python settings.py:CHANNEL_LAYERS`. | `#!python 'default'` |
347349

348350
<font size="4">**Returns**</font>
349351

350352
| Type | Description |
351353
| --- | --- |
352-
| `#!python AsyncMessageSender` | An async callable that can send a `#!python message: dict` |
354+
| `#!python AsyncMessageSender` | An async callable that can send a `#!python message: dict`. |
353355

354356
??? warning "Extra Django configuration required"
355357

@@ -359,13 +361,15 @@ This is often used to create chat systems, synchronize data between components,
359361

360362
In summary, you will need to:
361363

362-
1. Run the following command to install `channels-redis` in your Python environment.
364+
1. Install [`redis`](https://redis.io/download/) on your machine.
365+
366+
2. Run the following command to install `channels-redis` in your Python environment.
363367

364368
```bash linenums="0"
365369
pip install channels-redis
366370
```
367371

368-
2. Configure your `settings.py` to use `RedisChannelLayer` as your layer backend.
372+
3. Configure your `settings.py` to use `RedisChannelLayer` as your layer backend.
369373

370374
```python linenums="0"
371375
CHANNEL_LAYERS = {
@@ -380,9 +384,9 @@ This is often used to create chat systems, synchronize data between components,
380384

381385
??? question "How do I broadcast a message to multiple components?"
382386

383-
By default, if more than one receiver waits on the same channel, a random one will get the result.
387+
If more than one receiver waits on the same channel, a random one will get the result.
384388

385-
However, by defining `#!python group=True` you can configure a "group channel", which will broadcast messages to all receivers.
389+
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.
386390

387391
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).
388392

@@ -400,15 +404,16 @@ This is often used to create chat systems, synchronize data between components,
400404

401405
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.
402406

403-
=== "components.py"
407+
=== "signals.py"
404408

405409
```python
406-
{% include "../../examples/python/use-channel-layer-signal-receiver.py" %}
410+
{% include "../../examples/python/use-channel-layer-signal-sender.py" %}
407411
```
408-
=== "signals.py"
412+
413+
=== "components.py"
409414

410415
```python
411-
{% include "../../examples/python/use-channel-layer-signal-sender.py" %}
416+
{% include "../../examples/python/use-channel-layer-signal-receiver.py" %}
412417
```
413418

414419
---

Diff for: src/reactpy_django/hooks.py

+41-31
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@
4646

4747

4848
_logger = logging.getLogger(__name__)
49-
_REFETCH_CALLBACKS: DefaultDict[
50-
Callable[..., Any], set[Callable[[], None]]
51-
] = DefaultDict(set)
49+
_REFETCH_CALLBACKS: DefaultDict[Callable[..., Any], set[Callable[[], None]]] = (
50+
DefaultDict(set)
51+
)
5252

5353

5454
# TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.*
@@ -109,17 +109,15 @@ def use_query(
109109
query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred],
110110
*args: FuncParams.args,
111111
**kwargs: FuncParams.kwargs,
112-
) -> Query[Inferred]:
113-
...
112+
) -> Query[Inferred]: ...
114113

115114

116115
@overload
117116
def use_query(
118117
query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred],
119118
*args: FuncParams.args,
120119
**kwargs: FuncParams.kwargs,
121-
) -> Query[Inferred]:
122-
...
120+
) -> Query[Inferred]: ...
123121

124122

125123
def use_query(*args, **kwargs) -> Query[Inferred]:
@@ -221,20 +219,20 @@ def register_refetch_callback() -> Callable[[], None]:
221219
@overload
222220
def use_mutation(
223221
options: MutationOptions,
224-
mutation: Callable[FuncParams, bool | None]
225-
| Callable[FuncParams, Awaitable[bool | None]],
222+
mutation: (
223+
Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]
224+
),
226225
refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None,
227-
) -> Mutation[FuncParams]:
228-
...
226+
) -> Mutation[FuncParams]: ...
229227

230228

231229
@overload
232230
def use_mutation(
233-
mutation: Callable[FuncParams, bool | None]
234-
| Callable[FuncParams, Awaitable[bool | None]],
231+
mutation: (
232+
Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]
233+
),
235234
refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None,
236-
) -> Mutation[FuncParams]:
237-
...
235+
) -> Mutation[FuncParams]: ...
238236

239237

240238
def use_mutation(*args: Any, **kwargs: Any) -> Mutation[FuncParams]:
@@ -327,8 +325,9 @@ def use_user() -> AbstractUser:
327325

328326

329327
def use_user_data(
330-
default_data: None
331-
| dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any] = None,
328+
default_data: (
329+
None | dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any]
330+
) = None,
332331
save_default_data: bool = False,
333332
) -> UserData:
334333
"""Get or set user data stored within the REACTPY_DATABASE.
@@ -368,27 +367,37 @@ async def _set_user_data(data: dict):
368367

369368

370369
def use_channel_layer(
371-
name: str,
370+
name: str | None = None,
371+
*,
372+
group_name: str | None = None,
373+
group_add: bool = True,
374+
group_discard: bool = True,
372375
receiver: AsyncMessageReceiver | None = None,
373-
group: bool = False,
374376
layer: str = DEFAULT_CHANNEL_LAYER,
375377
) -> AsyncMessageSender:
376378
"""
377379
Subscribe to a Django Channels layer to send/receive messages.
378380
379381
Args:
380-
name: The name of the channel to subscribe to.
381-
receiver: An async function that receives a `message: dict` from the channel layer. \
382-
If more than one receiver waits on the same channel, a random one \
383-
will get the result (unless `group=True` is defined).
384-
group: If `True`, a "group channel" will be used. Messages sent within a \
385-
group are broadcasted to all receivers on that channel.
386-
layer: The channel layer to use. These layers must be defined in \
382+
name: The name of the channel to subscribe to. If you define a `group_name`, you \
383+
can keep `name` undefined to auto-generate a unique name.
384+
group_name: If configured, any messages sent within this hook will be broadcasted \
385+
to all channels in this group.
386+
group_add: If `True`, the channel will automatically be added to the group \
387+
when the component mounts.
388+
group_discard: If `True`, the channel will automatically be removed from the \
389+
group when the component dismounts.
390+
receiver: An async function that receives a `message: dict` from a channel. \
391+
If more than one receiver waits on the same channel name, a random receiver \
392+
will get the result.
393+
layer: The channel layer to use. This layer must be defined in \
387394
`settings.py:CHANNEL_LAYERS`.
388395
"""
389396
channel_layer: InMemoryChannelLayer | RedisChannelLayer = get_channel_layer(layer)
390-
channel_name = use_memo(lambda: str(uuid4() if group else name))
391-
group_name = name if group else ""
397+
channel_name = use_memo(lambda: str(name or uuid4()))
398+
399+
if not name and not group_name:
400+
raise ValueError("You must define a `name` or `group_name` for the channel.")
392401

393402
if not channel_layer:
394403
raise ValueError(
@@ -399,17 +408,18 @@ def use_channel_layer(
399408
# Add/remove a group's channel during component mount/dismount respectively.
400409
@use_effect(dependencies=[])
401410
async def group_manager():
402-
if group:
411+
if group_name and group_add:
403412
await channel_layer.group_add(group_name, channel_name)
404413

414+
if group_name and group_discard:
405415
return lambda: asyncio.run(
406416
channel_layer.group_discard(group_name, channel_name)
407417
)
408418

409419
# Listen for messages on the channel using the provided `receiver` function.
410420
@use_effect
411421
async def message_receiver():
412-
if not receiver or not channel_name:
422+
if not receiver:
413423
return
414424

415425
while True:
@@ -418,7 +428,7 @@ async def message_receiver():
418428

419429
# User interface for sending messages to the channel
420430
async def message_sender(message: dict):
421-
if group:
431+
if group_name:
422432
await channel_layer.group_send(group_name, message)
423433
else:
424434
await channel_layer.send(channel_name, message)

Diff for: tests/test_app/channel_layers/components.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def group_receiver(id: int):
4040
async def receiver(message):
4141
set_state(message["text"])
4242

43-
use_channel_layer("group-messenger", receiver=receiver, group=True)
43+
use_channel_layer(receiver=receiver, group_name="group-messenger")
4444

4545
return html.div(
4646
{"id": f"group-receiver-{id}", "data-message": state},
@@ -50,7 +50,7 @@ async def receiver(message):
5050

5151
@component
5252
def group_sender():
53-
sender = use_channel_layer("group-messenger", group=True)
53+
sender = use_channel_layer(group_name="group-messenger")
5454

5555
async def submit_event(event):
5656
if event["key"] == "Enter":

0 commit comments

Comments
 (0)