diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index b2318ddace8514b..fc5302d875fc1f9 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -835,7 +835,9 @@ how the command-line arguments should be handled. The supplied actions are: >>> parser.parse_args(['-vvv']) Namespace(verbose=3) - Note, the *default* will be ``None`` unless explicitly set to *0*. + Unless explicitly set, the *default* will be ``None``. If the default + value is a non-zero number, the count starts from that number rather + than from zero. * ``'help'`` - This prints a complete help message for all the options in the current parser and then exits. By default a help action is automatically diff --git a/Include/internal/pycore_compile.h b/Include/internal/pycore_compile.h index 7e248429af8eb8a..4597ae2763ad77a 100644 --- a/Include/internal/pycore_compile.h +++ b/Include/internal/pycore_compile.h @@ -110,6 +110,7 @@ enum _PyCompile_FBlockType { COMPILE_FBLOCK_EXCEPTION_HANDLER, COMPILE_FBLOCK_EXCEPTION_GROUP_HANDLER, COMPILE_FBLOCK_ASYNC_COMPREHENSION_GENERATOR, + COMPILE_FBLOCK_INLINED_COMPREHENSION, COMPILE_FBLOCK_STOP_ITERATION, }; diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index f26fba175b63cd7..90269c936555cb5 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1497,7 +1497,12 @@ async def create_datagram_endpoint(self, protocol_factory, else: raise exceptions[0] - protocol = protocol_factory() + try: + protocol = protocol_factory() + except: + # gh-156400: no transport owns the socket yet, so close it. + sock.close() + raise waiter = self.create_future() transport = self._make_datagram_transport( sock, protocol, r_addr, waiter) @@ -1714,7 +1719,12 @@ async def connect_accepted_socket( return transport, protocol async def connect_read_pipe(self, protocol_factory, pipe): - protocol = protocol_factory() + try: + protocol = protocol_factory() + except: + # gh-156400: no transport owns the pipe yet, so close it. + pipe.close() + raise waiter = self.create_future() transport = self._make_read_pipe_transport(pipe, protocol, waiter) @@ -1730,7 +1740,12 @@ async def connect_read_pipe(self, protocol_factory, pipe): return transport, protocol async def connect_write_pipe(self, protocol_factory, pipe): - protocol = protocol_factory() + try: + protocol = protocol_factory() + except: + # gh-156400: no transport owns the pipe yet, so close it. + pipe.close() + raise waiter = self.create_future() transport = self._make_write_pipe_transport(pipe, protocol, waiter) diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py index 94fcd33d7088a68..d5db59f5d6f5f36 100644 --- a/Lib/asyncio/graph.py +++ b/Lib/asyncio/graph.py @@ -155,7 +155,9 @@ def capture_call_graph( f = sys._getframe(depth) if limit != 0 else None try: while f is not None: - is_async = f.f_generator is not None + # gh-156988: sync gen should not clear the call chain + is_async = isinstance( + f.f_generator, (types.CoroutineType, types.AsyncGeneratorType)) call_stack.append(FrameCallGraphEntry(f)) if is_async: diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 18afdca23163a1e..e11f77ef10c0c96 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -2041,6 +2041,43 @@ def test_create_datagram_endpoint_sock(self): self.loop.run_until_complete(protocol.done) self.assertEqual('CLOSED', protocol.state) + def test_create_datagram_endpoint_transport_error_closes_sock(self): + # gh-156400: the socket is closed if the transport is never created. + sock = mock.Mock() + sock.type = socket.SOCK_DGRAM + + def factory(): + raise ZeroDivisionError + + coro = self.loop.create_datagram_endpoint(factory, sock=sock) + with self.assertRaises(ZeroDivisionError): + self.loop.run_until_complete(coro) + self.assertTrue(sock.close.called) + + def test_connect_read_pipe_transport_error_closes_pipe(self): + # gh-156400: the pipe is closed if the transport is never created. + pipe = mock.Mock() + + def factory(): + raise ZeroDivisionError + + coro = self.loop.connect_read_pipe(factory, pipe) + with self.assertRaises(ZeroDivisionError): + self.loop.run_until_complete(coro) + self.assertTrue(pipe.close.called) + + def test_connect_write_pipe_transport_error_closes_pipe(self): + # gh-156400: the pipe is closed if the transport is never created. + pipe = mock.Mock() + + def factory(): + raise ZeroDivisionError + + coro = self.loop.connect_write_pipe(factory, pipe) + with self.assertRaises(ZeroDivisionError): + self.loop.run_until_complete(coro) + self.assertTrue(pipe.close.called) + @unittest.skipUnless(hasattr(socket, 'AF_UNIX'), 'No UNIX Sockets') def test_create_datagram_endpoint_sock_unix(self): fut = self.loop.create_datagram_endpoint( diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 50d528fb9fb2c51..36841672e1f0f65 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -593,6 +593,26 @@ async def main(): await main() self.assertRegex(output[0], r'in generator [\w.<>]+\.gen\(\)') + async def test_capture_call_graph_generator_keeps_caller_frames(self): + # gh-156988: sync gen should not clear the call chain + stack = None + + def gen(): + nonlocal stack + graph = asyncio.capture_call_graph() + stack = [entry.frame.f_code.co_name for entry in graph.call_stack] + yield + + def middle(): + for _ in gen(): + pass + + async def main(): + middle() + + await main() + self.assertEqual(stack[:3], ['gen', 'middle', 'main']) + @unittest.skipIf( not hasattr(asyncio.futures, "_c_future_add_to_awaited_by"), diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index a63042055210c5c..dbd518707c18266 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -3519,6 +3519,21 @@ def test_syntax_error_on_deeply_nested_blocks(self): """ self._check_error(source, "too many statically nested blocks") + @support.cpython_only + def test_nested_inlined_comprehensions_block_limit(self): + # Each inlined comprehension with locals emits SETUP_FINALLY, which + # must count toward CO_MAXBLOCKS (gh-156091). + def src(depth): + e = "i for i in r" + for _ in range(depth - 1): + e = "[" + e + "] for i in r" + return "x = [" + e + "]" + + CO_MAXBLOCKS = 21 + compile(src(CO_MAXBLOCKS), "", "exec") + self._check_error(src(CO_MAXBLOCKS + 1), + "too many statically nested blocks") + @support.cpython_only def test_error_on_parser_stack_overflow(self): source = "-" * 100000 + "4" diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index d0ca94668e05bc2..1effc70b5323139 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -595,7 +595,7 @@ def __set_return_value(self, value): __return_value_doc = "The value to be returned when the mock is called." return_value = property(__get_return_value, __set_return_value, - __return_value_doc) + doc=__return_value_doc) @property diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-04-14-06-00.gh-issue-156091.nested-comp.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-04-14-06-00.gh-issue-156091.nested-comp.rst new file mode 100644 index 000000000000000..fdceb3209b7f7cb --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-04-14-06-00.gh-issue-156091.nested-comp.rst @@ -0,0 +1,3 @@ +Fix a crash when compiling deeply nested inlined list, set, or dict +comprehensions. A :exc:`SyntaxError` is now raised when the nesting exceeds +the compiler's static block limit. diff --git a/Misc/NEWS.d/next/Library/2026-08-26-13-58-01.gh-issue-156400.dGrmP1.rst b/Misc/NEWS.d/next/Library/2026-08-26-13-58-01.gh-issue-156400.dGrmP1.rst new file mode 100644 index 000000000000000..4c7f05a19b86934 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-26-13-58-01.gh-issue-156400.dGrmP1.rst @@ -0,0 +1,6 @@ +Fix socket and pipe leaks in :mod:`asyncio` when ``protocol_factory()`` raises +in :meth:`loop.create_datagram_endpoint +`, :meth:`loop.connect_read_pipe +`, and :meth:`loop.connect_write_pipe +`. The socket or pipe is now closed instead +of leaking until garbage collection. diff --git a/Misc/NEWS.d/next/Library/2026-09-05-15-27-11.gh-issue-156970.DxlgbB.rst b/Misc/NEWS.d/next/Library/2026-09-05-15-27-11.gh-issue-156970.DxlgbB.rst new file mode 100644 index 000000000000000..dd60841efe821c2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-05-15-27-11.gh-issue-156970.DxlgbB.rst @@ -0,0 +1 @@ +Set the docstring of :attr:`unittest.mock.Mock.return_value`. diff --git a/Misc/NEWS.d/next/Library/2026-09-05-18-23-30.gh-issue-156988.nEUQE6.rst b/Misc/NEWS.d/next/Library/2026-09-05-18-23-30.gh-issue-156988.nEUQE6.rst new file mode 100644 index 000000000000000..6cb2f1f4cf14ae0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-05-18-23-30.gh-issue-156988.nEUQE6.rst @@ -0,0 +1,2 @@ +:func:`asyncio.print_call_graph` no longer truncates the call stack at a +synchronous generator. diff --git a/Python/codegen.c b/Python/codegen.c index 88e3aa8648fc584..c12baf6b15a6dec 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -551,6 +551,7 @@ codegen_unwind_fblock(compiler *c, location *ploc, case COMPILE_FBLOCK_EXCEPTION_HANDLER: case COMPILE_FBLOCK_EXCEPTION_GROUP_HANDLER: case COMPILE_FBLOCK_ASYNC_COMPREHENSION_GENERATOR: + case COMPILE_FBLOCK_INLINED_COMPREHENSION: case COMPILE_FBLOCK_STOP_ITERATION: return SUCCESS; @@ -4976,8 +4977,11 @@ codegen_push_inlined_comprehension_locals(compiler *c, location loc, NEW_JUMP_TARGET_LABEL(c, cleanup); state->cleanup = cleanup; - // no need to push an fblock for this "virtual" try/finally; there can't - // be return/continue/break inside a comprehension + // Count against CO_MAXBLOCKS: SETUP_FINALLY consumes an except-stack + // slot even though return/continue/break cannot appear here. + RETURN_IF_ERROR(_PyCompile_PushFBlock( + c, loc, COMPILE_FBLOCK_INLINED_COMPREHENSION, + cleanup, NO_LABEL, NULL)); ADDOP_JUMP(c, loc, SETUP_FINALLY, cleanup); } return SUCCESS; @@ -5023,6 +5027,8 @@ codegen_pop_inlined_comprehension_locals(compiler *c, location loc, { if (state->pushed_locals) { ADDOP(c, NO_LOCATION, POP_BLOCK); + _PyCompile_PopFBlock(c, COMPILE_FBLOCK_INLINED_COMPREHENSION, + state->cleanup); NEW_JUMP_TARGET_LABEL(c, end); ADDOP_JUMP(c, NO_LOCATION, JUMP_NO_INTERRUPT, end);