Skip to content

Commit 73e8637

Browse files
gh-113812: Allow DatagramTransport.sendto to send empty data (#115199)
Also include the UDP packet header sizes (8 bytes per packet) in the buffer size reported to the flow control subsystem.
1 parent 8db8d71 commit 73e8637

File tree

8 files changed

+50
-16
lines changed

8 files changed

+50
-16
lines changed

Doc/library/asyncio-protocol.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,11 @@ Datagram Transports
362362
This method does not block; it buffers the data and arranges
363363
for it to be sent out asynchronously.
364364

365+
.. versionchanged:: 3.13
366+
This method can be called with an empty bytes object to send a
367+
zero-length datagram. The buffer size calculation used for flow
368+
control is also updated to account for the datagram header.
369+
365370
.. method:: DatagramTransport.abort()
366371

367372
Close the transport immediately, without waiting for pending

Doc/whatsnew/3.13.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,12 @@ asyncio
218218
the Unix socket when the server is closed.
219219
(Contributed by Pierre Ossman in :gh:`111246`.)
220220

221+
* :meth:`asyncio.DatagramTransport.sendto` will now send zero-length
222+
datagrams if called with an empty bytes object. The transport flow
223+
control also now accounts for the datagram header when calculating
224+
the buffer size.
225+
(Contributed by Jamie Phan in :gh:`115199`.)
226+
221227
copy
222228
----
223229

Lib/asyncio/proactor_events.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -487,9 +487,6 @@ def sendto(self, data, addr=None):
487487
raise TypeError('data argument must be bytes-like object (%r)',
488488
type(data))
489489

490-
if not data:
491-
return
492-
493490
if self._address is not None and addr not in (None, self._address):
494491
raise ValueError(
495492
f'Invalid address: must be None or {self._address}')
@@ -502,7 +499,7 @@ def sendto(self, data, addr=None):
502499

503500
# Ensure that what we buffer is immutable.
504501
self._buffer.append((bytes(data), addr))
505-
self._buffer_size += len(data)
502+
self._buffer_size += len(data) + 8 # include header bytes
506503

507504
if self._write_fut is None:
508505
# No current write operations are active, kick one off

Lib/asyncio/selector_events.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,8 +1241,6 @@ def sendto(self, data, addr=None):
12411241
if not isinstance(data, (bytes, bytearray, memoryview)):
12421242
raise TypeError(f'data argument must be a bytes-like object, '
12431243
f'not {type(data).__name__!r}')
1244-
if not data:
1245-
return
12461244

12471245
if self._address:
12481246
if addr not in (None, self._address):
@@ -1278,7 +1276,7 @@ def sendto(self, data, addr=None):
12781276

12791277
# Ensure that what we buffer is immutable.
12801278
self._buffer.append((bytes(data), addr))
1281-
self._buffer_size += len(data)
1279+
self._buffer_size += len(data) + 8 # include header bytes
12821280
self._maybe_pause_protocol()
12831281

12841282
def _sendto_ready(self):

Lib/asyncio/transports.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ def sendto(self, data, addr=None):
181181
to be sent out asynchronously.
182182
addr is target socket address.
183183
If addr is None use target address pointed on transport creation.
184+
If data is an empty bytes object a zero-length datagram will be
185+
sent.
184186
"""
185187
raise NotImplementedError
186188

Lib/test/test_asyncio/test_proactor_events.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -585,11 +585,10 @@ def test_sendto_memoryview(self):
585585

586586
def test_sendto_no_data(self):
587587
transport = self.datagram_transport()
588-
transport._buffer.append((b'data', ('0.0.0.0', 12345)))
589-
transport.sendto(b'', ())
590-
self.assertFalse(self.sock.sendto.called)
591-
self.assertEqual(
592-
[(b'data', ('0.0.0.0', 12345))], list(transport._buffer))
588+
transport.sendto(b'', ('0.0.0.0', 1234))
589+
self.assertTrue(self.proactor.sendto.called)
590+
self.proactor.sendto.assert_called_with(
591+
self.sock, b'', addr=('0.0.0.0', 1234))
593592

594593
def test_sendto_buffer(self):
595594
transport = self.datagram_transport()
@@ -628,6 +627,19 @@ def test_sendto_buffer_memoryview(self):
628627
list(transport._buffer))
629628
self.assertIsInstance(transport._buffer[1][0], bytes)
630629

630+
def test_sendto_buffer_nodata(self):
631+
data2 = b''
632+
transport = self.datagram_transport()
633+
transport._buffer.append((b'data1', ('0.0.0.0', 12345)))
634+
transport._write_fut = object()
635+
transport.sendto(data2, ('0.0.0.0', 12345))
636+
self.assertFalse(self.proactor.sendto.called)
637+
self.assertEqual(
638+
[(b'data1', ('0.0.0.0', 12345)),
639+
(b'', ('0.0.0.0', 12345))],
640+
list(transport._buffer))
641+
self.assertIsInstance(transport._buffer[1][0], bytes)
642+
631643
@mock.patch('asyncio.proactor_events.logger')
632644
def test_sendto_exception(self, m_log):
633645
data = b'data'

Lib/test/test_asyncio/test_selector_events.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,11 +1280,10 @@ def test_sendto_memoryview(self):
12801280

12811281
def test_sendto_no_data(self):
12821282
transport = self.datagram_transport()
1283-
transport._buffer.append((b'data', ('0.0.0.0', 12345)))
1284-
transport.sendto(b'', ())
1285-
self.assertFalse(self.sock.sendto.called)
1283+
transport.sendto(b'', ('0.0.0.0', 1234))
1284+
self.assertTrue(self.sock.sendto.called)
12861285
self.assertEqual(
1287-
[(b'data', ('0.0.0.0', 12345))], list(transport._buffer))
1286+
self.sock.sendto.call_args[0], (b'', ('0.0.0.0', 1234)))
12881287

12891288
def test_sendto_buffer(self):
12901289
transport = self.datagram_transport()
@@ -1320,6 +1319,18 @@ def test_sendto_buffer_memoryview(self):
13201319
list(transport._buffer))
13211320
self.assertIsInstance(transport._buffer[1][0], bytes)
13221321

1322+
def test_sendto_buffer_nodata(self):
1323+
data2 = b''
1324+
transport = self.datagram_transport()
1325+
transport._buffer.append((b'data1', ('0.0.0.0', 12345)))
1326+
transport.sendto(data2, ('0.0.0.0', 12345))
1327+
self.assertFalse(self.sock.sendto.called)
1328+
self.assertEqual(
1329+
[(b'data1', ('0.0.0.0', 12345)),
1330+
(b'', ('0.0.0.0', 12345))],
1331+
list(transport._buffer))
1332+
self.assertIsInstance(transport._buffer[1][0], bytes)
1333+
13231334
def test_sendto_tryagain(self):
13241335
data = b'data'
13251336

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:meth:`DatagramTransport.sendto` will now send zero-length datagrams if
2+
called with an empty bytes object. The transport flow control also now
3+
accounts for the datagram header when calculating the buffer size.

0 commit comments

Comments
 (0)