Skip to content

Commit 2715b35

Browse files
committed
Align the asyncio and sync connection modules.
1 parent 759b2a2 commit 2715b35

File tree

2 files changed

+101
-98
lines changed

2 files changed

+101
-98
lines changed

src/websockets/asyncio/connection.py

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -100,19 +100,19 @@ def __init__(
100100
# Deadline for the closing handshake.
101101
self.close_deadline: float | None = None
102102

103-
# Protect sending fragmented messages.
103+
# Whether we are busy sending a fragmented message.
104104
self.send_in_progress: asyncio.Future[None] | None = None
105105

106106
# Mapping of ping IDs to pong waiters, in chronological order.
107107
self.pending_pings: dict[bytes, tuple[asyncio.Future[float], float]] = {}
108108

109-
self.latency: float = 0
109+
self.latency: float = 0.0
110110
"""
111111
Latency of the connection, in seconds.
112112
113113
Latency is defined as the round-trip time of the connection. It is
114114
measured by sending a Ping frame and waiting for a matching Pong frame.
115-
Before the first measurement, :attr:`latency` is ``0``.
115+
Before the first measurement, :attr:`latency` is ``0.0``.
116116
117117
By default, websockets enables a :ref:`keepalive <keepalive>` mechanism
118118
that sends Ping frames automatically at regular intervals. You can also
@@ -130,7 +130,7 @@ def __init__(
130130
# connection state becomes CLOSED.
131131
self.connection_lost_waiter: asyncio.Future[None] = self.loop.create_future()
132132

133-
# Adapted from asyncio.FlowControlMixin
133+
# Adapted from asyncio.FlowControlMixin.
134134
self.paused: bool = False
135135
self.drain_waiters: collections.deque[asyncio.Future[None]] = (
136136
collections.deque()
@@ -291,9 +291,9 @@ async def recv(self, decode: bool | None = None) -> Data:
291291
return a bytestring (:class:`bytes`). This improves performance
292292
when decoding isn't needed, for example if the message contains
293293
JSON and you're using a JSON library that expects a bytestring.
294-
* Set ``decode=True`` to force UTF-8 decoding of Binary_ frames
295-
and return a string (:class:`str`). This may be useful for
296-
servers that send binary frames instead of text frames.
294+
* Set ``decode=True`` to force UTF-8 decoding of Binary_ frames and
295+
return strings (:class:`str`). This may be useful for servers that
296+
send binary frames instead of text frames.
297297
298298
Raises:
299299
ConnectionClosed: When the connection is closed.
@@ -363,12 +363,12 @@ async def recv_streaming(self, decode: bool | None = None) -> AsyncIterator[Data
363363
364364
You may override this behavior with the ``decode`` argument:
365365
366-
* Set ``decode=False`` to disable UTF-8 decoding of Text_ frames
367-
and return bytestrings (:class:`bytes`). This may be useful to
368-
optimize performance when decoding isn't needed.
369-
* Set ``decode=True`` to force UTF-8 decoding of Binary_ frames
370-
and return strings (:class:`str`). This is useful for servers
371-
that send binary frames instead of text frames.
366+
* Set ``decode=False`` to disable UTF-8 decoding of Text_ frames and
367+
yield bytestrings (:class:`bytes`). This improves performance
368+
when decoding isn't needed.
369+
* Set ``decode=True`` to force UTF-8 decoding of Binary_ frames and
370+
yield strings (:class:`str`). This may be useful for servers that
371+
send binary frames instead of text frames.
372372
373373
Raises:
374374
ConnectionClosed: When the connection is closed.
@@ -417,16 +417,16 @@ async def send(
417417
418418
You may override this behavior with the ``text`` argument:
419419
420-
* Set ``text=True`` to send a bytestring or bytes-like object
421-
(:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) as a
420+
* Set ``text=True`` to send an UTF-8 bytestring or bytes-like object
421+
(:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) in a
422422
Text_ frame. This improves performance when the message is already
423423
UTF-8 encoded, for example if the message contains JSON and you're
424424
using a JSON library that produces a bytestring.
425425
* Set ``text=False`` to send a string (:class:`str`) in a Binary_
426426
frame. This may be useful for servers that expect binary frames
427427
instead of text frames.
428428
429-
:meth:`send` also accepts an iterable or an asynchronous iterable of
429+
:meth:`send` also accepts an iterable or asynchronous iterable of
430430
strings, bytestrings, or bytes-like objects to enable fragmentation_.
431431
Each item is treated as a message fragment and sent in its own frame.
432432
All items must be of the same type, or else :meth:`send` will raise a
@@ -441,8 +441,8 @@ async def send(
441441
Canceling :meth:`send` is discouraged. Instead, you should close the
442442
connection with :meth:`close`. Indeed, there are only two situations
443443
where :meth:`send` may yield control to the event loop and then get
444-
canceled; in both cases, :meth:`close` has the same effect and is
445-
more clear:
444+
canceled; in both cases, :meth:`close` has the same effect and the
445+
effect is more obvious:
446446
447447
1. The write buffer is full. If you don't want to wait until enough
448448
data is sent, your only alternative is to close the connection.
@@ -708,9 +708,10 @@ async def ping(self, data: DataLike | None = None) -> Awaitable[float]:
708708
data = struct.pack("!I", random.getrandbits(32))
709709

710710
pong_received = self.loop.create_future()
711+
ping_timestamp = self.loop.time()
711712
# The event loop's default clock is time.monotonic(). Its resolution
712713
# is a bit low on Windows (~16ms). This is improved in Python 3.13.
713-
self.pending_pings[data] = (pong_received, self.loop.time())
714+
self.pending_pings[data] = (pong_received, ping_timestamp)
714715
self.protocol.send_ping(data)
715716
return pong_received
716717

@@ -787,9 +788,7 @@ def acknowledge_pings(self, data: bytes) -> None:
787788

788789
def terminate_pending_pings(self) -> None:
789790
"""
790-
Raise ConnectionClosed in pending pings.
791-
792-
They'll never receive a pong once the connection is closed.
791+
Raise ConnectionClosed in pending pings when the connection is closed.
793792
794793
"""
795794
assert self.protocol.state is CLOSED
@@ -837,7 +836,8 @@ async def keepalive(self) -> None:
837836
# pong_received. A CancelledError is raised here,
838837
# not a ConnectionClosed exception.
839838
latency = await pong_received
840-
self.logger.debug("% received keepalive pong")
839+
if self.debug:
840+
self.logger.debug("% received keepalive pong")
841841
except asyncio.TimeoutError:
842842
if self.debug:
843843
self.logger.debug("- timed out waiting for keepalive pong")
@@ -908,20 +908,22 @@ async def send_context(
908908
# Check if the connection is expected to close soon.
909909
if self.protocol.close_expected():
910910
wait_for_close = True
911-
# If the connection is expected to close soon, set the
912-
# close deadline based on the close timeout.
913-
# Since we tested earlier that protocol.state was OPEN
911+
# Set the close deadline based on the close timeout.
912+
# Since we tested earlier that protocol.state is OPEN
914913
# (or CONNECTING), self.close_deadline is still None.
914+
assert self.close_deadline is None
915915
if self.close_timeout is not None:
916-
assert self.close_deadline is None
917916
self.close_deadline = self.loop.time() + self.close_timeout
918-
# Write outgoing data to the socket and enforce flow control.
917+
# Write outgoing data to the socket with flow control.
919918
try:
920919
self.send_data()
921920
await self.drain()
922921
except Exception as exc:
923922
if self.debug:
924-
self.logger.debug("! error while sending data", exc_info=True)
923+
self.logger.debug(
924+
"! error while sending data",
925+
exc_info=True,
926+
)
925927
# While the only expected exception here is OSError,
926928
# other exceptions would be treated identically.
927929
wait_for_close = False
@@ -933,8 +935,8 @@ async def send_context(
933935
# will be closing soon if it isn't in the expected state.
934936
wait_for_close = True
935937
# Calculate close_deadline if it wasn't set yet.
936-
if self.close_timeout is not None:
937-
if self.close_deadline is None:
938+
if self.close_deadline is None:
939+
if self.close_timeout is not None:
938940
self.close_deadline = self.loop.time() + self.close_timeout
939941
raise_close_exc = True
940942

@@ -945,7 +947,7 @@ async def send_context(
945947
async with asyncio_timeout_at(self.close_deadline):
946948
await asyncio.shield(self.connection_lost_waiter)
947949
except TimeoutError:
948-
# There's no risk to overwrite another error because
950+
# There's no risk of overwriting another error because
949951
# original_exc is never set when wait_for_close is True.
950952
assert original_exc is None
951953
original_exc = TimeoutError("timed out while closing connection")
@@ -966,9 +968,6 @@ def send_data(self) -> None:
966968
"""
967969
Send outgoing data.
968970
969-
Raises:
970-
OSError: When a socket operations fails.
971-
972971
"""
973972
for data in self.protocol.data_to_send():
974973
if data:
@@ -982,7 +981,7 @@ def send_data(self) -> None:
982981
# OSError is plausible. uvloop can raise RuntimeError here.
983982
try:
984983
self.transport.write_eof()
985-
except (OSError, RuntimeError): # pragma: no cover
984+
except Exception: # pragma: no cover
986985
pass
987986
# Else, close the TCP connection.
988987
else: # pragma: no cover
@@ -994,6 +993,8 @@ def set_recv_exc(self, exc: BaseException | None) -> None:
994993
"""
995994
Set recv_exc, if not set yet.
996995
996+
This method must be called only from connection callbacks.
997+
997998
"""
998999
if self.recv_exc is None:
9991000
self.recv_exc = exc
@@ -1096,26 +1097,29 @@ def data_received(self, data: bytes) -> None:
10961097
self.logger.debug("! error while sending data", exc_info=True)
10971098
self.set_recv_exc(exc)
10981099

1100+
# If needed, set the close deadline based on the close timeout.
10991101
if self.protocol.close_expected():
1100-
# If the connection is expected to close soon, set the
1101-
# close deadline based on the close timeout.
1102-
if self.close_timeout is not None:
1103-
if self.close_deadline is None:
1102+
if self.close_deadline is None:
1103+
if self.close_timeout is not None:
11041104
self.close_deadline = self.loop.time() + self.close_timeout
11051105

1106+
# If self.send_data raised an exception, then events are lost.
1107+
# Given that automatic responses write small amounts of data,
1108+
# this should be uncommon, so we don't handle the edge case.
1109+
11061110
for event in events:
11071111
# This isn't expected to raise an exception.
11081112
self.process_event(event)
11091113

11101114
def eof_received(self) -> None:
1111-
# Feed the end of the data stream to the connection.
1115+
# Feed the end of the data stream to the protocol.
11121116
self.protocol.receive_eof()
11131117

11141118
# This isn't expected to raise an exception.
11151119
events = self.protocol.events_received()
11161120

11171121
# There is no error handling because send_data() can only write
1118-
# the end of the data stream here and it shouldn't raise errors.
1122+
# the end of the data stream and it handles errors by itself.
11191123
self.send_data()
11201124

11211125
# This code path is triggered when receiving an HTTP response

0 commit comments

Comments
 (0)