Skip to content

Commit 5d40963

Browse files
generatedunixname1734921407115435meta-codesync[bot]
authored andcommitted
Sync pre-release CPython 3.15 branch from GitHub (2026-08-03)
Summary: Imported python/cpython `3.15.0b4+dev` from upstream rev [`5bc7459`](https://www.github.com/python/cpython/commit/5bc7459474b7d3391b1d566f83728d8822654d1a) (committed 2026-08-03 21:21:11+00:00). # Commit Info - Base: (`3.15.0b4+dev`) - [`07e73c0`](https://www.github.com/python/cpython/commit/07e73c01c5ba2f8ff6b5f237a8a87dfb88787383) (commit date: 2026-08-02 18:49:20+00:00) - Imported: (`3.15.0b4+dev`) - [`5bc7459`](https://www.github.com/python/cpython/commit/5bc7459474b7d3391b1d566f83728d8822654d1a) (commit date: 2026-08-03 21:21:11+00:00) # Noteworthy file changes - Stdlib files (1 added, 1 removed) - Low-signal files (7 added) (NEWS.d, docs, .github) Complete list of added/removed files: https://www.internalfb.com/intern/everpaste/?color=0&handle=GBay8yVwo4cfQaEDALspVNZF0AUCbr0LAAAz Differential Revision: D114690383 fbshipit-source-id: 9765de93257f2638e4a43d6bdb20b5c38aa962aa
1 parent 57a9eb7 commit 5d40963

28 files changed

Lines changed: 351 additions & 68 deletions

Doc/library/asyncio-task.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,10 @@ unless it is :exc:`asyncio.CancelledError`,
433433
is also included in the exception group.
434434
The same special case is made for
435435
:exc:`KeyboardInterrupt` and :exc:`SystemExit` as in the previous paragraph.
436+
There is an additional special case made only for the body of the
437+
``async with``: if it raises :exc:`GeneratorExit` and none of the
438+
other tasks raise exceptions that would be reported, then the
439+
:exc:`GeneratorExit` is reraised.
436440

437441
Task groups are careful not to mix up the internal cancellation used to
438442
"wake up" their :meth:`~object.__aexit__` with cancellation requests
@@ -456,6 +460,10 @@ reported by :meth:`asyncio.Task.cancelling`.
456460
Improved handling of simultaneous internal and external cancellations
457461
and correct preservation of cancellation counts.
458462

463+
.. versionchanged:: 3.15
464+
465+
Addition of the special case for :exc:`GeneratorExit`.
466+
459467
Sleeping
460468
========
461469

Lib/asyncio/taskgroups.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,23 @@ async def _aexit(self, et, exc):
174174
self._parent_task.uncancel()
175175
self._parent_task.cancel()
176176
try:
177-
raise BaseExceptionGroup(
178-
'unhandled errors in a TaskGroup',
179-
self._errors,
180-
) from None
177+
# If the *only* error is a GeneratorExit from the body
178+
# of the group, then instead of raising an
179+
# ExceptionGroup we raise GeneratorExit. This ensures
180+
# that async generators that use TaskGroup properly
181+
# swallow the exception on `aclose()` while ensuring
182+
# that no exceptions from subtasks are swallowed.
183+
if (
184+
et is not None
185+
and issubclass(et, GeneratorExit)
186+
and len(self._errors) == 1
187+
):
188+
raise exc
189+
else:
190+
raise BaseExceptionGroup(
191+
'unhandled errors in a TaskGroup',
192+
self._errors,
193+
) from None
181194
finally:
182195
exc = None
183196

Lib/csv.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ def sniff(self, sample, delimiters=None):
247247
that order, no matter how many times each of them occurs.
248248
"""
249249

250+
sample = sample.replace('\r\n', '\n').replace('\r', '\n')
251+
250252
quotechar, doublequote, delimiter, skipinitialspace = \
251253
self._guess_quote_and_delimiter(sample, delimiters)
252254
if not delimiter:
@@ -284,12 +286,16 @@ def _guess_quote_and_delimiter(self, data, delimiters):
284286
"""
285287
import re
286288

289+
# The body of a quoted field ends at the first quote which is
290+
# not doubled, as it does for a reader. A lazy ".*?" scans to
291+
# the end of the sample instead, from every start: quadratically.
292+
body = r'(?:(?P=quote){2}|(?!(?P=quote)).)*+'
287293
matches = []
288-
for restr in (r'(?P<delim>[^\w\n"\'])(?P<space> ?)(?P<quote>["\']).*?(?P=quote)(?P=delim)', # ,".*?",
289-
r'(?:^|\n)(?P<quote>["\']).*?(?P=quote)(?P<delim>[^\w\n"\'])(?P<space> ?)', # ".*?",
290-
r'(?P<delim>[^\w\n"\'])(?P<space> ?)(?P<quote>["\']).*?(?P=quote)(?:$|\r|\n)', # ,".*?"
291-
r'(?:^|\n)(?P<quote>["\']).*?(?P=quote)(?:$|\r|\n)'): # ".*?" (no delim, no space)
292-
regexp = re.compile(restr, re.DOTALL | re.MULTILINE)
294+
for restr in (r'(?P<delim>[^\w\n"\'])(?P<space> ?)(?P<quote>["\'])%s(?P=quote)(?P=delim)', # ,"...",
295+
r'(?:^|\n)(?P<quote>["\'])%s(?P=quote)(?P<delim>[^\w\n"\'])(?P<space> ?)', # "...",
296+
r'(?P<delim>[^\w\n"\'])(?P<space> ?)(?P<quote>["\'])%s(?P=quote)(?:$|\n)', # ,"..."
297+
r'(?:^|\n)(?P<quote>["\'])%s(?P=quote)(?:$|\n)'): # "..." (no delim, no space)
298+
regexp = re.compile(restr % body, re.DOTALL | re.MULTILINE)
293299
matches = regexp.findall(data)
294300
if matches:
295301
break
@@ -332,18 +338,22 @@ def _guess_quote_and_delimiter(self, data, delimiters):
332338
delim = ''
333339
skipinitialspace = 0
334340

335-
# if we see an extra quote between delimiters, we've got a
336-
# double quoted format
337-
dq_regexp = re.compile(
338-
r"((%(delim)s)|^)\W*%(quote)s[^%(delim)s\n]*%(quote)s[^%(delim)s\n]*%(quote)s\W*((%(delim)s)|$)" % \
339-
{'delim':re.escape(delim), 'quote':quotechar}, re.MULTILINE)
340-
341-
342-
343-
if dq_regexp.search(data):
344-
doublequote = True
345-
else:
346-
doublequote = False
341+
# A doubled quote character inside a quoted field means
342+
# a double quoted format. Match whole fields, so that a match
343+
# cannot slide across field boundaries.
344+
doublequote = False
345+
if delim:
346+
dq_regexp = re.compile(
347+
r"(?:(?<=%(delim)s)|^)%(space)s%(quote)s" # ,"
348+
r"((?:%(quote)s%(quote)s|[^%(quote)s]++)*+)" # the body
349+
r"%(quote)s(?:%(delim)s|$)" # ",
350+
% {'delim': re.escape(delim), 'quote': quotechar,
351+
# Skipping spaces after a space rescans them.
352+
'space': ' *+' if delim != ' ' else ''},
353+
re.MULTILINE)
354+
dquotechar = quotechar * 2
355+
doublequote = any(dquotechar in m[1]
356+
for m in dq_regexp.finditer(data))
347357

348358
return (quotechar, doublequote, delim, skipinitialspace)
349359

Lib/ensurepip/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
__all__ = ["version", "bootstrap"]
13-
_PIP_VERSION = "26.1.2"
13+
_PIP_VERSION = "26.2"
1414

1515
# Directory of system wheel packages. Some Linux distribution packaging
1616
# policies recommend against bundling dependencies. For example, Fedora
1.73 MB
Binary file not shown.

Lib/test/test_asyncio/test_taskgroups.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,72 @@ async def fn_3():
12271227
self.assertEqual(await race(fn_1, fn_2, fn_3), 1)
12281228
self.assertListEqual(record, ["1 started", "2 started", "3 started", "1 finished"])
12291229

1230+
async def test_taskgroup_generator_exit_01(self):
1231+
# GeneratorExit in a TaskGroup should be fine
1232+
async def gen():
1233+
yield 1
1234+
1235+
async def fn():
1236+
async with asyncio.TaskGroup() as tg:
1237+
async for n in gen():
1238+
yield n
1239+
1240+
g = fn()
1241+
await g.asend(None)
1242+
await g.aclose()
1243+
1244+
async def test_taskgroup_generator_exit_02(self):
1245+
# A lone GeneratorExit in a task should still give an ExceptionGroup
1246+
async def t():
1247+
raise GeneratorExit
1248+
1249+
async def fn():
1250+
async with asyncio.TaskGroup() as tg:
1251+
tg.create_task(t())
1252+
1253+
with self.assertRaises(BaseExceptionGroup) as cm:
1254+
await fn()
1255+
self.assertEqual(get_error_types(cm.exception), {GeneratorExit})
1256+
1257+
async def test_taskgroup_generator_exit_03(self):
1258+
# A GeneratorExit in one task and an error in another should
1259+
# still give an ExceptionGroup
1260+
async def t1():
1261+
raise GeneratorExit
1262+
1263+
async def t2():
1264+
raise AssertionError('t2 failed')
1265+
1266+
async def fn():
1267+
async with asyncio.TaskGroup() as tg:
1268+
tg.create_task(t1())
1269+
tg.create_task(t2())
1270+
1271+
with self.assertRaises(BaseExceptionGroup) as cm:
1272+
await fn()
1273+
1274+
self.assertEqual(get_error_types(cm.exception), {GeneratorExit, AssertionError})
1275+
1276+
async def test_taskgroup_generator_exit_04(self):
1277+
event = asyncio.Event()
1278+
async def t():
1279+
event.set()
1280+
raise AssertionError('t failed')
1281+
1282+
async def fn():
1283+
async with asyncio.TaskGroup() as tg:
1284+
tg.create_task(t())
1285+
yield 1
1286+
1287+
g = fn()
1288+
await g.asend(None)
1289+
await event.wait() # wait for t() to run
1290+
1291+
with self.assertRaises(BaseExceptionGroup) as cm:
1292+
await g.aclose()
1293+
1294+
self.assertEqual(get_error_types(cm.exception), {GeneratorExit, AssertionError})
1295+
12301296

12311297
class TestTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase):
12321298
loop_factory = asyncio.EventLoop

