From d1a5b54abaca2b443c7642ef3a7be8e1cd077ae3 Mon Sep 17 00:00:00 2001 From: Duprat Date: Sat, 11 Jul 2026 14:29:38 +0200 Subject: [PATCH 1/4] gh-152614: Add raises to `QueueShutDown` in `asyncio.Queue.put_no_wait` and `asyncio.Queue.get_nowait` documentation methods (#152681) --- Doc/library/asyncio-queue.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Doc/library/asyncio-queue.rst b/Doc/library/asyncio-queue.rst index a9735ae80652df6..5e3f531f4e482e4 100644 --- a/Doc/library/asyncio-queue.rst +++ b/Doc/library/asyncio-queue.rst @@ -71,6 +71,8 @@ Queue Return an item if one is immediately available, else raise :exc:`QueueEmpty`. + Raises :exc:`QueueShutDown` if the queue has been shut down and is empty. + .. method:: join() :async: @@ -96,6 +98,8 @@ Queue If no free slot is immediately available, raise :exc:`QueueFull`. + Raises :exc:`QueueShutDown` if the queue has been shut down. + .. method:: qsize() Return the number of items in the queue. @@ -188,8 +192,9 @@ Exceptions .. exception:: QueueShutDown - Exception raised when :meth:`~Queue.put` or :meth:`~Queue.get` is - called on a queue which has been shut down. + Exception raised when :meth:`~Queue.put`, :meth:`~Queue.put_nowait`, + :meth:`~Queue.get` or :meth:`~Queue.get_nowait` is called + on a queue which has been shut down. .. versionadded:: 3.13 From 7671ee1eba3c7c13747761b35b4b9d4166a4670a Mon Sep 17 00:00:00 2001 From: Xuyang Zhang <119476662+kn1g78@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:03:23 +0800 Subject: [PATCH 2/4] gh-152431: update StreamReader transport after StreamWriter.start_tls() (#152432) Co-authored-by: Kumar Aditya --- Lib/asyncio/streams.py | 7 +++++ Lib/test/test_asyncio/test_streams.py | 28 +++++++++++++++++++ ...-06-28-00-00-00.gh-issue-152431.Ja3K9m.rst | 2 ++ 3 files changed, 37 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-06-28-00-00-00.gh-issue-152431.Ja3K9m.rst diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index d6076465f79366f..f5c4f0b0c3297ba 100644 --- a/Lib/asyncio/streams.py +++ b/Lib/asyncio/streams.py @@ -216,6 +216,9 @@ def _stream_reader(self): def _replace_transport(self, transport): self._transport = transport self._over_ssl = transport.get_extra_info('sslcontext') is not None + reader = self._stream_reader + if reader is not None: + reader._replace_transport(transport) def connection_made(self, transport): if self._reject_connection: @@ -473,6 +476,10 @@ def set_transport(self, transport): assert self._transport is None, 'Transport already set' self._transport = transport + def _replace_transport(self, transport): + assert self._transport is not None, 'Transport not set' + self._transport = transport + def _maybe_resume_transport(self): if self._paused and len(self._buffer) <= self._limit: self._paused = False diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index fb774895a7e52a5..911087a128f9713 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -861,6 +861,34 @@ async def run_test(): self.loop.run_until_complete(run_test()) self.assertEqual(messages, []) + def test_streamwriter_start_tls_updates_reader_transport(self): + reader = asyncio.StreamReader(loop=self.loop) + protocol = asyncio.StreamReaderProtocol(reader, loop=self.loop) + old_transport = mock.Mock() + old_transport.get_extra_info.return_value = None + old_transport.is_closing.return_value = False + protocol.connection_made(old_transport) + + writer = asyncio.StreamWriter(old_transport, protocol, reader, self.loop) + + ssl_context = mock.sentinel.ssl_context + new_transport = mock.Mock() + new_transport.get_extra_info.return_value = ssl_context + self.loop.start_tls = mock.AsyncMock(return_value=new_transport) + + self.loop.run_until_complete(writer.start_tls(ssl_context)) + + self.loop.start_tls.assert_awaited_once_with( + old_transport, protocol, ssl_context, + server_side=False, server_hostname=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + ) + self.assertIs(writer.transport, new_transport) + self.assertIs(protocol._transport, new_transport) + self.assertIs(reader._transport, new_transport) + self.assertTrue(protocol._over_ssl) + def test_streamreader_constructor_without_loop(self): with self.assertRaisesRegex(RuntimeError, 'no current event loop'): asyncio.StreamReader() diff --git a/Misc/NEWS.d/next/Library/2026-06-28-00-00-00.gh-issue-152431.Ja3K9m.rst b/Misc/NEWS.d/next/Library/2026-06-28-00-00-00.gh-issue-152431.Ja3K9m.rst new file mode 100644 index 000000000000000..bda2dfd7cd9d89b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-28-00-00-00.gh-issue-152431.Ja3K9m.rst @@ -0,0 +1,2 @@ +Fix ``asyncio.StreamWriter.start_tls()`` to keep the linked +``StreamReader`` transport in sync with the upgraded transport. From 931cfdf8edcd867e3fe1f20fc001f917e3f8890e Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 11 Jul 2026 08:57:37 -0700 Subject: [PATCH 3/4] gh-145694: Update tutorial indentation guidance for PyREPL auto-indent (#145725) The tutorial stated users must manually type tabs/spaces at the interactive prompt, which is no longer accurate since the PyREPL in Python 3.13+ auto-indents after compound statement headers. Updated to mention both the default REPL behavior and the basic REPL fallback. --- Doc/tutorial/introduction.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Doc/tutorial/introduction.rst b/Doc/tutorial/introduction.rst index 7778e37a9adaa95..465c32d0b72431c 100644 --- a/Doc/tutorial/introduction.rst +++ b/Doc/tutorial/introduction.rst @@ -532,10 +532,13 @@ This example introduces several new features. and ``!=`` (not equal to). * The *body* of the loop is *indented*: indentation is Python's way of grouping - statements. At the interactive prompt, you have to type a tab or space(s) for - each indented line. In practice you will prepare more complicated input - for Python with a text editor; all decent text editors have an auto-indent - facility. When a compound statement is entered interactively, it must be + statements. At the interactive prompt, the default REPL automatically + indents continuation lines after compound statement headers like ``if`` or + ``while``. In the basic REPL (invoked with :envvar:`PYTHON_BASIC_REPL`) + or in older Python versions, you have to type a tab or space(s) for each + indented line manually. In practice you will prepare more complicated + input for Python with a text editor; all decent text editors have an + auto-indent facility. When a compound statement is entered interactively, it must be followed by a blank line to indicate completion (since the parser cannot guess when you have typed the last line). Note that each line within a basic block must be indented by the same amount. From 65afd6579fb23513432e152ca05a22d0c174f7bb Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sat, 11 Jul 2026 16:59:37 +0100 Subject: [PATCH 4/4] gh-141968: Use `take_bytes` to simplify and remove copy from pyrepl `BaseEventQueue` (#149852) --- Lib/_pyrepl/base_eventqueue.py | 16 ++++------------ Lib/test/test_pyrepl/test_eventqueue.py | 8 +------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/Lib/_pyrepl/base_eventqueue.py b/Lib/_pyrepl/base_eventqueue.py index 0589a0f437ec7c5..f520ffd7d8ad11d 100644 --- a/Lib/_pyrepl/base_eventqueue.py +++ b/Lib/_pyrepl/base_eventqueue.py @@ -54,14 +54,6 @@ def empty(self) -> bool: """ return not self.events - def flush_buf(self) -> bytearray: - """ - Flushes the buffer and returns its contents. - """ - old = self.buf - self.buf = bytearray() - return old - def insert(self, event: Event) -> None: """ Inserts an event into the queue. @@ -87,7 +79,7 @@ def push(self, char: int | bytes) -> None: if isinstance(k, dict): self.keymap = k else: - self.insert(Event('key', k, bytes(self.flush_buf()))) + self.insert(Event('key', k, self.buf.take_bytes())) # type: ignore[attr-defined] self.keymap = self.compiled_keymap elif self.buf and self.buf[0] == 27: # escape @@ -97,14 +89,14 @@ def push(self, char: int | bytes) -> None: trace('unrecognized escape sequence, propagating...') self.keymap = self.compiled_keymap self.insert(Event('key', '\033', b'\033')) - for _c in self.flush_buf()[1:]: + for _c in self.buf.take_bytes()[1:]: # type: ignore[attr-defined] self.push(_c) else: try: - decoded = bytes(self.buf).decode(self.encoding) + decoded = self.buf.decode(self.encoding) except UnicodeError: return else: - self.insert(Event('key', decoded, bytes(self.flush_buf()))) + self.insert(Event('key', decoded, self.buf.take_bytes())) # type: ignore[attr-defined] self.keymap = self.compiled_keymap diff --git a/Lib/test/test_pyrepl/test_eventqueue.py b/Lib/test/test_pyrepl/test_eventqueue.py index 69d9612b70dc776..56ab43211847a49 100644 --- a/Lib/test/test_pyrepl/test_eventqueue.py +++ b/Lib/test/test_pyrepl/test_eventqueue.py @@ -38,12 +38,6 @@ def test_empty(self): eq.insert(Event("key", "a", b"a")) self.assertFalse(eq.empty()) - def test_flush_buf(self): - eq = self.make_eventqueue() - eq.buf.extend(b"test") - self.assertEqual(eq.flush_buf(), b"test") - self.assertEqual(eq.buf, bytearray()) - def test_insert(self): eq = self.make_eventqueue() event = Event("key", "a", b"a") @@ -93,7 +87,7 @@ def test_push_with_keymap_in_keymap_and_escape(self, mock_keymap): eq.push(b"a") mock_keymap.compile_keymap.assert_called() self.assertTrue(eq.empty()) - eq.flush_buf() + eq.buf.resize(0) eq.push(b"\033") self.assertEqual(eq.events[0].evt, "key") self.assertEqual(eq.events[0].data, "\033")