Skip to content

Commit 2a17e1d

Browse files
committed
Move broadcast() to the server module.
Rationale: * It is only useful for servers. (Maybe there's a use case for client but I couldn't picture it.) * It was already documented in the page covering the server module. * Within this page, it was the only API from the connection or protocol module. The implementation remains in the connection or protocol module because moving it would require refactoring tests. I'd rather keep them simple. (And I'm lazy.) This change doesn't require a backwards compatibility shim because the documentated location of the legacy implementation of broadcast was websockets.broadcast, it's changing with the introduction of the new asyncio API, and the changes are already documented.
1 parent 14ca557 commit 2a17e1d

File tree

23 files changed

+96
-63
lines changed

23 files changed

+96
-63
lines changed

docs/faq/server.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,9 @@ Record all connections in a global variable::
116116
finally:
117117
CONNECTIONS.remove(websocket)
118118

119-
Then, call :func:`~websockets.asyncio.connection.broadcast`::
119+
Then, call :func:`~websockets.asyncio.server.broadcast`::
120120

121-
from websockets.asyncio.connection import broadcast
121+
from websockets.asyncio.server import broadcast
122122

123123
def message_all(message):
124124
broadcast(CONNECTIONS, message)

docs/howto/patterns.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ connect and unregister them when they disconnect::
9090
connected.add(websocket)
9191
try:
9292
# Broadcast a message to all connected clients.
93-
websockets.broadcast(connected, "Hello!")
93+
broadcast(connected, "Hello!")
9494
await asyncio.sleep(10)
9595
finally:
9696
# Unregister.

docs/howto/upgrade.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ Server APIs
162162
| ``websockets.server.WebSocketServerProtocol`` |br| | |
163163
| :class:`websockets.legacy.server.WebSocketServerProtocol` | |
164164
+-------------------------------------------------------------------+-----------------------------------------------------+
165-
| ``websockets.broadcast`` |br| | :func:`websockets.asyncio.connection.broadcast` |
166-
| :func:`websockets.legacy.protocol.broadcast()` | |
165+
| ``websockets.broadcast`` |br| | :func:`websockets.asyncio.server.broadcast` |
166+
| :func:`websockets.legacy.server.broadcast()` | |
167167
+-------------------------------------------------------------------+-----------------------------------------------------+
168168
| ``websockets.BasicAuthWebSocketServerProtocol`` |br| | *not available yet* |
169169
| ``websockets.auth.BasicAuthWebSocketServerProtocol`` |br| | |

docs/intro/tutorial2.rst

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -482,11 +482,11 @@ you're using this pattern:
482482
...
483483
484484
Since this is a very common pattern in WebSocket servers, websockets provides
485-
the :func:`~asyncio.connection.broadcast` helper for this purpose:
485+
the :func:`~asyncio.server.broadcast` helper for this purpose:
486486

487487
.. code-block:: python
488488
489-
from websockets.asyncio.connection import broadcast
489+
from websockets.asyncio.server import broadcast
490490
491491
async def handler(websocket):
492492
@@ -496,13 +496,12 @@ the :func:`~asyncio.connection.broadcast` helper for this purpose:
496496
497497
...
498498
499-
Calling :func:`~asyncio.connection.broadcast` once is more efficient than
499+
Calling :func:`~asyncio.server.broadcast` once is more efficient than
500500
calling :meth:`~asyncio.server.ServerConnection.send` in a loop.
501501

502502
However, there's a subtle difference in behavior. Did you notice that there's no
503-
``await`` in the second version? Indeed, :func:`~asyncio.connection.broadcast`
504-
is a function, not a coroutine like
505-
:meth:`~asyncio.server.ServerConnection.send` or
503+
``await`` in the second version? Indeed, :func:`~asyncio.server.broadcast` is a
504+
function, not a coroutine like :meth:`~asyncio.server.ServerConnection.send` or
506505
:meth:`~asyncio.server.ServerConnection.recv`.
507506