Lib/test/test_csv.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,57 @@ def test_zero_mode_tie_order_colon_first(self):
15641564
sniffer.sniff(sample)
15651565

15661566

1567+
def test_sniff_regex_backtracking(self):
1568+
# gh-109638: this artificial sample used to take minutes.
1569+
sniffer = csv.Sniffer()
1570+
sample = '"",' * 100 + '"' * 100 + '0' + '"' * 100 + '0'
1571+
self.assertEqual(sniffer.sniff(sample).delimiter, ',')
1572+
1573+
def test_sniff_doublequote_across_fields(self):
1574+
# A quoted field which contains the delimiter, followed by
1575+
# an empty quoted field, is not a doubled quote.
1576+
sniffer = csv.Sniffer()
1577+
sample = '",","",","\n' * 4
1578+
dialect = sniffer.sniff(sample)
1579+
self.assertEqual(dialect.delimiter, ',')
1580+
self.assertEqual(dialect.quotechar, '"')
1581+
self.assertIs(dialect.doublequote, False)
1582+
self.assertEqual(next(csv.reader(StringIO(sample), dialect)),
1583+
[',', '', ','])
1584+
1585+
def test_sniff_doublequote_record_separators(self):
1586+
# The record separator ends a field as a delimiter does.
1587+
sniffer = csv.Sniffer()
1588+
for sep in '\n', '\r\n', '\r':
1589+
with self.subTest(sep=sep):
1590+
sample = ('x,"a""b"' + sep + 'y,"c"' + sep) * 2
1591+
self.assertIs(sniffer.sniff(sample).doublequote, True)
1592+
sample = ('"",","' + sep) * 4
1593+
self.assertIs(sniffer.sniff(sample).doublequote, False)
1594+
1595+
def test_sniff_single_column(self):
1596+
# This sample used to be quadratic.
1597+
sniffer = csv.Sniffer()
1598+
sample = '"a"\n' + ' ' * 100000
1599+
with self.assertRaisesRegex(csv.Error, "Could not determine delimiter"):
1600+
sniffer.sniff(sample, delimiters=',;')
1601+
1602+
def test_sniff_space_delimiter(self):
1603+
# This sample used to be quadratic.
1604+
sniffer = csv.Sniffer()
1605+
sample = '"a" "b"\n' + ' ' * 100000
1606+
dialect = sniffer.sniff(sample)
1607+
self.assertEqual(dialect.delimiter, ' ')
1608+
self.assertIs(dialect.doublequote, False)
1609+
1610+
def test_sniff_quoted_single_column(self):
1611+
# gh-98820: this sample used to take minutes.
1612+
sniffer = csv.Sniffer()
1613+
sample = '"abcdefghijklmnopqrstuvwxyz"\n' * 10000
1614+
with self.assertRaisesRegex(csv.Error, "Could not determine delimiter"):
1615+
sniffer.sniff(sample, delimiters=',:|\t')
1616+
1617+
15671618
class NUL:
15681619
def write(s, *args):
15691620
pass

