Skip to content

Commit 0267335

Browse files
authored
bpo-39769: Fix compileall ddir for subpkgs. (GH-18676)
Fix compileall.compile_dir() ddir= behavior on sub-packages. Fixes compileall.compile_dir's ddir parameter and compileall command line flag `-d` to no longer write the wrong pathname to the generated pyc file for submodules beneath the root of the directory tree being compiled. This fixes a regression introduced with Python 3.5. Also marks the _new_ in 3.9 from PR #16012 parameters to compile_dir as keyword only (as that is the only way they will be used) and fixes an omission of them in one place from the docs.
1 parent 03153dd commit 0267335

File tree

5 files changed

+67
-4
lines changed

5 files changed

+67
-4
lines changed

Doc/library/compileall.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ runtime.
143143
Public functions
144144
----------------
145145

146-
.. function:: compile_dir(dir, maxlevels=sys.getrecursionlimit(), ddir=None, force=False, rx=None, quiet=0, legacy=False, optimize=-1, workers=1, invalidation_mode=None, stripdir=None, prependdir=None, limit_sl_dest=None)
146+
.. function:: compile_dir(dir, maxlevels=sys.getrecursionlimit(), ddir=None, force=False, rx=None, quiet=0, legacy=False, optimize=-1, workers=1, invalidation_mode=None, \*, stripdir=None, prependdir=None, limit_sl_dest=None)
147147

148148
Recursively descend the directory tree named by *dir*, compiling all :file:`.py`
149149
files along the way. Return a true value if all the files compiled successfully,
@@ -221,7 +221,7 @@ Public functions
221221
.. versionchanged:: 3.9
222222
Added *stripdir*, *prependdir* and *limit_sl_dest* arguments.
223223

224-
.. function:: compile_file(fullname, ddir=None, force=False, rx=None, quiet=0, legacy=False, optimize=-1, invalidation_mode=None)
224+
.. function:: compile_file(fullname, ddir=None, force=False, rx=None, quiet=0, legacy=False, optimize=-1, invalidation_mode=None, \*, stripdir=None, prependdir=None, limit_sl_dest=None)
225225

226226
Compile the file with path *fullname*. Return a true value if the file
227227
compiled successfully, and a false value otherwise.

Lib/compileall.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def _walk_dir(dir, maxlevels, quiet=0):
4646

4747
def compile_dir(dir, maxlevels=None, ddir=None, force=False,
4848
rx=None, quiet=0, legacy=False, optimize=-1, workers=1,
49-
invalidation_mode=None, stripdir=None,
49+
invalidation_mode=None, *, stripdir=None,
5050
prependdir=None, limit_sl_dest=None):
5151
"""Byte-compile all modules in the given directory tree.
5252
@@ -72,6 +72,13 @@ def compile_dir(dir, maxlevels=None, ddir=None, force=False,
7272
the defined path
7373
"""
7474
ProcessPoolExecutor = None
75+
if ddir is not None and (stripdir is not None or prependdir is not None):
76+
raise ValueError(("Destination dir (ddir) cannot be used "
77+
"in combination with stripdir or prependdir"))
78+
if ddir is not None:
79+
stripdir = dir
80+
prependdir = ddir
81+
ddir = None
7582
if workers < 0:
7683
raise ValueError('workers must be greater or equal to 0')
7784
if workers != 1:
@@ -111,7 +118,7 @@ def compile_dir(dir, maxlevels=None, ddir=None, force=False,
111118

112119
def compile_file(fullname, ddir=None, force=False, rx=None, quiet=0,
113120
legacy=False, optimize=-1,
114-
invalidation_mode=None, stripdir=None, prependdir=None,
121+
invalidation_mode=None, *, stripdir=None, prependdir=None,
115122
limit_sl_dest=None):
116123
"""Byte-compile one file.
117124

Lib/test/test_compileall.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,47 @@ def test_compile_dir_maxlevels(self):
212212
compileall.compile_dir(self.directory, quiet=True, maxlevels=depth)
213213
self.assertTrue(os.path.isfile(pyc_filename))
214214

215+
def _test_ddir_only(self, *, ddir, parallel=True):
216+
"""Recursive compile_dir ddir must contain package paths; bpo39769."""
217+
fullpath = ["test", "foo"]
218+
path = self.directory
219+
mods = []
220+
for subdir in fullpath:
221+
path = os.path.join(path, subdir)
222+
os.mkdir(path)
223+
script_helper.make_script(path, "__init__", "")
224+
mods.append(script_helper.make_script(path, "mod",
225+
"def fn(): 1/0\nfn()\n"))
226+
compileall.compile_dir(
227+
self.directory, quiet=True, ddir=ddir,
228+
workers=2 if parallel else 1)
229+
self.assertTrue(mods)
230+
for mod in mods:
231+
self.assertTrue(mod.startswith(self.directory), mod)
232+
modcode = importlib.util.cache_from_source(mod)
233+
modpath = mod[len(self.directory+os.sep):]
234+
_, _, err = script_helper.assert_python_failure(modcode)
235+
expected_in = os.path.join(ddir, modpath)
236+
mod_code_obj = test.test_importlib.util.get_code_from_pyc(modcode)
237+
self.assertEqual(mod_code_obj.co_filename, expected_in)
238+
self.assertIn(f'"{expected_in}"', os.fsdecode(err))
239+
240+
def test_ddir_only_one_worker(self):
241+
"""Recursive compile_dir ddir= contains package paths; bpo39769."""
242+
return self._test_ddir_only(ddir="<a prefix>", parallel=False)
243+
244+
def test_ddir_multiple_workers(self):
245+
"""Recursive compile_dir ddir= contains package paths; bpo39769."""
246+
return self._test_ddir_only(ddir="<a prefix>", parallel=True)
247+
248+
def test_ddir_empty_only_one_worker(self):
249+
"""Recursive compile_dir ddir='' contains package paths; bpo39769."""
250+
return self._test_ddir_only(ddir="", parallel=False)
251+
252+
def test_ddir_empty_multiple_workers(self):
253+
"""Recursive compile_dir ddir='' contains package paths; bpo39769."""
254+
return self._test_ddir_only(ddir="", parallel=True)
255+
215256
def test_strip_only(self):
216257
fullpath = ["test", "build", "real", "path"]
217258
path = os.path.join(self.directory, *fullpath)

Lib/test/test_importlib/util.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from importlib import machinery, util, invalidate_caches
88
from importlib.abc import ResourceReader
99
import io
10+
import marshal
1011
import os
1112
import os.path
1213
from pathlib import Path, PurePath
@@ -118,6 +119,16 @@ def submodule(parent, name, pkg_dir, content=''):
118119
return '{}.{}'.format(parent, name), path
119120

120121

122+
def get_code_from_pyc(pyc_path):
123+
"""Reads a pyc file and returns the unmarshalled code object within.
124+
125+
No header validation is performed.
126+
"""
127+
with open(pyc_path, 'rb') as pyc_f:
128+
pyc_f.seek(16)
129+
return marshal.load(pyc_f)
130+
131+
121132
@contextlib.contextmanager
122133
def uncache(*names):
123134
"""Uncache a module from sys.modules.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The :func:`compileall.compile_dir` function's *ddir* parameter and the
2+
compileall command line flag `-d` no longer write the wrong pathname to the
3+
generated pyc file for submodules beneath the root of the directory tree
4+
being compiled. This fixes a regression introduced with Python 3.5.

0 commit comments

Comments
 (0)