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_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/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.