Skip to content

Commit 56beaf2

Browse files
Merge branch 'main' into asyncio-subprocess-cancellation
2 parents ede4cc2 + 680a5d0 commit 56beaf2

File tree

10 files changed

+137
-129
lines changed

10 files changed

+137
-129
lines changed

‎Doc/library/code.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ build applications which provide an interactive interpreter prompt.
2929
module.
3030

3131

32-
.. class:: InteractiveConsole(locals=None, filename="<console>", local_exit=False)
32+
.. class:: InteractiveConsole(locals=None, filename="<console>", *, local_exit=False)
3333

3434
Closely emulate the behavior of the interactive Python interpreter. This class
3535
builds on :class:`InteractiveInterpreter` and adds prompting using the familiar

‎Lib/email/message.py‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,25 @@ def _parseparam(s):
7474
# RDM This might be a Header, so for now stringify it.
7575
s = ';' + str(s)
7676
plist = []
77-
while s[:1] == ';':
78-
s = s[1:]
79-
end = s.find(';')
80-
while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
81-
end = s.find(';', end + 1)
77+
start = 0
78+
while s.find(';', start) == start:
79+
start += 1
80+
end = s.find(';', start)
81+
ind, diff = start, 0
82+
while end > 0:
83+
diff += s.count('"', ind, end) - s.count('\\"', ind, end)
84+
if diff % 2 == 0:
85+
break
86+
end, ind = ind, s.find(';', end + 1)
8287
if end < 0:
8388
end = len(s)
84-
f = s[:end]
85-
if '=' in f:
86-
i = f.index('=')
87-
f = f[:i].strip().lower() + '=' + f[i+1:].strip()
89+
i = s.find('=', start, end)
90+
if i == -1:
91+
f = s[start:end]
92+
else:
93+
f = s[start:i].rstrip().lower() + '=' + s[i+1:end].lstrip()
8894
plist.append(f.strip())
89-
s = s[end:]
95+
start = end
9096
return plist
9197

9298

