diff --git a/Doc/library/concurrent.interpreters.rst b/Doc/library/concurrent.interpreters.rst index 24e9eaea5cad4b..2c509ab13a6acf 100644 --- a/Doc/library/concurrent.interpreters.rst +++ b/Doc/library/concurrent.interpreters.rst @@ -191,7 +191,7 @@ objects are either directly shared or copied efficiently. For example: * :class:`float` * :class:`tuple` (of similarly supported objects) -There is a small number of Python types that actually share mutable +There are a small number of Python types that actually share mutable data between interpreters: * :class:`memoryview` @@ -274,7 +274,7 @@ Interpreter objects .. method:: call(callable, /, *args, **kwargs) - Return the result of calling running the given function in the + Return the result of running the given function in the interpreter (in the current thread). .. _interp-call-in-thread: diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 451302715329fe..f46e938fd314dd 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -1601,6 +1601,14 @@ def _proc_pax(self, tarfile): # Fetch the next header. try: next = self._fromtarfile(tarfile, dircheck=False) + except EOFHeaderError: + if self.type == XGLTYPE: + # If this is a global header at the end of the archive + # (no regular members follow), let the EOFHeaderError + # propagate so the caller handles end-of-archive normally. + tarfile.offset = tarfile.fileobj.tell() - BLOCKSIZE + raise + raise SubsequentHeaderError("end of file header") from None except HeaderError as e: raise SubsequentHeaderError(str(e)) from None diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 1d2c105ac047e1..294355698c8683 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -542,6 +542,10 @@ async def sleep(delay, result=None): '''a = [x async for x in (x async for x in arange(5))][1]''', '''a, = [1 for x in {x async for x in arange(1)}]''', '''a = [await sleep(0, x) async for x in arange(2)][1]''', + '''a = [await sleep(0, 1) for _ in [0]][0]''', + '''a = {await sleep(0, 1) for _ in [0]}.pop()''', + '''a = {0: await sleep(0, 1) for _ in [0]}[0]''', + '''a = (lambda x=[await sleep(0, 1) for _ in [0]]: x)()[0]''', # gh-121637: Make sure we correctly handle the case where the # async code is optimized away '''assert not await sleep(0); a = 1''', @@ -610,7 +614,26 @@ async def __aexit__(self, *exc_info): '''def f(): async with Lock() as l: a = 1 - ''' + ''', + '''class C: + [await x for x in y] + ''', + '''class C: + [x async for x in arange(10)] + ''', + '''async def f(): + class C: + [await x for x in y] + ''', + '''lambda: [await x for x in y]''', + '''class C: + def f(self, x=[await y for y in z]): + pass + ''', + '''type T = [await x for x in y]''', + '''async def f[T=[await x for x in y]](): + pass + ''', ] for mode, code_sample in product(modes, code_samples): source = dedent(code_sample) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 433afac875aa7b..b4585adb07ece4 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -342,5 +342,20 @@ def test_pyobject_dump(self): self.assertRegex(output, r'') +class RefTracerTest(unittest.TestCase): + @support.requires_resource('cpu') + @support.skip_emscripten_stack_overflow() + @support.skip_wasi_stack_overflow() + def test_destroy_traced_for_trashcan_deferred_objects(self): + depth = 200_000 + chain = None + for _ in range(depth): + chain = [chain] + _testcapi.start_counting_list_destroys() + del chain + destroys = _testcapi.stop_counting_list_destroys() + self.assertEqual(destroys, depth) + + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py index 9d415238876c8f..ab854d56d5a3eb 100644 --- a/Lib/test/test_coroutines.py +++ b/Lib/test/test_coroutines.py @@ -1,3 +1,4 @@ +import ast import contextlib import copy import inspect @@ -407,6 +408,118 @@ async def bar(): with self.subTest(code=code), self.assertRaises(SyntaxError): compile(code, "", "exec") + def test_async_comprehension_scope(self): + # List/set/dict comprehensions with await or async for are allowed + # only in async functions, or at module level with top-level await. + allowed = [ + "async def f():\n [await x for x in y]", + "async def f():\n {await x for x in y}", + "async def f():\n {k: await x for k, x in y}", + "async def f():\n [x async for x in y]", + "async def f():\n {x async for x in y}", + "async def f():\n {k: x async for k, x in y}", + "async def f():\n [[await x for x in y] for y in z]", + # Defaults, bases, and genexp iterables are evaluated in the + # enclosing scope. + "async def outer():\n async def f(x=[await y for y in z]): pass", + "async def f():\n class C([await x for x in y]): pass", + "async def f():\n (x for x in [await y for y in z])", + "async def f():\n (x for x in [y async for y in z])", + ] + for code in allowed: + with self.subTest(code=code): + compile(code, "", "exec") + + # Generator expressions with await are async genexps and may appear + # outside async functions. A listcomp nested in a genexp body is also + # allowed (the genexp becomes an async generator). + for code in [ + "(await x for x in y)", + "def f():\n (await x for x in y)", + "class C:\n (await x for x in y)", + "lambda: (await x for x in y)", + "([await x for x in y] for y in z)", + "def f():\n ([await x for x in y] for y in z)", + "class C:\n ([await x for x in y] for y in z)", + "async def f():\n ([await x for x in y] for y in z)", + ]: + with self.subTest(code=code): + compile(code, "", "exec") + + err = "asynchronous comprehension outside of an asynchronous function" + invalid = [ + "[await x for x in y]", + "{await x for x in y}", + "{k: await x for k, x in y}", + "[x async for x in y]", + "{x async for x in y}", + "{k: x async for k, x in y}", + "[[await x for x in y] for y in z]", + "[[x async for x in y] for y in z]", + "def f():\n [await x for x in y]", + "def f():\n [x async for x in y]", + "async def f():\n def g():\n [await x for x in y]", + "class C:\n [await x for x in y]", + "class C:\n {await x for x in y}", + "class C:\n {k: await x for k, x in y}", + "class C:\n [x async for x in y]", + "class C:\n [[await x for x in y] for y in z]", + "async def f():\n class C:\n x = [await y for y in z]", + "async def f():\n class C:\n x = [y async for y in z]", + # Lambdas are never async, even inside async def. + "lambda: [await x for x in y]", + "async def f():\n lambda: [await x for x in y]", + "class C:\n f = lambda: [await x for x in y]", + # Defaults, bases, and genexp iterables run in the enclosing scope. + "(x for x in [await y for y in z])", + "(x for x in [y async for y in z])", + "def f():\n (x for x in [await y for y in z])", + "async def f(x=[await y for y in z]): pass", + "def f(x=[await y for y in z]): pass", + "class C:\n def f(self, x=[await y for y in z]): pass", + "class C([await x for x in y]): pass", + # Type aliases and type-parameter scopes. + "type T = [await x for x in y]", + "type T = [x async for x in y]", + "def f[T=[await x for x in y]](): pass", + "def f[T: [await x for x in y]](): pass", + "async def f[T=[await x for x in y]](): pass", + "async def f(x: [await y for y in z]): pass", + ] + for code in invalid: + with self.subTest(code=code): + support.check_syntax_error(self, code, err) + + support.check_syntax_error( + self, "await x", "'await' outside function") + support.check_syntax_error( + self, "class C:\n await x", "'await' outside function") + support.check_syntax_error( + self, "def f():\n await x", "'await' outside async function") + + flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT + for code in [ + "[await x for x in y]", + "async def f(x=[await y for y in z]): pass", + "class C([await x for x in y]): pass", + "f'{[await x for x in y]}'", + "(x for x in [await y for y in z])", + "(x for x in [y async for y in z])", + ]: + with self.subTest(code=code, tla=True): + compile(code, "", "exec", flags=flags) + still_invalid = [ + "lambda: [await x for x in y]", + "def f():\n (x for x in [await y for y in z])", + "class C:\n def f(self, x=[await y for y in z]): pass", + "type T = [await x for x in y]", + "async def f[T=[await x for x in y]](): pass", + ] + for code in still_invalid: + with self.subTest(code=code, tla=True): + with self.assertRaisesRegex(SyntaxError, err): + compile(code, "", "exec", flags=flags) + def test_badsyntax_2(self): samples = [ """def foo(): diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 3899eac6be3b3a..10106c3ada9ba5 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -2499,6 +2499,35 @@ def test_pax_global_header(self): finally: tar.close() + def test_pax_global_header_empty_archive(self): + # An archive that contains only a global header and no regular + # members should be opened successfully (gh-149578). + pax_headers = {"foo": "bar"} + + # Create a PAX archive with global headers but no file entries. + with tarfile.open(tmpname, "w", format=tarfile.PAX_FORMAT, + pax_headers=pax_headers): + pass + + # Reading the archive should work and preserve global headers. + with tarfile.open(tmpname) as tar: + self.assertEqual(tar.pax_headers, pax_headers) + self.assertEqual(tar.getmembers(), []) + + # Appending to the archive should work. + with tarfile.open(tmpname, "a") as tar: + self.assertEqual(tar.pax_headers, pax_headers) + self.assertEqual(tar.getmembers(), []) + tar.addfile(tarfile.TarInfo("test")) + + # Verify the appended member is present and global headers + # are preserved. + with tarfile.open(tmpname) as tar: + self.assertEqual(tar.pax_headers, pax_headers) + members = tar.getmembers() + self.assertEqual(len(members), 1) + self.assertEqual(members[0].name, "test") + def test_pax_extended_header(self): # The fields from the pax header have priority over the # TarInfo. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-01-08-38.gh-issue-156371.USpwKG.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-01-08-38.gh-issue-156371.USpwKG.rst new file mode 100644 index 00000000000000..31ebff6f247f7e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-26-01-08-38.gh-issue-156371.USpwKG.rst @@ -0,0 +1,2 @@ +Fix missing ``PyRefTracer_DESTROY`` events for trashcan deferred objects. +Patch by Donghee Na. diff --git a/Misc/NEWS.d/next/Library/2026-05-10-12-00-00.gh-issue-149578.5cgq7iV.rst b/Misc/NEWS.d/next/Library/2026-05-10-12-00-00.gh-issue-149578.5cgq7iV.rst new file mode 100644 index 00000000000000..eb738df5beb0c5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-10-12-00-00.gh-issue-149578.5cgq7iV.rst @@ -0,0 +1,3 @@ +Fix :func:`tarfile.open` failing with :exc:`~tarfile.ReadError` when +opening a PAX format tar archive that contains only global headers and +no regular members. diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index c01197d15bad5f..f6009e7e73f249 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -39,6 +39,7 @@ static struct PyModuleDef _testcapimodule; // Module state typedef struct { PyObject *error; // _testcapi.error object + Py_ssize_t list_destroys; } testcapistate_t; static testcapistate_t* @@ -2412,6 +2413,36 @@ test_reftracer(PyObject *ob, PyObject *Py_UNUSED(ignored)) return NULL; } +static int +_listdestroytracer(PyObject *obj, PyRefTracerEvent event, void *data) +{ + if (event == PyRefTracer_DESTROY && PyList_CheckExact(obj)) { + _Py_atomic_add_ssize((Py_ssize_t *)data, 1); + } + return 0; +} + +static PyObject * +start_counting_list_destroys(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + testcapistate_t *state = get_testcapi_state(self); + _Py_atomic_store_ssize(&state->list_destroys, 0); + if (PyRefTracer_SetTracer(_listdestroytracer, &state->list_destroys) != 0) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +stop_counting_list_destroys(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + if (PyRefTracer_SetTracer(NULL, NULL) != 0) { + return NULL; + } + return PyLong_FromSsize_t( + _Py_atomic_load_ssize(&get_testcapi_state(self)->list_destroys)); +} + static PyObject * function_set_warning(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { @@ -3016,6 +3047,8 @@ static PyMethodDef TestMethods[] = { {"test_buildvalue_N", test_buildvalue_N, METH_NOARGS}, {"test_buildvalue_p", test_buildvalue_p, METH_NOARGS}, {"test_reftracer", test_reftracer, METH_NOARGS}, + {"start_counting_list_destroys", start_counting_list_destroys, METH_NOARGS}, + {"stop_counting_list_destroys", stop_counting_list_destroys, METH_NOARGS}, {"_test_thread_state", test_thread_state, METH_VARARGS}, {"gilstate_ensure_release", gilstate_ensure_release, METH_NOARGS}, #ifndef MS_WINDOWS diff --git a/Objects/object.c b/Objects/object.c index c0cb0da7a0d92e..856d9fc41a4154 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -3228,6 +3228,10 @@ _PyTrash_thread_destroy_chain(PyThreadState *tstate) * up distorting allocation statistics. */ _PyObject_ASSERT(op, Py_REFCNT(op) == 0); +#ifdef Py_TRACE_REFS + _Py_ForgetReference(op); +#endif + _PyReftracerTrack(op, PyRefTracer_DESTROY); (*dealloc)(op); } }