Lib/test/test_pyexpat.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,13 @@ def test_parent_parser_outlives_its_subparsers__chain(self):
996996
del parser
997997
del subparser
998998

999+
def test_subparser_inherits_reparse_deferral(self):
1000+
for enabled in (True, False):
1001+
parser = expat.ParserCreate()
1002+
parser.SetReparseDeferralEnabled(enabled)
1003+
subparser = parser.ExternalEntityParserCreate(None)
1004+
self.assertEqual(subparser.GetReparseDeferralEnabled(), enabled)
1005+
9991006

10001007
class ExternalEntityParserCreateErrorTest(unittest.TestCase):
10011008
"""ExternalEntityParserCreate error paths should not crash or leak

Lib/test/test_with.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import unittest
1111
from collections import deque
1212
from contextlib import _GeneratorContextManager, contextmanager, nullcontext
13+
from _testinternalcapi import SelfInterruptingContextManager
1314

1415

1516
def do_with(obj):
@@ -850,5 +851,21 @@ def exit_raises():
850851
expected)
851852

852853

854+
class InterruptDuringEnter(unittest.TestCase):
855+
856+
def test_exit_called_after_interrupt(self):
857+
cm = SelfInterruptingContextManager()
858+
self.assertFalse(cm.within())
859+
try:
860+
with cm:
861+
self.assertTrue(cm.within())
862+
except KeyboardInterrupt:
863+
self.assertFalse(cm.within())
864+
return
865+
except:
866+
self.fail("Wrong exception raised")
867+
self.fail("No exception raised")
868+
869+
853870
if __name__ == '__main__':
854871
unittest.main()

Makefile.pre.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ DIST= $(DISTFILES) $(DISTDIRS)
284284
LIBRARY= @LIBRARY@
285285
LDLIBRARY= @LDLIBRARY@
286286
BLDLIBRARY= @BLDLIBRARY@
287+
MODULE_LDFLAGS_SHARED=$(if $(LIBPYTHON),$(BLDLIBRARY))
287288
PY3LIBRARY= @PY3LIBRARY@
288289
DLLLIBRARY= @DLLLIBRARY@
289290
LDLIBRARYDIR= @LDLIBRARYDIR@

0 commit comments

Comments
 (0)