‎Lib/http/client.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,8 +1391,7 @@ def getresponse(self):
13911391
"""Get the response from the server.
13921392
13931393
If the HTTPConnection is in the correct state, returns an
1394-
instance of HTTPResponse or of whatever object is returned by
1395-
the response_class variable.
1394+
instance of HTTPResponse.
13961395
13971396
If a request has not been sent or if a previous response has
13981397
not be handled, ResponseNotReady is raised. If the HTTP

‎Lib/ntpath.py‎

Lines changed: 41 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -400,17 +400,23 @@ def expanduser(path):
400400
# XXX With COMMAND.COM you can use any characters in a variable name,
401401
# XXX except '^|<>='.
402402

403+
_varpattern = r"'[^']*'?|%(%|[^%]*%?)|\$(\$|[-\w]+|\{[^}]*\}?)"
404+
_varsub = None
405+
_varsubb = None
406+
403407
def expandvars(path):
404408
"""Expand shell variables of the forms $var, ${var} and %var%.
405409
406410
Unknown variables are left unchanged."""
407411
path = os.fspath(path)
412+
global _varsub, _varsubb
408413
if isinstance(path, bytes):
409414
if b'$' not in path and b'%' not in path:
410415
return path
411-
import string
412-
varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii')
413-
quote = b'\''
416+
if not _varsubb:
417+
import re
418+
_varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
419+
sub = _varsubb
414420
percent = b'%'
415421
brace = b'{'
416422
rbrace = b'}'
@@ -419,94 +425,44 @@ def expandvars(path):
419425
else:
420426
if '$' not in path and '%' not in path:
421427
return path
422-
import string
423-
varchars = string.ascii_letters + string.digits + '_-'
424-
quote = '\''
428+
if not _varsub:
429+
import re
430+
_varsub = re.compile(_varpattern, re.ASCII).sub
431+
sub = _varsub
425432
percent = '%'
426433
brace = '{'
427434
rbrace = '}'
428435
dollar = '$'
429436
environ = os.environ
430-
res = path[:0]
431-
index = 0
432-
pathlen = len(path)
433-
while index < pathlen:
434-
c = path[index:index+1]
435-
if c == quote: # no expansion within single quotes
436-
path = path[index + 1:]
437-
pathlen = len(path)
438-
try:
439-
index = path.index(c)
440-
res += c + path[:index + 1]
441-
except ValueError:
442-
res += c + path
443-
index = pathlen - 1
444-
elif c == percent: # variable or '%'
445-
if path[index + 1:index + 2] == percent:
446-
res += c
447-
index += 1
448-
else:
449-
path = path[index+1:]
450-
pathlen = len(path)
451-
try:
452-
index = path.index(percent)
453-
except ValueError:
454-
res += percent + path
455-
index = pathlen - 1
456-
else:
457-
var = path[:index]
458-
try:
459-
if environ is None:
460-
value = os.fsencode(os.environ[os.fsdecode(var)])
461-
else:
462-
value = environ[var]
463-
except KeyError:
464-
value = percent + var + percent
465-
res += value
466-
elif c == dollar: # variable or '$$'
467-
if path[index + 1:index + 2] == dollar:
468-
res += c
469-
index += 1
470-
elif path[index + 1:index + 2] == brace:
471-
path = path[index+2:]
472-
pathlen = len(path)
473-
try:
474-
index = path.index(rbrace)
475-
except ValueError:
476-
res += dollar + brace + path
477-
index = pathlen - 1
478-
else:
479-
var = path[:index]
480-
try:
481-
if environ is None:
482-
value = os.fsencode(os.environ[os.fsdecode(var)])
483-
else:
484-
value = environ[var]
485-
except KeyError:
486-
value = dollar + brace + var + rbrace
487-
res += value
488-
else:
489-
var = path[:0]
490-
index += 1
491-
c = path[index:index + 1]
492-
while c and c in varchars:
493-
var += c
494-
index += 1
495-
c = path[index:index + 1]
496-
try:
497-
if environ is None:
498-
value = os.fsencode(os.environ[os.fsdecode(var)])
499-
else:
500-
value = environ[var]
501-
except KeyError:
502-
value = dollar + var
503-
res += value
504-
if c:
505-
index -= 1
437+
438+
def repl(m):
439+
lastindex = m.lastindex
440+
if lastindex is None:
441+
return m[0]
442+
name = m[lastindex]
443+
if lastindex == 1:
444+
if name == percent:
445+
return name
446+
if not name.endswith(percent):
447+
return m[0]
448+
name = name[:-1]
506449
else:
507-
res += c
508-
index += 1
509-
return res
450+
if name == dollar:
451+
return name
452+
if name.startswith(brace):
453+
if not name.endswith(rbrace):
454+
return m[0]
455+
name = name[1:-1]
456+
457+
try:
458+
if environ is None:
459+
return os.fsencode(os.environ[os.fsdecode(name)])
460+
else:
461+
return environ[name]
462+
except KeyError:
463+
return m[0]
464+
465+
return sub(repl, path)
510466

511467

512468
# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B.

‎Lib/posixpath.py‎

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -285,56 +285,53 @@ def expanduser(path):
285285
# This expands the forms $variable and ${variable} only.
286286
# Non-existent variables are left unchanged.
287287

288-
_varprog = None
289-
_varprogb = None
288+
_varpattern = r'\$(\w+|\{[^}]*\}?)'
289+
_varsub = None
290+
_varsubb = None
290291

291292
def expandvars(path):
292293
"""Expand shell variables of form $var and ${var}. Unknown variables
293294
are left unchanged."""
294295
path = os.fspath(path)
295-
global _varprog, _varprogb
296+
global _varsub, _varsubb
296297
if isinstance(path, bytes):
297298
if b'$' not in path:
298299
return path
299-
if not _varprogb:
300+
if not _varsubb:
300301
import re
301-
_varprogb = re.compile(br'\$(\w+|\{[^}]*\})', re.ASCII)
302-
search = _varprogb.search
302+
_varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
303+
sub = _varsubb
303304
start = b'{'
304305
end = b'}'
305306
environ = getattr(os, 'environb', None)
306307
else:
307308
if '$' not in path:
308309
return path
309-
if not _varprog:
310+
if not _varsub:
310311
import re
311-
_varprog = re.compile(r'\$(\w+|\{[^}]*\})', re.ASCII)
312-
search = _varprog.search
312+
_varsub = re.compile(_varpattern, re.ASCII).sub
313+
sub = _varsub
313314
start = '{'
314315
end = '}'
315316
environ = os.environ
316-
i = 0
317-
while True:
318-
m = search(path, i)
319-
if not m:
320-
break
321-
i, j = m.span(0)
322-
name = m.group(1)
323-
if name.startswith(start) and name.endswith(end):
317+
318+
def repl(m):
319+
name = m[1]
320+
if name.startswith(start):
321+
if not name.endswith(end):
322+
return m[0]
324323
name = name[1:-1]
325324
try:
326325
if environ is None:
327326
value = os.fsencode(os.environ[os.fsdecode(name)])
328327
else:
329328
value = environ[name]
330329
except KeyError:
331-
i = j
330+
return m[0]
332331
else:
333-
tail = path[j:]
334-
path = path[:i] + value
335-
i = len(path)
336-
path += tail
337-
return path
332+
return value
333+
334+
return sub(repl, path)
338335

339336

340337
# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B.

‎Lib/test/test_email/test_email.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,27 @@ def test_get_param_with_quotes(self):
481481
"Content-Type: foo; bar*0=\"baz\\\"foobar\"; bar*1=\"\\\"baz\"")
482482
self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz')
483483

484+
def test_get_param_linear_complexity(self):
485+
# Ensure that email.message._parseparam() is fast.
486+
# See https://github.com/python/cpython/issues/136063.
487+
N = 100_000
488+
for s, r in [
489+
("", ""),
490+
("foo=bar", "foo=bar"),
491+
(" FOO = bar ", "foo=bar"),
492+
]:
493+
with self.subTest(s=s, r=r, N=N):
494+
src = f'{s};' * (N - 1) + s
495+
res = email.message._parseparam(src)
496+
self.assertEqual(len(res), N)
497+
self.assertEqual(len(set(res)), 1)
498+
self.assertEqual(res[0], r)
499+
500+
# This will be considered as a single parameter.
501+
malformed = 's="' + ';' * (N - 1)
502+
res = email.message._parseparam(malformed)
503+
self.assertEqual(res, [malformed])
504+
484505
def test_field_containment(self):
485506
msg = email.message_from_string('Header: exists')
486507
self.assertIn('header', msg)

‎Lib/test/test_genericpath.py‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
import sys
1010
import unittest
1111
import warnings
12-
from test.support import (
13-
is_apple, os_helper, warnings_helper
14-
)
12+
from test import support
13+
from test.support import os_helper
14+
from test.support import warnings_helper
1515
from test.support.script_helper import assert_python_ok
1616
from test.support.os_helper import FakePath
1717

@@ -462,6 +462,19 @@ def check(value, expected):
462462
os.fsencode('$bar%s bar' % nonascii))
463463
check(b'$spam}bar', os.fsencode('%s}bar' % nonascii))
464464

465+
@support.requires_resource('cpu')
466+
def test_expandvars_large(self):
467+
expandvars = self.pathmodule.expandvars
468+
with os_helper.EnvironmentVarGuard() as env:
469+
env.clear()
470+
env["A"] = "B"
471+
n = 100_000
472+
self.assertEqual(expandvars('$A'*n), 'B'*n)
473+
self.assertEqual(expandvars('${A}'*n), 'B'*n)
474+
self.assertEqual(expandvars('$A!'*n), 'B!'*n)
475+
self.assertEqual(expandvars('${A}A'*n), 'BA'*n)
476+
self.assertEqual(expandvars('${'*10*n), '${'*10*n)
477+
465478
def test_abspath(self):
466479
self.assertIn("foo", self.pathmodule.abspath("foo"))
467480
with warnings.catch_warnings():
@@ -519,7 +532,7 @@ def test_nonascii_abspath(self):
519532
# directory (when the bytes name is used).
520533
and sys.platform not in {
521534
"win32", "emscripten", "wasi"
522-
} and not is_apple
535+
} and not support.is_apple
523536
):
524537
name = os_helper.TESTFN_UNDECODABLE
525538
elif os_helper.TESTFN_NONASCII:

0 commit comments

Comments
 (0)