diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index bf20f053ded4353..6f33654b0f62fda 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -1143,7 +1143,7 @@ def _adjust_leftover_buffer(self, nbytes: int) -> None: if b_len <= nbytes: nbytes -= b_len else: - buffer.appendleft(b[nbytes:]) + buffer.appendleft(memoryview(b)[nbytes:]) break def _write_send(self): @@ -1155,7 +1155,7 @@ def _write_send(self): n = self._sock.send(buffer) if n != len(buffer): # Not all data was written - self._buffer.appendleft(buffer[n:]) + self._buffer.appendleft(memoryview(buffer)[n:]) self._buffer_size -= n except (BlockingIOError, InterruptedError): self._buffer.appendleft(buffer) diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index 74322105a7c7577..620397a049b7cf4 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -958,19 +958,22 @@ def test_write_sendmsg_full(self): @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') def test_write_sendmsg_partial(self): - - data = memoryview(b'data') - self.sock.sendmsg = mock.Mock() - # Sent partial data - self.sock.sendmsg.return_value = 2 - - transport = self.socket_transport(sendmsg=True) - transport._buffer.append(data) - self.loop._add_writer(7, transport._write_ready) - transport._write_ready() - self.assertTrue(self.sock.sendmsg.called) - self.assertTrue(self.loop.writers) - self.assertEqual(list_to_buffer([b'ta']), transport._buffer) + data = b'data' + for buf in data, memoryview(data): + with self.subTest(type=type(buf).__name__): + self.sock.sendmsg = mock.Mock() + # Sent partial data + self.sock.sendmsg.return_value = 2 + + transport = self.socket_transport(sendmsg=True) + transport._buffer.append(buf) + self.loop._add_writer(7, transport._write_ready) + transport._write_ready() + self.assertTrue(self.sock.sendmsg.called) + self.assertTrue(self.loop.writers) + self.assertEqual(list_to_buffer([b'ta']), transport._buffer) + # gh-159503: the remainder is a view, not a copy + self.assertIs(transport._buffer[0].obj, data) @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') def test_write_sendmsg_half_buffer(self): @@ -1071,15 +1074,19 @@ def test_write_ready_no_data(self): self.assertRaises(AssertionError, transport._write_ready) def test_write_ready_partial(self): - data = memoryview(b'data') - self.sock.send.return_value = 2 - - transport = self.socket_transport() - transport._buffer.append(data) - self.loop._add_writer(7, transport._write_ready) - transport._write_ready() - self.loop.assert_writer(7, transport._write_ready) - self.assertEqual(list_to_buffer([b'ta']), transport._buffer) + data = b'data' + for buf in data, memoryview(data): + with self.subTest(type=type(buf).__name__): + self.sock.send.return_value = 2 + + transport = self.socket_transport() + transport._buffer.append(buf) + self.loop._add_writer(7, transport._write_ready) + transport._write_ready() + self.loop.assert_writer(7, transport._write_ready) + self.assertEqual(list_to_buffer([b'ta']), transport._buffer) + # gh-159503: the remainder is a view, not a copy + self.assertIs(transport._buffer[0].obj, data) def test_write_ready_partial_none(self): data = b'data' diff --git a/Misc/NEWS.d/next/Library/2026-10-16-01-56-15.gh-issue-159503.WgYiow.rst b/Misc/NEWS.d/next/Library/2026-10-16-01-56-15.gh-issue-159503.WgYiow.rst new file mode 100644 index 000000000000000..fca69ffe0087b82 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-16-01-56-15.gh-issue-159503.WgYiow.rst @@ -0,0 +1,4 @@ +Fix quadratic copying in :mod:`asyncio` selector socket transports when a +large :class:`bytes` write is only partly sent while other data is buffered. +The unsent remainder is now kept as a :class:`memoryview` instead of being +copied on every partial send.