Skip to content

Commit 87c9baf

Browse files
authored
Merge pull request #242 from python/master
Sync Fork from Upstream Repo
2 parents c989e87 + 8c579b1 commit 87c9baf

33 files changed

+496
-133
lines changed

Doc/whatsnew/3.9.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,17 @@ case), and one used ``__VENV_NAME__`` instead.
315315
Optimizations
316316
=============
317317

318+
* Optimized the idiom for assignment a temporary variable in comprehensions.
319+
Now ``for y in [expr]`` in comprehensions is as fast as a simple assignment
320+
``y = expr``. For example:
321+
322+
sums = [s for s in [0] for x in data for s in [s + x]]
323+
324+
Unlike to the ``:=`` operator this idiom does not leak a variable to the
325+
outer scope.
326+
327+
(Contributed by Serhiy Storchaka in :issue:`32856`.)
328+
318329

319330
Build and C API Changes
320331
=======================

Lib/importlib/metadata.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ class FastPath:
391391

392392
def __init__(self, root):
393393
self.root = root
394+
self.base = os.path.basename(root).lower()
394395

395396
def joinpath(self, child):
396397
return pathlib.Path(self.root, child)
@@ -413,12 +414,11 @@ def zip_children(self):
413414
)
414415

415416
def is_egg(self, search):
416-
root_n_low = os.path.split(self.root)[1].lower()
417-
417+
base = self.base
418418
return (
419-
root_n_low == search.normalized + '.egg'
420-
or root_n_low.startswith(search.prefix)
421-
and root_n_low.endswith('.egg'))
419+
base == search.versionless_egg_name
420+
or base.startswith(search.prefix)
421+
and base.endswith('.egg'))
422422

423423
def search(self, name):
424424
for child in self.children():
@@ -439,6 +439,7 @@ class Prepared:
439439
prefix = ''
440440
suffixes = '.dist-info', '.egg-info'
441441
exact_matches = [''][:0]
442+
versionless_egg_name = ''
442443

443444
def __init__(self, name):
444445
self.name = name
@@ -448,6 +449,7 @@ def __init__(self, name):
448449
self.prefix = self.normalized + '-'
449450
self.exact_matches = [
450451
self.normalized + suffix for suffix in self.suffixes]
452+
self.versionless_egg_name = self.normalized + '.egg'
451453

452454

453455
class MetadataPathFinder(DistributionFinder):

Lib/os.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,10 @@ def walk(top, topdown=True, onerror=None, followlinks=False):
336336
dirs.remove('CVS') # don't visit CVS directories
337337
338338
"""
339-
top = fspath(top)
339+
sys.audit("os.walk", top, topdown, onerror, followlinks)
340+
return _walk(fspath(top), topdown, onerror, followlinks)
341+
342+
def _walk(top, topdown, onerror, followlinks):
340343
dirs = []
341344
nondirs = []
342345
walk_dirs = []
@@ -410,11 +413,11 @@ def walk(top, topdown=True, onerror=None, followlinks=False):
410413
# the caller can replace the directory entry during the "yield"
411414
# above.
412415
if followlinks or not islink(new_path):
413-
yield from walk(new_path, topdown, onerror, followlinks)
416+
yield from _walk(new_path, topdown, onerror, followlinks)
414417
else:
415418
# Recurse into sub-directories
416419
for new_path in walk_dirs:
417-
yield from walk(new_path, topdown, onerror, followlinks)
420+
yield from _walk(new_path, topdown, onerror, followlinks)
418421
# Yield after recursion if going bottom up
419422
yield top, dirs, nondirs
420423

@@ -455,6 +458,7 @@ def fwalk(top=".", topdown=True, onerror=None, *, follow_symlinks=False, dir_fd=
455458
if 'CVS' in dirs:
456459
dirs.remove('CVS') # don't visit CVS directories
457460
"""
461+
sys.audit("os.fwalk", top, topdown, onerror, follow_symlinks, dir_fd)
458462
if not isinstance(top, int) or not hasattr(top, '__index__'):
459463
top = fspath(top)
460464
# Note: To guard against symlink races, we use the standard