508507
It's quite obvious why :meth:`~asyncio.server.ServerConnection.recv`
@@ -524,8 +523,8 @@ That said, when you're sending the same messages to many clients in a loop,
524523
applying backpressure in this way can become counterproductive. When you're
525524
broadcasting, you don't want to slow down everyone to the pace of the slowest
526525
clients; you want to drop clients that cannot keep up with the data stream.
527-
That's why :func:`~asyncio.connection.broadcast` doesn't wait until write
528-
buffers drain and therefore doesn't need to be a coroutine.
526+
That's why :func:`~asyncio.server.broadcast` doesn't wait until write buffers
527+
drain and therefore doesn't need to be a coroutine.
529528

530529
For our Connect Four game, there's no difference in practice. The total amount
531530
of data sent on a connection for a game of Connect Four is so small that the

docs/project/changelog.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ Improvements
212212

213213
* Added platform-independent wheels.
214214

215-
* Improved error handling in :func:`~legacy.protocol.broadcast`.
215+
* Improved error handling in :func:`~legacy.server.broadcast`.
216216

217217
* Set ``server_hostname`` automatically on TLS connections when providing a
218218
``sock`` argument to :func:`~sync.client.connect`.
@@ -402,7 +402,7 @@ New features
402402

403403
* Added compatibility with Python 3.10.
404404

405-
* Added :func:`~legacy.protocol.broadcast` to send a message to many clients.
405+
* Added :func:`~legacy.server.broadcast` to send a message to many clients.
406406

407407
* Added support for reconnecting automatically by using
408408
:func:`~legacy.client.connect` as an asynchronous iterator.

docs/reference/asyncio/server.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,4 @@ Using a connection
8080
Broadcast
8181
---------
8282

83-
.. autofunction:: websockets.asyncio.connection.broadcast
83+
.. autofunction:: websockets.asyncio.server.broadcast

docs/reference/features.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ Both sides
3535
+------------------------------------+--------+--------+--------+--------+
3636
| Send a message |||||
3737
+------------------------------------+--------+--------+--------+--------+
38+
| Broadcast a message |||||
39+
+------------------------------------+--------+--------+--------+--------+
3840
| Receive a message |||||
3941
+------------------------------------+--------+--------+--------+--------+
4042
| Iterate over received messages |||||

docs/reference/legacy/server.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ Using a connection
8989
.. autoproperty:: close_reason
9090

9191

92+
Broadcast
93+
---------
94+
95+
.. autofunction:: websockets.legacy.server.broadcast
96+
9297
Basic authentication
9398
--------------------
9499

@@ -106,8 +111,3 @@ websockets supports HTTP Basic Authentication according to
106111
.. autoattribute:: username
107112

108113
.. automethod:: check_credentials
109-
110-
Broadcast
111-
---------
112-
113-
.. autofunction:: websockets.legacy.protocol.broadcast

docs/topics/broadcast.rst

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@ Broadcasting
44
.. currentmodule:: websockets
55

66
.. admonition:: If you want to send a message to all connected clients,
7-
use :func:`~asyncio.connection.broadcast`.
7+
use :func:`~asyncio.server.broadcast`.
88
:class: tip
99

1010
If you want to learn about its design, continue reading this document.
1111

1212
For the legacy :mod:`asyncio` implementation, use
13-
:func:`~legacy.protocol.broadcast`.
13+
:func:`~legacy.server.broadcast`.
1414

1515
WebSocket servers often send the same message to all connected clients or to a
1616
subset of clients for which the message is relevant.
1717

1818
Let's explore options for broadcasting a message, explain the design of
19-
:func:`~asyncio.connection.broadcast`, and discuss alternatives.
19+
:func:`~asyncio.server.broadcast`, and discuss alternatives.
2020

2121
For each option, we'll provide a connection handler called ``handler()`` and a
2222
function or coroutine called ``broadcast()`` that sends a message to all
@@ -124,7 +124,7 @@ connections before the write buffer can fill up.
124124

125125
Don't set extreme values of ``write_limit``, ``ping_interval``, or
126126
``ping_timeout`` to ensure that this condition holds! Instead, set reasonable
127-
values and use the built-in :func:`~asyncio.connection.broadcast` function.
127+
values and use the built-in :func:`~asyncio.server.broadcast` function.
128128

129129
The concurrent way
130130
------------------
@@ -209,11 +209,11 @@ If a client gets too far behind, eventually it reaches the limit defined by
209209
``ping_timeout`` and websockets terminates the connection. You can refer to the
210210
discussion of :doc:`keepalive <keepalive>` for details.
211211

