diff options
author | Carl Meyer <carl@oddbird.net> | 2018-06-16 04:40:56 (GMT) |
---|---|---|
committer | Nick Coghlan <ncoghlan@gmail.com> | 2018-06-16 04:40:56 (GMT) |
commit | b193fa996a746111252156f11fb14c12fd6267e6 (patch) | |
tree | 1995957ce580ba762a19f64e41db2db32aec096a /Lib/importlib | |
parent | 68680035143a3a6398faa88f067f244c74691d19 (diff) | |
download | cpython-b193fa996a746111252156f11fb14c12fd6267e6.zip cpython-b193fa996a746111252156f11fb14c12fd6267e6.tar.gz cpython-b193fa996a746111252156f11fb14c12fd6267e6.tar.bz2 |
bpo-33499: Add PYTHONPYCACHEPREFIX env var for alt bytecode cache location. (GH-6834)
In some development setups it is inconvenient or impossible to write bytecode
caches to the code tree, but the bytecode caches are still useful. The
PYTHONPYCACHEPREFIX environment variable allows specifying an alternate
location for cached bytecode files, within which a directory tree mirroring the code
tree will be created. This cache tree is then used (for both reading and writing)
instead of the local `__pycache__` subdirectory within each source directory.
Exposed at runtime as sys.pycache_prefix (defaulting to None), and can
be set from the CLI as "-X pycache_prefix=path".
Patch by Carl Meyer.
Diffstat (limited to 'Lib/importlib')
-rw-r--r-- | Lib/importlib/_bootstrap_external.py | 62 |
1 files changed, 52 insertions, 10 deletions
diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index bdd7859..7f8bc21 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -102,6 +102,15 @@ def _path_isdir(path): return _path_is_mode_type(path, 0o040000) +def _path_isabs(path): + """Replacement for os.path.isabs. + + Considers a Windows drive-relative path (no drive, but starts with slash) to + still be "absolute". + """ + return path.startswith(path_separators) or path[1:3] in _pathseps_with_colon + + def _write_atomic(path, data, mode=0o666): """Best-effort function to write data to a path atomically. Be prepared to handle a FileExistsError if concurrent writing of the @@ -312,7 +321,33 @@ def cache_from_source(path, debug_override=None, *, optimization=None): if not optimization.isalnum(): raise ValueError('{!r} is not alphanumeric'.format(optimization)) almost_filename = '{}.{}{}'.format(almost_filename, _OPT, optimization) - return _path_join(head, _PYCACHE, almost_filename + BYTECODE_SUFFIXES[0]) + filename = almost_filename + BYTECODE_SUFFIXES[0] + if sys.pycache_prefix is not None: + # We need an absolute path to the py file to avoid the possibility of + # collisions within sys.pycache_prefix, if someone has two different + # `foo/bar.py` on their system and they import both of them using the + # same sys.pycache_prefix. Let's say sys.pycache_prefix is + # `C:\Bytecode`; the idea here is that if we get `Foo\Bar`, we first + # make it absolute (`C:\Somewhere\Foo\Bar`), then make it root-relative + # (`Somewhere\Foo\Bar`), so we end up placing the bytecode file in an + # unambiguous `C:\Bytecode\Somewhere\Foo\Bar\`. + if not _path_isabs(head): + head = _path_join(_os.getcwd(), head) + + # Strip initial drive from a Windows path. We know we have an absolute + # path here, so the second part of the check rules out a POSIX path that + # happens to contain a colon at the second character. + if head[1] == ':' and head[0] not in path_separators: + head = head[2:] + + # Strip initial path separator from `head` to complete the conversion + # back to a root-relative path before joining. + return _path_join( + sys.pycache_prefix, + head.lstrip(path_separators), + filename, + ) + return _path_join(head, _PYCACHE, filename) def source_from_cache(path): @@ -328,23 +363,29 @@ def source_from_cache(path): raise NotImplementedError('sys.implementation.cache_tag is None') path = _os.fspath(path) head, pycache_filename = _path_split(path) - head, pycache = _path_split(head) - if pycache != _PYCACHE: - raise ValueError('{} not bottom-level directory in ' - '{!r}'.format(_PYCACHE, path)) + found_in_pycache_prefix = False + if sys.pycache_prefix is not None: + stripped_path = sys.pycache_prefix.rstrip(path_separators) + if head.startswith(stripped_path + path_sep): + head = head[len(stripped_path):] + found_in_pycache_prefix = True + if not found_in_pycache_prefix: + head, pycache = _path_split(head) + if pycache != _PYCACHE: + raise ValueError(f'{_PYCACHE} not bottom-level directory in ' + f'{path!r}') dot_count = pycache_filename.count('.') if dot_count not in {2, 3}: - raise ValueError('expected only 2 or 3 dots in ' - '{!r}'.format(pycache_filename)) + raise ValueError(f'expected only 2 or 3 dots in {pycache_filename!r}') elif dot_count == 3: optimization = pycache_filename.rsplit('.', 2)[-2] if not optimization.startswith(_OPT): raise ValueError("optimization portion of filename does not start " - "with {!r}".format(_OPT)) + f"with {_OPT!r}") opt_level = optimization[len(_OPT):] if not opt_level.isalnum(): - raise ValueError("optimization level {!r} is not an alphanumeric " - "value".format(optimization)) + raise ValueError(f"optimization level {optimization!r} is not an " + "alphanumeric value") base_filename = pycache_filename.partition('.')[0] return _path_join(head, base_filename + SOURCE_SUFFIXES[0]) @@ -1533,6 +1574,7 @@ def _setup(_bootstrap_module): setattr(self_module, '_os', os_module) setattr(self_module, 'path_sep', path_sep) setattr(self_module, 'path_separators', ''.join(path_separators)) + setattr(self_module, '_pathseps_with_colon', {f':{s}' for s in path_separators}) # Directly load the _thread module (needed during bootstrap). thread_module = _bootstrap._builtin_from_name('_thread') |