Issue #16104: Allow compileall to do parallel bytecode compilation. · pythoncapi/cpython@f1a8df0 · GitHub
Skip to content

Commit f1a8df0

Browse files
committed
Issue python#16104: Allow compileall to do parallel bytecode compilation.
Both compileall.compile_dir() and the CLI for compileall now allow for specifying how many workers to use (or 0 to use all CPUs). Thanks to Claudiu Popa for the patch.
1 parent a56411e commit f1a8df0

4 files changed

Lines changed: 137 additions & 25 deletions

File tree

Doc/library/compileall.rst

Lines changed: 17 additions & 2 deletions

Doc/whatsnew/3.5.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,13 @@ New Modules
134134
Improved Modules
135135
================
136136

137+
compileall
138+
----------
139+
140+
* :func:`compileall.compile_dir` and :mod:`compileall`'s command-line interface
141+
can now do parallel bytecode compilation.
142+
(Contributed by Claudiu Popa in :issue:`16104`).
143+
137144
doctest
138145
-------
139146

Lib/compileall.py

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,15 @@
1616
import py_compile
1717
import struct
1818

19-
__all__ = ["compile_dir","compile_file","compile_path"]
19+
try:
20+
from concurrent.futures import ProcessPoolExecutor
21+
except ImportError:
22+
ProcessPoolExecutor = None
23+
from functools import partial
2024

21-
def compile_dir(dir, maxlevels=10, ddir=None, force=False, rx=None,
22-
quiet=False, legacy=False, optimize=-1):
23-
"""Byte-compile all modules in the given directory tree.
24-
25-
Arguments (only dir is required):
25+
__all__ = ["compile_dir","compile_file","compile_path"]
2626

27-
dir: the directory to byte-compile
28-
maxlevels: maximum recursion level (default 10)
29-
ddir: the directory that will be prepended to the path to the
30-
file as it is compiled into each byte-code file.
31-
force: if True, force compilation, even if timestamps are up-to-date
32-
quiet: if True, be quiet during compilation
33-
legacy: if True, produce legacy pyc paths instead of PEP 3147 paths
34-
optimize: optimization level or -1 for level of the interpreter
35-
"""
27+
def _walk_dir(dir, ddir=None, maxlevels=10, quiet=False):
3628
if not quiet:
3729
print('Listing {!r}...'.format(dir))
3830
try:
@@ -41,7 +33,6 @@ def compile_dir(dir, maxlevels=10, ddir=None, force=False, rx=None,
4133
print("Can't list {!r}".format(dir))
4234
names = []
4335
names.sort()
44-
success = 1
4536
for name in names:
4637
if name == '__pycache__':
4738
continue
@@ -51,13 +42,50 @@ def compile_dir(dir, maxlevels=10, ddir=None, force=False, rx=None,
5142
else:
5243
dfile = None
5344
if not os.path.isdir(fullname):
54-
if not compile_file(fullname, ddir, force, rx, quiet,
55-
legacy, optimize):
56-
success = 0
45+
yield fullname
5746
elif (maxlevels > 0 and name != os.curdir and name != os.pardir and
5847
os.path.isdir(fullname) and not os.path.islink(fullname)):
59-
if not compile_dir(fullname, maxlevels - 1, dfile, force, rx,
60-
quiet, legacy, optimize):
48+
yield from _walk_dir(fullname, ddir=dfile,
49+
maxlevels=maxlevels - 1, quiet=quiet)
50+
51+
def compile_dir(dir, maxlevels=10, ddir=None, force=False, rx=None,
52+
quiet=False, legacy=False, optimize=-1, workers=1):
53+
"""Byte-compile all modules in the given directory tree.
54+
55+
Arguments (only dir is required):
56+
57+
dir: the directory to byte-compile
58+
maxlevels: maximum recursion level (default 10)
59+
ddir: the directory that will be prepended to the path to the
60+
file as it is compiled into each byte-code file.
61+
force: if True, force compilation, even if timestamps are up-to-date
62+
quiet: if True, be quiet during compilation
63+
legacy: if True, produce legacy pyc paths instead of PEP 3147 paths
64+
optimize: optimization level or -1 for level of the interpreter
65+
workers: maximum number of parallel workers
66+
"""
67+
files = _walk_dir(dir, quiet=quiet, maxlevels=maxlevels,
68+
ddir=ddir)
69+
success = 1
70+
if workers is not None and workers != 1:
71+
if workers < 0:
72+
raise ValueError('workers must be greater or equal to 0')
73+
if ProcessPoolExecutor is None:
74+
raise NotImplementedError('multiprocessing support not available')
75+
76+
workers = workers or None
77+
with ProcessPoolExecutor(max_workers=workers) as executor:
78+
results = executor.map(partial(compile_file,
79+
ddir=ddir, force=force,
80+
rx=rx, quiet=quiet,
81+
legacy=legacy,
82+
optimize=optimize),
83+
files)
84+
success = min(results, default=1)
85+
else:
86+
for file in files:
87+
if not compile_file(file, ddir, force, rx, quiet,
88+
legacy, optimize):
6189
success = 0
6290
return success
6391

@@ -196,8 +224,10 @@ def main():
196224
help=('zero or more file and directory names '
197225
'to compile; if no arguments given, defaults '
198226
'to the equivalent of -l sys.path'))
199-
args = parser.parse_args()
227+
parser.add_argument('-j', '--workers', default=1,
228+
type=int, help='Run compileall concurrently')
200229

230+
args = parser.parse_args()
201231
compile_dests = args.compile_dest
202232

203233
if (args.ddir and (len(compile_dests) != 1
@@ -223,6 +253,9 @@ def main():
223253
print("Error reading file list {}".format(args.flist))
224254
return False
225255

256+
if args.workers is not None:
257+
args.workers = args.workers or None
258+
226259
success = True
227260
try:
228261
if compile_dests:
@@ -234,7 +267,7 @@ def main():
234267
else:
235268
if not compile_dir(dest, maxlevels, args.ddir,
236269
args.force, args.rx, args.quiet,
237-
args.legacy):
270+
args.legacy, workers=args.workers):
238271
success = False
239272
return success
240273
else:

Lib/test/test_compileall.py

Lines changed: 57 additions & 0 deletions

0 commit comments

Comments
 (0)