Lib/pathlib.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,7 @@ def glob(self, pattern):
11341134
"""Iterate over this subtree and yield all existing files (of any
11351135
kind, including directories) matching the given relative pattern.
11361136
"""
1137+
sys.audit("pathlib.Path.glob", self, pattern)
11371138
if not pattern:
11381139
raise ValueError("Unacceptable pattern: {!r}".format(pattern))
11391140
drv, root, pattern_parts = self._flavour.parse_parts((pattern,))
@@ -1148,6 +1149,7 @@ def rglob(self, pattern):
11481149
directories) matching the given relative pattern, anywhere in
11491150
this subtree.
11501151
"""
1152+
sys.audit("pathlib.Path.rglob", self, pattern)
11511153
drv, root, pattern_parts = self._flavour.parse_parts((pattern,))
11521154
if drv or root:
11531155
raise NotImplementedError("Non-relative patterns are unsupported")

Lib/test/test_dictcomps.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,22 @@ def add_call(pos, value):
111111
self.assertEqual(actual, expected)
112112
self.assertEqual(actual_calls, expected_calls)
113113

114+
def test_assignment_idiom_in_comprehensions(self):
115+
expected = {1: 1, 2: 4, 3: 9, 4: 16}
116+
actual = {j: j*j for i in range(4) for j in [i+1]}
117+
self.assertEqual(actual, expected)
118+
expected = {3: 2, 5: 6, 7: 12, 9: 20}
119+
actual = {j+k: j*k for i in range(4) for j in [i+1] for k in [j+1]}
120+
self.assertEqual(actual, expected)
121+
expected = {3: 2, 5: 6, 7: 12, 9: 20}
122+
actual = {j+k: j*k for i in range(4) for j, k in [(i+1, i+2)]}
123+
self.assertEqual(actual, expected)
124+
125+
def test_star_expression(self):
126+
expected = {0: 0, 1: 1, 2: 4, 3: 9}
127+
self.assertEqual({i: i*i for i in [*range(4)]}, expected)
128+
self.assertEqual({i: i*i for i in (*range(4),)}, expected)
129+
130+
114131
if __name__ == "__main__":
115132
unittest.main()

Lib/test/test_exceptions.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,17 +179,25 @@ def ckmsg(src, msg, exception=SyntaxError):
179179
ckmsg(s, "inconsistent use of tabs and spaces in indentation", TabError)
180180

181181
def testSyntaxErrorOffset(self):
182-
def check(src, lineno, offset):
182+
def check(src, lineno, offset, encoding='utf-8'):
183183
with self.assertRaises(SyntaxError) as cm:
184184
compile(src, '<fragment>', 'exec')
185185
self.assertEqual(cm.exception.lineno, lineno)
186186
self.assertEqual(cm.exception.offset, offset)
187+
if cm.exception.text is not None:
188+
if not isinstance(src, str):
189+
src = src.decode(encoding, 'replace')
190+
line = src.split('\n')[lineno-1]
191+
self.assertEqual(cm.exception.text.rstrip('\n'), line)
187192

188193
check('def fact(x):\n\treturn x!\n', 2, 10)
189194
check('1 +\n', 1, 4)
190195
check('def spam():\n print(1)\n print(2)', 3, 10)
191196
check('Python = "Python" +', 1, 20)
192197
check('Python = "\u1e54\xfd\u0163\u0125\xf2\xf1" +', 1, 20)
198+
check(b'# -*- coding: cp1251 -*-\nPython = "\xcf\xb3\xf2\xee\xed" +',
199+
2, 19, encoding='cp1251')
200+
check(b'Python = "\xcf\xb3\xf2\xee\xed" +', 1, 18)
193201
check('x = "a', 1, 7)
194202
check('lambda x: x = 2', 1, 1)
195203

@@ -205,6 +213,10 @@ def check(src, lineno, offset):
205213
check('0010 + 2', 1, 4)
206214
check('x = 32e-+4', 1, 8)
207215
check('x = 0o9', 1, 6)
216+
check('\u03b1 = 0xI', 1, 6)
217+
check(b'\xce\xb1 = 0xI', 1, 6)
218+
check(b'# -*- coding: iso8859-7 -*-\n\xe1 = 0xI', 2, 6,
219+
encoding='iso8859-7')
208220

209221
# Errors thrown by symtable.c
210222
check('x = [(yield i) for i in range(3)]', 1, 5)

Lib/test/test_genexps.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@
1515
>>> list((i,j) for i in range(4) for j in range(i) )
1616
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2)]
1717
18+
Test the idiom for temporary variable assignment in comprehensions.
19+
20+
>>> list((j*j for i in range(4) for j in [i+1]))
21+
[1, 4, 9, 16]
22+
>>> list((j*k for i in range(4) for j in [i+1] for k in [j+1]))
23+
[2, 6, 12, 20]
24+
>>> list((j*k for i in range(4) for j, k in [(i+1, i+2)]))
25+
[2, 6, 12, 20]
26+
27+
Not assignment
28+
29+
>>> list((i*i for i in [*range(4)]))
30+
[0, 1, 4, 9]
31+
>>> list((i*i for i in (*range(4),)))
32+
[0, 1, 4, 9]
33+
1834
Make sure the induction variable is not exposed
1935
2036
>>> i = 20

Lib/test/test_importlib/fixtures.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,28 @@ def tempdir_as_cwd():
4747
yield tmp
4848

4949

50-
class SiteDir:
50+
@contextlib.contextmanager
51+
def install_finder(finder):
52+
sys.meta_path.append(finder)
53+
try:
54+
yield
55+
finally:
56+
sys.meta_path.remove(finder)
57+
58+
59+
class Fixtures:
5160
def setUp(self):
5261
self.fixtures = ExitStack()
5362
self.addCleanup(self.fixtures.close)
63+
64+
65+
class SiteDir(Fixtures):
66+
def setUp(self):
67+
super(SiteDir, self).setUp()
5468
self.site_dir = self.fixtures.enter_context(tempdir())
5569

5670

57-
class OnSysPath:
71+
class OnSysPath(Fixtures):
5872
@staticmethod
5973
@contextlib.contextmanager
6074
def add_sys_path(dir):
@@ -198,3 +212,8 @@ def build_files(file_defs, prefix=pathlib.Path()):
198212
def DALS(str):
199213
"Dedent and left-strip"
200214
return textwrap.dedent(str).lstrip()
215+
216+
217+
class NullFinder:
218+
def find_module(self, name):
219+
pass

Lib/test/test_importlib/stubs.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import unittest
2+
3+
4+
class fake_filesystem_unittest:
5+
"""
6+
Stubbed version of the pyfakefs module
7+
"""
8+
class TestCase(unittest.TestCase):
9+
def setUpPyfakefs(self):
10+
self.skipTest("pyfakefs not available")

Lib/test/test_importlib/test_main.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
import unittest
88
import importlib.metadata
99

10+
try:
11+
import pyfakefs.fake_filesystem_unittest as ffs
12+
except ImportError:
13+
from .stubs import fake_filesystem_unittest as ffs
14+
1015
from . import fixtures
1116
from importlib.metadata import (
1217
Distribution, EntryPoint,
@@ -185,6 +190,33 @@ def test_egg(self):
185190
version('foo')
186191

187192

193+
class MissingSysPath(fixtures.OnSysPath, unittest.TestCase):
194+
site_dir = '/does-not-exist'
195+
196+
def test_discovery(self):
197+
"""
198+
Discovering distributions should succeed even if
199+
there is an invalid path on sys.path.
200+
"""
201+
importlib.metadata.distributions()
202+
203+
204+
class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase):
205+
site_dir = '/access-denied'
206+
207+
def setUp(self):
208+
super(InaccessibleSysPath, self).setUp()
209+
self.setUpPyfakefs()
210+
self.fs.create_dir(self.site_dir, perm_bits=000)
211+
212+
def test_discovery(self):
213+
"""
214+
Discovering distributions should succeed even if
215+
there is an invalid path on sys.path.
216+
"""
217+
list(importlib.metadata.distributions())
218+
219+
188220
class TestEntryPoints(unittest.TestCase):
189221
def __init__(self, *args):
190222
super(TestEntryPoints, self).__init__(*args)

Lib/test/test_listcomps.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@
1616
>>> [(i,j) for i in range(4) for j in range(i)]
1717
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2)]
1818
19+
Test the idiom for temporary variable assignment in comprehensions.
20+
21+
>>> [j*j for i in range(4) for j in [i+1]]
22+
[1, 4, 9, 16]
23+
>>> [j*k for i in range(4) for j in [i+1] for k in [j+1]]
24+
[2, 6, 12, 20]
25+
>>> [j*k for i in range(4) for j, k in [(i+1, i+2)]]
26+
[2, 6, 12, 20]
27+
28+
Not assignment
29+
30+
>>> [i*i for i in [*range(4)]]
31+
[0, 1, 4, 9]
32+
>>> [i*i for i in (*range(4),)]
33+
[0, 1, 4, 9]
34+
1935
Make sure the induction variable is not exposed
2036
2137
>>> i = 20

Lib/test/test_peepholer.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,20 @@ def f(x):
495495
return 6
496496
self.check_lnotab(f)
497497

498+
def test_assignment_idiom_in_comprehensions(self):
499+
def listcomp():
500+
return [y for x in a for y in [f(x)]]
501+
self.assertEqual(count_instr_recursively(listcomp, 'FOR_ITER'), 1)
502+
def setcomp():
503+
return {y for x in a for y in [f(x)]}
504+
self.assertEqual(count_instr_recursively(setcomp, 'FOR_ITER'), 1)
505+
def dictcomp():
506+
return {y: y for x in a for y in [f(x)]}
507+
self.assertEqual(count_instr_recursively(dictcomp, 'FOR_ITER'), 1)
508+
def genexpr():
509+
return (y for x in a for y in [f(x)])
510+
self.assertEqual(count_instr_recursively(genexpr, 'FOR_ITER'), 1)
511+
498512

499513
class TestBuglets(unittest.TestCase):
500514

Lib/test/test_setcomps.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@
2121
>>> list(sorted({(i,j) for i in range(4) for j in range(i)}))
2222
[(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2)]
2323
24+
Test the idiom for temporary variable assignment in comprehensions.
25+
26+
>>> sorted({j*j for i in range(4) for j in [i+1]})
27+
[1, 4, 9, 16]
28+
>>> sorted({j*k for i in range(4) for j in [i+1] for k in [j+1]})
29+
[2, 6, 12, 20]
30+
>>> sorted({j*k for i in range(4) for j, k in [(i+1, i+2)]})
31+
[2, 6, 12, 20]
32+
33+
Not assignment
34+
35+
>>> sorted({i*i for i in [*range(4)]})
36+
[0, 1, 4, 9]
37+
>>> sorted({i*i for i in (*range(4),)})
38+
[0, 1, 4, 9]
39+
2440
Make sure the induction variable is not exposed
2541
2642
>>> i = 20

0 commit comments

Comments
 (0)