212-
How :func:`~asyncio.connection.broadcast` works
213-
-----------------------------------------------
212+
How :func:`~asyncio.server.broadcast` works
213+
-------------------------------------------
214214

215-
The built-in :func:`~asyncio.connection.broadcast` function is similar to the
216-
naive way. The main difference is that it doesn't apply backpressure.
215+
The built-in :func:`~asyncio.server.broadcast` function is similar to the naive
216+
way. The main difference is that it doesn't apply backpressure.
217217

218218
This provides the best performance by avoiding the overhead of scheduling and
219219
running one task per client.
@@ -324,7 +324,7 @@ the asynchronous iterator returned by ``subscribe()``.
324324
Performance considerations
325325
--------------------------
326326

327-
The built-in :func:`~asyncio.connection.broadcast` function sends all messages
327+
The built-in :func:`~asyncio.server.broadcast` function sends all messages
328328
without yielding control to the event loop. So does the naive way when the
329329
network and clients are fast and reliable.
330330

@@ -346,7 +346,7 @@ However, this isn't possible in general for two reasons:
346346

347347
All other patterns discussed above yield control to the event loop once per
348348
client because messages are sent by different tasks. This makes them slower
349-
than the built-in :func:`~asyncio.connection.broadcast` function.
349+
than the built-in :func:`~asyncio.server.broadcast` function.
350350

351351
There is no major difference between the performance of per-client queues and
352352
publish–subscribe.

docs/topics/logging.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ Here's what websockets logs at each level.
221221
``WARNING``
222222
...........
223223

224-
* Failures in :func:`~asyncio.connection.broadcast`
224+
* Failures in :func:`~asyncio.server.broadcast`
225225

226226
``INFO``
227227
........

docs/topics/performance.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ application.)
1818
broadcast
1919
---------
2020

21-
:func:`~asyncio.connection.broadcast` is the most efficient way to send a
22-
message to many clients.
21+
:func:`~asyncio.server.broadcast` is the most efficient way to send a message to
22+
many clients.

example/django/notifications.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010

1111
from django.contrib.contenttypes.models import ContentType
1212
from sesame.utils import get_user
13-
from websockets.asyncio.connection import broadcast
14-
from websockets.asyncio.server import serve
13+
from websockets.asyncio.server import broadcast, serve
1514
from websockets.frames import CloseCode
1615

1716

example/quickstart/counter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import asyncio
44
import json
55
import logging
6-
from websockets.asyncio.connection import broadcast
7-
from websockets.asyncio.server import serve
6+
from websockets.asyncio.server import broadcast, serve
87

98
logging.basicConfig()
109

example/quickstart/show_time_2.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
import datetime
55
import random
66

7-
from websockets.asyncio.connection import broadcast
8-
from websockets.asyncio.server import serve
7+
from websockets.asyncio.server import broadcast, serve
98

109
CONNECTIONS = set()
1110

example/tutorial/step2/app.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
import json
55
import secrets
66

7-
from websockets.asyncio.connection import broadcast
8-
from websockets.asyncio.server import serve
7+
from websockets.asyncio.server import broadcast, serve
98

109
from connect4 import PLAYER1, PLAYER2, Connect4
1110

example/tutorial/step3/app.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import secrets
77
import signal
88

9-
from websockets.asyncio.connection import broadcast
10-
from websockets.asyncio.server import serve
9+
from websockets.asyncio.server import broadcast, serve
1110

1211
from connect4 import PLAYER1, PLAYER2, Connect4
1312

experiments/broadcast/server.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
import time
88

99
from websockets import ConnectionClosed
10-
from websockets.asyncio.server import serve
11-
from websockets.asyncio.connection import broadcast
10+
from websockets.asyncio.server import broadcast, serve
1211

1312

1413
CLIENTS = set()

