From 0ee847693907ccbdefd316db0d86dc7c339b9722 Mon Sep 17 00:00:00 2001 From: Armaan Sandhu Date: Thu, 15 Oct 2026 21:57:08 -0400 Subject: [PATCH 1/5] gh-159503: Avoid copying the unsent remainder of a bytes write in asyncio selector transports When a large bytes write is queued behind pending data, every partial send re-queued the leftover as b[nbytes:], copying the rest of the object each time. Keep the remainder as a memoryview in _adjust_leftover_buffer() and _write_send(), as write() already does on its first-send path. --- Lib/asyncio/selector_events.py | 4 +-- Lib/test/test_asyncio/test_selector_events.py | 30 +++++++++++++++++++ ...-10-16-01-56-15.gh-issue-159503.WgYiow.rst | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-16-01-56-15.gh-issue-159503.WgYiow.rst diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index bf20f053ded435..6f33654b0f62fd 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 74322105a7c757..2395215891c622 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -972,6 +972,22 @@ def test_write_sendmsg_partial(self): self.assertTrue(self.loop.writers) self.assertEqual(list_to_buffer([b'ta']), transport._buffer) + @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') + def test_write_sendmsg_partial_bytes_no_copy(self): + # gh-159503: the unsent remainder of a bytes object must not be + # copied, otherwise a large write costs O(n**2). + data = b'data' + self.sock.sendmsg = mock.Mock() + 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.assertEqual(list_to_buffer([b'ta']), transport._buffer) + self.assertIsInstance(transport._buffer[0], memoryview) + self.assertIs(transport._buffer[0].obj, data) + @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') def test_write_sendmsg_half_buffer(self): data = [memoryview(b'data1'), memoryview(b'data2')] @@ -1081,6 +1097,20 @@ def test_write_ready_partial(self): self.loop.assert_writer(7, transport._write_ready) self.assertEqual(list_to_buffer([b'ta']), transport._buffer) + def test_write_ready_partial_bytes_no_copy(self): + # gh-159503: the unsent remainder of a bytes object must not be + # copied, otherwise a large write costs O(n**2). + data = 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.assertEqual(list_to_buffer([b'ta']), transport._buffer) + self.assertIsInstance(transport._buffer[0], memoryview) + self.assertIs(transport._buffer[0].obj, data) + def test_write_ready_partial_none(self): data = b'data' self.sock.send.return_value = 0 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 00000000000000..02038cf984a135 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-16-01-56-15.gh-issue-159503.WgYiow.rst @@ -0,0 +1 @@ +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. From 12de092b269d7dfbbd4a54823223cd3765103bae Mon Sep 17 00:00:00 2001 From: Armaan Sandhu Date: Fri, 16 Oct 2026 12:20:35 -0400 Subject: [PATCH 2/5] Fold the new checks into the existing partial-write tests Address review: test_write_ready_partial and test_write_sendmsg_partial now run for both bytes and memoryview input and check that the remainder is a view on the original object. Wrap the NEWS entry at 78 columns. --- Lib/test/test_asyncio/test_selector_events.py | 77 +++++++------------ ...-10-16-01-56-15.gh-issue-159503.WgYiow.rst | 5 +- 2 files changed, 31 insertions(+), 51 deletions(-) diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index 2395215891c622..620397a049b7cf 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -958,35 +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) - - @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') - def test_write_sendmsg_partial_bytes_no_copy(self): - # gh-159503: the unsent remainder of a bytes object must not be - # copied, otherwise a large write costs O(n**2). data = b'data' - self.sock.sendmsg = mock.Mock() - 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.assertEqual(list_to_buffer([b'ta']), transport._buffer) - self.assertIsInstance(transport._buffer[0], memoryview) - self.assertIs(transport._buffer[0].obj, 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): @@ -1087,29 +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) - - def test_write_ready_partial_bytes_no_copy(self): - # gh-159503: the unsent remainder of a bytes object must not be - # copied, otherwise a large write costs O(n**2). data = 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.assertEqual(list_to_buffer([b'ta']), transport._buffer) - self.assertIsInstance(transport._buffer[0], memoryview) - self.assertIs(transport._buffer[0].obj, 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 index 02038cf984a135..fca69ffe0087b8 100644 --- 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 @@ -1 +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. +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. From 07641dc3aac0a59fe630cd50f365283165be6fa0 Mon Sep 17 00:00:00 2001 From: Armaan Sandhu Date: Wed, 28 Oct 2026 09:19:01 -0400 Subject: [PATCH 3/5] Address review: slice memoryviews directly, test bytearray, trim NEWS --- Doc/library/asyncio-protocol.rst | 2 ++ Lib/asyncio/selector_events.py | 11 ++++++--- Lib/test/test_asyncio/test_selector_events.py | 24 +++++++++++++++---- ...-10-16-01-56-15.gh-issue-159503.WgYiow.rst | 7 +++--- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Doc/library/asyncio-protocol.rst b/Doc/library/asyncio-protocol.rst index 58f77feb311984..8bdbf8a5676299 100644 --- a/Doc/library/asyncio-protocol.rst +++ b/Doc/library/asyncio-protocol.rst @@ -333,6 +333,8 @@ Write-only Transports This method does not block; it buffers the data and arranges for it to be sent out asynchronously. + Do not modify *data* until it has been sent. + .. method:: WriteTransport.writelines(list_of_data) Write a list (or any iterable) of data bytes to the transport. diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 6f33654b0f62fd..5be796b84eba55 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -1143,7 +1143,10 @@ def _adjust_leftover_buffer(self, nbytes: int) -> None: if b_len <= nbytes: nbytes -= b_len else: - buffer.appendleft(memoryview(b)[nbytes:]) + # Keep a view of the remainder, not a copy. + if not isinstance(b, memoryview): + b = memoryview(b) + buffer.appendleft(b[nbytes:]) break def _write_send(self): @@ -1154,8 +1157,10 @@ def _write_send(self): buffer = self._buffer.popleft() n = self._sock.send(buffer) if n != len(buffer): - # Not all data was written - self._buffer.appendleft(memoryview(buffer)[n:]) + # Not all data was written; keep a view, not a copy. + if not isinstance(buffer, memoryview): + buffer = memoryview(buffer) + self._buffer.appendleft(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 620397a049b7cf..916a680a42049c 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -959,7 +959,7 @@ def test_write_sendmsg_full(self): @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') def test_write_sendmsg_partial(self): data = b'data' - for buf in data, memoryview(data): + for buf in data, bytearray(data), memoryview(data): with self.subTest(type=type(buf).__name__): self.sock.sendmsg = mock.Mock() # Sent partial data @@ -973,7 +973,15 @@ def test_write_sendmsg_partial(self): 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) + rest = transport._buffer[0] + self.assertIsInstance(rest, memoryview) + self.assertIs( + rest.obj, + buf.obj if isinstance(buf, memoryview) else buf) + if isinstance(buf, bytearray): + # The unsent view pins the bytearray against resizing. + with self.assertRaises(BufferError): + buf.append(0) @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') def test_write_sendmsg_half_buffer(self): @@ -1075,7 +1083,7 @@ def test_write_ready_no_data(self): def test_write_ready_partial(self): data = b'data' - for buf in data, memoryview(data): + for buf in data, bytearray(data), memoryview(data): with self.subTest(type=type(buf).__name__): self.sock.send.return_value = 2 @@ -1086,7 +1094,15 @@ def test_write_ready_partial(self): 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) + rest = transport._buffer[0] + self.assertIsInstance(rest, memoryview) + self.assertIs( + rest.obj, + buf.obj if isinstance(buf, memoryview) else buf) + if isinstance(buf, bytearray): + # The unsent view pins the bytearray against resizing. + with self.assertRaises(BufferError): + buf.append(0) 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 index fca69ffe0087b8..2a9b2b59384864 100644 --- 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 @@ -1,4 +1,3 @@ -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. +Fix quadratic copying in :mod:`asyncio` selector socket transports on partial +sends. A :class:`bytearray` passed to ``write()`` now stays locked against +resizing until it has been sent. From a5f0f6c23d4dac99c815b20da619cc35d4bb0cb6 Mon Sep 17 00:00:00 2001 From: Armaan Sandhu Date: Thu, 29 Oct 2026 09:06:21 -0400 Subject: [PATCH 4/5] Address review: reword write() docs, share a test helper, fix NEWS role --- Doc/library/asyncio-protocol.rst | 3 +- Lib/test/test_asyncio/test_selector_events.py | 32 +++++++------------ ...-10-16-01-56-15.gh-issue-159503.WgYiow.rst | 4 +-- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/Doc/library/asyncio-protocol.rst b/Doc/library/asyncio-protocol.rst index 8bdbf8a5676299..42c2d509d5a729 100644 --- a/Doc/library/asyncio-protocol.rst +++ b/Doc/library/asyncio-protocol.rst @@ -333,7 +333,8 @@ Write-only Transports This method does not block; it buffers the data and arranges for it to be sent out asynchronously. - Do not modify *data* until it has been sent. + The transport keeps a reference to *data* rather than copying. *data* + must not be mutated while being sent. .. method:: WriteTransport.writelines(list_of_data) diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index 916a680a42049c..c3b04faced1c2f 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -618,6 +618,16 @@ def socket_transport(self, waiter=None, sendmsg=False): self.addCleanup(close_transport, transport) return transport + def assert_remainder_is_view(self, transport, buf): + # gh-159503: the unsent remainder is a view, not a copy + rest = transport._buffer[0] + self.assertIsInstance(rest, memoryview) + self.assertIs(rest.obj, memoryview(buf).obj) + if isinstance(buf, bytearray): + # The unsent view pins the bytearray against resizing. + with self.assertRaises(BufferError): + buf.append(0) + def test_ctor(self): waiter = self.loop.create_future() tr = self.socket_transport(waiter=waiter) @@ -972,16 +982,7 @@ def test_write_sendmsg_partial(self): 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 - rest = transport._buffer[0] - self.assertIsInstance(rest, memoryview) - self.assertIs( - rest.obj, - buf.obj if isinstance(buf, memoryview) else buf) - if isinstance(buf, bytearray): - # The unsent view pins the bytearray against resizing. - with self.assertRaises(BufferError): - buf.append(0) + self.assert_remainder_is_view(transport, buf) @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') def test_write_sendmsg_half_buffer(self): @@ -1093,16 +1094,7 @@ def test_write_ready_partial(self): 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 - rest = transport._buffer[0] - self.assertIsInstance(rest, memoryview) - self.assertIs( - rest.obj, - buf.obj if isinstance(buf, memoryview) else buf) - if isinstance(buf, bytearray): - # The unsent view pins the bytearray against resizing. - with self.assertRaises(BufferError): - buf.append(0) + self.assert_remainder_is_view(transport, buf) 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 index 2a9b2b59384864..158968d0c2a1ce 100644 --- 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 @@ -1,3 +1,3 @@ Fix quadratic copying in :mod:`asyncio` selector socket transports on partial -sends. A :class:`bytearray` passed to ``write()`` now stays locked against -resizing until it has been sent. +sends. A :class:`bytearray` passed to :meth:`~asyncio.WriteTransport.write` +now stays locked against resizing until it has been sent. From 792a686f6673c016d542bc840193987d1af7dd3e Mon Sep 17 00:00:00 2001 From: Armaan Sandhu Date: Thu, 29 Oct 2026 09:44:27 -0400 Subject: [PATCH 5/5] Wrap the data in a memoryview when write() buffers it The send paths are back to plain slicing, since every buffered item is now a memoryview. --- Lib/asyncio/selector_events.py | 11 +-- Lib/test/test_asyncio/test_selector_events.py | 78 +++++++++---------- 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 5be796b84eba55..45e2101aa184d0 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -1095,7 +1095,9 @@ def write(self, data): # Not all was written; register write handler. self._add_writer(self._sock_fd, self._write_ready) - # Add it to the buffer. + # Add it to the buffer as a view, so partial sends don't copy it. + if not isinstance(data, memoryview): + data = memoryview(data) self._buffer.append(data) self._buffer_size += len(data) self._maybe_pause_protocol() @@ -1143,9 +1145,6 @@ def _adjust_leftover_buffer(self, nbytes: int) -> None: if b_len <= nbytes: nbytes -= b_len else: - # Keep a view of the remainder, not a copy. - if not isinstance(b, memoryview): - b = memoryview(b) buffer.appendleft(b[nbytes:]) break @@ -1157,9 +1156,7 @@ def _write_send(self): buffer = self._buffer.popleft() n = self._sock.send(buffer) if n != len(buffer): - # Not all data was written; keep a view, not a copy. - if not isinstance(buffer, memoryview): - buffer = memoryview(buffer) + # Not all data was written self._buffer.appendleft(buffer[n:]) self._buffer_size -= n except (BlockingIOError, InterruptedError): diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index c3b04faced1c2f..e080225bd976d9 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -618,16 +618,6 @@ def socket_transport(self, waiter=None, sendmsg=False): self.addCleanup(close_transport, transport) return transport - def assert_remainder_is_view(self, transport, buf): - # gh-159503: the unsent remainder is a view, not a copy - rest = transport._buffer[0] - self.assertIsInstance(rest, memoryview) - self.assertIs(rest.obj, memoryview(buf).obj) - if isinstance(buf, bytearray): - # The unsent view pins the bytearray against resizing. - with self.assertRaises(BufferError): - buf.append(0) - def test_ctor(self): waiter = self.loop.create_future() tr = self.socket_transport(waiter=waiter) @@ -865,14 +855,23 @@ def test_write_partial_none(self): self.assertEqual(list_to_buffer([b'data']), transport._buffer) def test_write_tryagain(self): - self.sock.send.side_effect = BlockingIOError - data = b'data' - transport = self.socket_transport() - transport.write(data) + for buf in data, bytearray(data), memoryview(data): + with self.subTest(type=type(buf).__name__): + self.sock.send.side_effect = BlockingIOError - self.loop.assert_writer(7, transport._write_ready) - self.assertEqual(list_to_buffer([b'data']), transport._buffer) + transport = self.socket_transport() + transport.write(buf) + + self.loop.assert_writer(7, transport._write_ready) + self.assertEqual(list_to_buffer([b'data']), transport._buffer) + # gh-159503: the data is buffered as a view, not a copy + self.assertIsInstance(transport._buffer[0], memoryview) + self.assertIs(transport._buffer[0].obj, memoryview(buf).obj) + if isinstance(buf, bytearray): + # The buffered view pins the bytearray against resizing. + with self.assertRaises(BufferError): + buf.append(0) def test_write_sendmsg_no_data(self): self.sock.sendmsg = mock.Mock() @@ -968,21 +967,19 @@ def test_write_sendmsg_full(self): @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') def test_write_sendmsg_partial(self): - data = b'data' - for buf in data, bytearray(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) - self.assert_remainder_is_view(transport, buf) + + 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) @unittest.skipUnless(selector_events._HAS_SENDMSG, 'no sendmsg') def test_write_sendmsg_half_buffer(self): @@ -1083,18 +1080,15 @@ def test_write_ready_no_data(self): self.assertRaises(AssertionError, transport._write_ready) def test_write_ready_partial(self): - data = b'data' - for buf in data, bytearray(data), memoryview(data): - with self.subTest(type=type(buf).__name__): - self.sock.send.return_value = 2 + data = memoryview(b'data') + 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) - self.assert_remainder_is_view(transport, buf) + 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) def test_write_ready_partial_none(self): data = b'data'