src/websockets/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848
"unix_connect",
4949
# .legacy.protocol
5050
"WebSocketCommonProtocol",
51-
"broadcast",
5251
# .legacy.server
5352
"WebSocketServer",
5453
"WebSocketServerProtocol",
54+
"broadcast",
5555
"serve",
5656
"unix_serve",
5757
# .server
@@ -102,10 +102,11 @@
102102
basic_auth_protocol_factory,
103103
)
104104
from .legacy.client import WebSocketClientProtocol, connect, unix_connect
105-
from .legacy.protocol import WebSocketCommonProtocol, broadcast
105+
from .legacy.protocol import WebSocketCommonProtocol
106106
from .legacy.server import (
107107
WebSocketServer,
108108
WebSocketServerProtocol,
109+
broadcast,
109110
serve,
110111
unix_serve,
111112
)
@@ -164,10 +165,10 @@
164165
"unix_connect": ".legacy.client",
165166
# .legacy.protocol
166167
"WebSocketCommonProtocol": ".legacy.protocol",
167-
"broadcast": ".legacy.protocol",
168168
# .legacy.server
169169
"WebSocketServer": ".legacy.server",
170170
"WebSocketServerProtocol": ".legacy.server",
171+
"broadcast": ".legacy.server",
171172
"serve": ".legacy.server",
172173
"unix_serve": ".legacy.server",
173174
# .server

src/websockets/asyncio/connection.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from .messages import Assembler
3535

3636

37-
__all__ = ["Connection", "broadcast"]
37+
__all__ = ["Connection"]
3838

3939

4040
class Connection(asyncio.Protocol):
@@ -1011,6 +1011,12 @@ def eof_received(self) -> None:
10111011
# Besides, that doesn't work on TLS connections.
10121012

10131013

1014+
# broadcast() is defined in the connection module even though it's primarily
1015+
# used by servers and documented in the server module because it works with
1016+
# client connections too and because it's easier to test together with the
1017+
# Connection class.
1018+
1019+
10141020
def broadcast(
10151021
connections: Iterable[Connection],
10161022
message: Data,
@@ -1034,10 +1040,11 @@ def broadcast(
10341040
``ping_interval`` and ``ping_timeout`` low to prevent excessive memory usage
10351041
from slow connections.
10361042
1037-
Unlike :meth:`~Connection.send`, :func:`broadcast` doesn't support sending
1038-
fragmented messages. Indeed, fragmentation is useful for sending large
1039-
messages without buffering them in memory, while :func:`broadcast` buffers
1040-
one copy per connection as fast as possible.
1043+
Unlike :meth:`~websockets.asyncio.connection.Connection.send`,
1044+
:func:`broadcast` doesn't support sending fragmented messages. Indeed,
1045+
fragmentation is useful for sending large messages without buffering them in
1046+
memory, while :func:`broadcast` buffers one copy per connection as fast as
1047+
possible.
10411048
10421049
:func:`broadcast` skips connections that aren't open in order to avoid
10431050
errors on connections where the closing handshake is in progress.
@@ -1047,6 +1054,10 @@ def broadcast(
10471054
set ``raise_exceptions`` to :obj:`True` to record failures and raise all
10481055
exceptions in a :pep:`654` :exc:`ExceptionGroup`.
10491056
1057+
While :func:`broadcast` makes more sense for servers, it works identically
1058+
with clients, if you have a use case for opening connections to many servers
1059+
and broadcasting a message to them.
1060+
10501061
Args:
10511062
websockets: WebSocket connections to which the message will be sent.
10521063
message: Message to send.
@@ -1101,3 +1112,7 @@ def broadcast(
11011112

11021113
if raise_exceptions and exceptions:
11031114
raise ExceptionGroup("skipped broadcast", exceptions)
1115+
1116+
1117+
# Pretend that broadcast is actually defined in the server module.
1118+
broadcast.__module__ = "websockets.asyncio.server"

src/websockets/asyncio/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
from ..server import ServerProtocol
2626
from ..typing import LoggerLike, Origin, StatusLike, Subprotocol
2727
from .compatibility import asyncio_timeout
28-
from .connection import Connection
28+
from .connection import Connection, broadcast
2929

3030

31-
__all__ = ["serve", "unix_serve", "ServerConnection", "WebSocketServer"]
31+
__all__ = ["broadcast", "serve", "unix_serve", "ServerConnection", "WebSocketServer"]
3232

3333

3434
class ServerConnection(Connection):

0 commit comments

Comments
 (0)