diff options
author | Bernát Gábor <bgabor8@bloomberg.net> | 2020-01-18 17:07:49 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-18 17:07:49 +0000 |
commit | 8d6af57d76edcf425beab6d53d4c14f1e49f7ca5 (patch) | |
tree | de400611862464320c6a45a932e27c2bac0c63ea | |
parent | 8f4128879b26921af2ec0f0f3f690ed2324bfb04 (diff) | |
download | virtualenv-8d6af57d76edcf425beab6d53d4c14f1e49f7ca5.tar.gz |
CentOs and Fedora support (#1500)
* CentOs support
Instead of hard coding patterns and guessing let's ask the host python
via the sysconfig.
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
* add isolated test
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
* fixes for Fedora
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
25 files changed, 207 insertions, 219 deletions
diff --git a/src/virtualenv/activation/python/__init__.py b/src/virtualenv/activation/python/__init__.py index b06af78..8d7e80e 100644 --- a/src/virtualenv/activation/python/__init__.py +++ b/src/virtualenv/activation/python/__init__.py @@ -14,6 +14,6 @@ class PythonActivator(ViaTemplateActivator): def replacements(self, creator, dest_folder): replacements = super(PythonActivator, self).replacements(creator, dest_folder) - site_dump = json.dumps([os.path.relpath(str(i), str(dest_folder)) for i in creator.site_packages], indent=2) + site_dump = json.dumps(list({os.path.relpath(str(i), str(dest_folder)) for i in creator.libs}), indent=2) replacements.update({"__SITE_PACKAGES__": site_dump}) return replacements diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py index f707897..442776a 100644 --- a/src/virtualenv/activation/via_template.py +++ b/src/virtualenv/activation/via_template.py @@ -31,7 +31,7 @@ class ViaTemplateActivator(Activator): "__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt, "__VIRTUAL_ENV__": six.ensure_text(str(creator.dest_dir)), "__VIRTUAL_NAME__": creator.env_name, - "__BIN_NAME__": six.ensure_text(str(creator.bin_name)), + "__BIN_NAME__": six.ensure_text(str(creator.bin_dir.relative_to(creator.dest_dir))), "__PATH_SEP__": os.pathsep, } diff --git a/src/virtualenv/info.py b/src/virtualenv/info.py index 573a97f..fbb7573 100644 --- a/src/virtualenv/info.py +++ b/src/virtualenv/info.py @@ -45,7 +45,7 @@ def is_fs_case_sensitive(): with tempfile.NamedTemporaryFile(prefix="TmP") as tmp_file: _FS_CASE_SENSITIVE = not os.path.exists(tmp_file.name.lower()) logging.debug( - "filesystem under is %r%s case-sensitive", tmp_file.name, "" if _FS_CASE_SENSITIVE else " not" + "filesystem under %r is %scase-sensitive", tmp_file.name, "" if _FS_CASE_SENSITIVE else "not " ) return _FS_CASE_SENSITIVE diff --git a/src/virtualenv/interpreters/create/builtin_way.py b/src/virtualenv/interpreters/create/builtin_way.py index 4134d7b..31ea9e4 100644 --- a/src/virtualenv/interpreters/create/builtin_way.py +++ b/src/virtualenv/interpreters/create/builtin_way.py @@ -13,22 +13,6 @@ class VirtualenvBuiltin(ViaGlobalRefApi): """A creator that does operations itself without delegation""" @property - def bin_name(self): - raise NotImplementedError - - @property - def bin_dir(self): - return self.dest_dir / self.bin_name - - @property - def lib_dir(self): - raise NotImplementedError - - @property - def site_packages(self): - return [self.lib_dir / "site-packages"] - - @property def exe_name(self): raise NotImplementedError diff --git a/src/virtualenv/interpreters/create/cpython/common.py b/src/virtualenv/interpreters/create/cpython/common.py index 490b0ad..ba921b1 100644 --- a/src/virtualenv/interpreters/create/cpython/common.py +++ b/src/virtualenv/interpreters/create/cpython/common.py @@ -24,18 +24,6 @@ class CPython(ViaGlobalRefVirtualenvBuiltin): class CPythonPosix(CPython, PosixSupports): """Create a CPython virtual environment on POSIX platforms""" - @property - def bin_name(self): - return "bin" - - @property - def lib_name(self): - return "lib" - - @property - def lib_base(self): - return Path(self.lib_name) / self.interpreter.python_name - def link_exe(self): host = Path(self.interpreter.system_executable) major, minor = self.interpreter.version_info.major, self.interpreter.version_info.minor @@ -44,18 +32,6 @@ class CPythonPosix(CPython, PosixSupports): @six.add_metaclass(abc.ABCMeta) class CPythonWindows(CPython, WindowsSupports): - @property - def bin_name(self): - return "Scripts" - - @property - def lib_name(self): - return "Lib" - - @property - def lib_base(self): - return Path(self.lib_name) - def link_exe(self): host = Path(self.interpreter.system_executable) return {p: [p.name] for p in (host.parent / n for n in ("python.exe", "pythonw.exe", host.name)) if p.exists()} diff --git a/src/virtualenv/interpreters/create/creator.py b/src/virtualenv/interpreters/create/creator.py index 1185b56..f0adf9e 100644 --- a/src/virtualenv/interpreters/create/creator.py +++ b/src/virtualenv/interpreters/create/creator.py @@ -36,6 +36,55 @@ class Creator(object): self.clear = options.clear self.pyenv_cfg = PyEnvCfg.from_folder(self.dest_dir) + self._stdlib = None + self._system_stdlib = None + self._conf_vars = None + + @property + def bin_dir(self): + return self.script_dir + + @property + def script_dir(self): + return self.dest_dir / Path(self.interpreter.distutils_install["scripts"]) + + @property + def purelib(self): + return self.dest_dir / self.interpreter.distutils_install["purelib"] + + @property + def platlib(self): + return self.dest_dir / self.interpreter.distutils_install["platlib"] + + @property + def libs(self): + return list(OrderedDict(((self.platlib, None), (self.purelib, None))).keys()) + + @property + def stdlib(self): + if self._stdlib is None: + self._stdlib = Path(self.interpreter.sysconfig_path("stdlib", config_var=self._config_vars)) + return self._stdlib + + @property + def system_stdlib(self): + if self._system_stdlib is None: + conf_vars = self._calc_config_vars(self.interpreter.system_prefix) + self._system_stdlib = Path(self.interpreter.sysconfig_path("stdlib", conf_vars)) + return self._system_stdlib + + @property + def _config_vars(self): + if self._conf_vars is None: + self._conf_vars = self._calc_config_vars(six.ensure_text(str(self.dest_dir))) + return self._conf_vars + + def _calc_config_vars(self, to): + return { + k: (to if v.startswith(self.interpreter.prefix) else v) + for k, v in self.interpreter.sysconfig_config_vars.items() + } + def __repr__(self): return six.ensure_str(self.__unicode__()) @@ -154,6 +203,7 @@ class Creator(object): "home": self.interpreter.system_exec_prefix, "include-system-site-packages": "true" if self.enable_system_site_package else "false", "implementation": self.interpreter.implementation, + "version_info": ".".join(str(i) for i in self.interpreter.version_info), "virtualenv": __version__, } diff --git a/src/virtualenv/interpreters/create/debug.py b/src/virtualenv/interpreters/create/debug.py index e2b1585..674d1bb 100644 --- a/src/virtualenv/interpreters/create/debug.py +++ b/src/virtualenv/interpreters/create/debug.py @@ -66,6 +66,14 @@ def run(): except ImportError as exception: # pragma: no cover result["datetime"] = repr(exception) # pragma: no cover + try: + # noinspection PyUnresolvedReferences + import math # site + + result["math"] = repr(math) + except ImportError as exception: # pragma: no cover + result["math"] = repr(exception) # pragma: no cover + # try to print out, this will validate if other core modules are available (json in this case) try: import json diff --git a/src/virtualenv/interpreters/create/pypy/common.py b/src/virtualenv/interpreters/create/pypy/common.py index 1c98064..72dfb21 100644 --- a/src/virtualenv/interpreters/create/pypy/common.py +++ b/src/virtualenv/interpreters/create/pypy/common.py @@ -14,10 +14,6 @@ class PyPy(ViaGlobalRefVirtualenvBuiltin): def supports(cls, interpreter): return interpreter.implementation == "PyPy" and super(PyPy, cls).supports(interpreter) - @property - def site_packages(self): - return [self.dest_dir / "site-packages"] - def link_exe(self): host = Path(self.interpreter.system_executable) return {host: sorted("{}{}".format(name, self.suffix) for name in self.exe_names())} diff --git a/src/virtualenv/interpreters/create/pypy/pypy2.py b/src/virtualenv/interpreters/create/pypy/pypy2.py index fe34cdc..b16b53d 100644 --- a/src/virtualenv/interpreters/create/pypy/pypy2.py +++ b/src/virtualenv/interpreters/create/pypy/pypy2.py @@ -6,7 +6,6 @@ import six from virtualenv.interpreters.create.support import PosixSupports, WindowsSupports from virtualenv.interpreters.create.via_global_ref.python2 import Python2 -from virtualenv.util.path import Path from .common import PyPy @@ -20,21 +19,17 @@ class PyPy2(PyPy, Python2): return "pypy" @property - def lib_name(self): - return "lib-python" - - @property def lib_pypy(self): return self.dest_dir / "lib_pypy" - @property - def lib_base(self): - return Path(self.lib_name) / self.interpreter.version_release_str + def _calc_config_vars(self, to): + base = super(PyPy, self)._calc_config_vars(to) + # for some reason pypy seems to provide the wrong information for implementation_lower, fix it + base["implementation_lower"] = "python" + return base def ensure_directories(self): - dirs = super(PyPy, self).ensure_directories() - dirs.add(self.lib_pypy) - return dirs + return super(PyPy, self).ensure_directories() | {self.lib_pypy} def modules(self): return [ @@ -51,10 +46,6 @@ class PyPy2(PyPy, Python2): class PyPy2Posix(PyPy2, PosixSupports): """PyPy 2 on POSIX""" - @property - def bin_name(self): - return "bin" - def modules(self): return super(PyPy2Posix, self).modules() + ["posixpath"] @@ -66,10 +57,6 @@ class PyPy2Posix(PyPy2, PosixSupports): class Pypy2Windows(PyPy2, WindowsSupports): """PyPy 2 on Windows""" - @property - def bin_name(self): - return "Scripts" - def modules(self): return super(Pypy2Windows, self).modules() + ["ntpath"] diff --git a/src/virtualenv/interpreters/create/pypy/pypy3.py b/src/virtualenv/interpreters/create/pypy/pypy3.py index 6b759cb..724f0fb 100644 --- a/src/virtualenv/interpreters/create/pypy/pypy3.py +++ b/src/virtualenv/interpreters/create/pypy/pypy3.py @@ -5,67 +5,48 @@ import abc import six from virtualenv.interpreters.create.support import PosixSupports, Python3Supports, WindowsSupports -from virtualenv.util.path import Path from .common import PyPy @six.add_metaclass(abc.ABCMeta) class PyPy3(PyPy, Python3Supports): - """""" - @property def exe_base(self): return "pypy3" + @property + def stdlib(self): + """ + PyPy3 seems to respect sysconfig only for the host python... + virtual environments purelib is instead lib/pythonx.y + """ + return self.dest_dir / "lib" / "python{}".format(self.interpreter.version_release_str) / "site-packages" + def exe_names(self): base = super(PyPy3, self).exe_names() base.add("pypy") return base - def ensure_directories(self): - dirs = super(PyPy, self).ensure_directories() - dirs.add(self.lib_dir / "site-packages") - return dirs - class PyPy3Posix(PyPy3, PosixSupports): """PyPy 2 on POSIX""" @property - def bin_name(self): - return "bin" - - @property - def lib_name(self): - return "lib" - - @property - def lib_base(self): - return Path(self.lib_name) / self.interpreter.python_name - - @property def _shared_libs(self): return ["libpypy3-c.so", "libpypy3-c.dylib"] def _shared_lib_to(self): - return super(PyPy3, self)._shared_lib_to() + [self.dest_dir / self.lib_name] + return super(PyPy3, self)._shared_lib_to() + [self.stdlib.parent.parent] class Pypy3Windows(PyPy3, WindowsSupports): """PyPy 2 on Windows""" @property - def bin_name(self): - return "Scripts" - - @property - def lib_name(self): - return "Lib" - - @property - def lib_base(self): - return Path(self.lib_name) + def bin_dir(self): + """PyPy3 needs to fallback to pypy definition""" + return self.dest_dir / "Scripts" @property def _shared_libs(self): diff --git a/src/virtualenv/interpreters/create/venv.py b/src/virtualenv/interpreters/create/venv.py index afb9471..78d4622 100644 --- a/src/virtualenv/interpreters/create/venv.py +++ b/src/virtualenv/interpreters/create/venv.py @@ -13,10 +13,10 @@ from .via_global_ref.api import ViaGlobalRefApi class Venv(ViaGlobalRefApi): def __init__(self, options, interpreter): + self.builtin_way = options.builtin_way super(Venv, self).__init__(options, interpreter) self.can_be_inline = interpreter is CURRENT and interpreter.executable == interpreter.system_executable self._context = None - self.builtin_way = options.builtin_way def _args(self): return super(Venv, self)._args() + ( @@ -33,9 +33,8 @@ class Venv(ViaGlobalRefApi): else: self.create_via_sub_process() # TODO: cleanup activation scripts - if self.builtin_way is not None: - for site_package in self.builtin_way.site_packages: - ensure_dir(site_package) + for lib in self.libs: + ensure_dir(lib) def create_inline(self): from venv import EnvBuilder @@ -66,27 +65,10 @@ class Venv(ViaGlobalRefApi): super(Venv, self).set_pyenv_cfg() self.pyenv_cfg.update(venv_content) - def _proxy_builtin_way(self, key): - if self.builtin_way is None: - return None - return getattr(self.builtin_way, key) - - @property - def exe(self): - return self._proxy_builtin_way("exe") - - @property - def site_packages(self): - return self._proxy_builtin_way("site_packages") - - @property - def bin_dir(self): - return self._proxy_builtin_way("bin_dir") - - @property - def bin_name(self): - return self._proxy_builtin_way("bin_name") - - @property - def lib_dir(self): - return self._proxy_builtin_way("lib_dir") + def __getattribute__(self, item): + builtin = object.__getattribute__(self, "builtin_way") + if builtin is not None and hasattr(builtin, item): + element = getattr(builtin, item) + if not callable(element): + return element + return object.__getattribute__(self, item) diff --git a/src/virtualenv/interpreters/create/via_global_ref/python2.py b/src/virtualenv/interpreters/create/via_global_ref/python2.py index 4ff6e84..9c7222a 100644 --- a/src/virtualenv/interpreters/create/via_global_ref/python2.py +++ b/src/virtualenv/interpreters/create/via_global_ref/python2.py @@ -27,16 +27,16 @@ class Python2(ViaGlobalRefVirtualenvBuiltin, Python2Supports): self.add_module(module) # 2. install a patched site-package, the default Python 2 site.py is not smart enough to understand pyvenv.cfg, # so we inject a small shim that can do this - site_py = self.lib_dir / "site.py" - relative_site_packages = [ - os.path.relpath(six.ensure_text(str(s)), six.ensure_text(str(site_py))) for s in self.site_packages - ] + site_py = self.stdlib / "site.py" custom_site = get_custom_site() if IS_ZIPAPP: custom_site_text = read_from_zipapp(custom_site) else: custom_site_text = custom_site.read_text() - site_py.write_text(custom_site_text.replace("___EXPECTED_SITE_PACKAGES___", json.dumps(relative_site_packages))) + expected = json.dumps( + [os.path.relpath(six.ensure_text(str(i)), six.ensure_text(str(site_py))) for i in self.libs] + ) + site_py.write_text(custom_site_text.replace("___EXPECTED_SITE_PACKAGES___", expected)) @abc.abstractmethod def modules(self): @@ -48,10 +48,10 @@ class Python2(ViaGlobalRefVirtualenvBuiltin, Python2Supports): def add_module(self, req): for ext in ["py", "pyc"]: file_path = "{}.{}".format(req, ext) - self.copier(self.system_stdlib / file_path, self.lib_dir / file_path) + self.copier(self.system_stdlib / file_path, self.stdlib / file_path) def add_folder(self, folder): - self.copier(self.system_stdlib / folder, self.lib_dir / folder) + self.copier(self.system_stdlib / folder, self.stdlib / folder) def get_custom_site(): diff --git a/src/virtualenv/interpreters/create/via_global_ref/via_global_self_do.py b/src/virtualenv/interpreters/create/via_global_ref/via_global_self_do.py index 7e259f3..5844a92 100644 --- a/src/virtualenv/interpreters/create/via_global_ref/via_global_self_do.py +++ b/src/virtualenv/interpreters/create/via_global_ref/via_global_self_do.py @@ -9,7 +9,7 @@ from six import add_metaclass from virtualenv.info import is_fs_case_sensitive from virtualenv.interpreters.create.builtin_way import VirtualenvBuiltin -from virtualenv.util.path import Path, copy, ensure_dir, make_exe, symlink +from virtualenv.util.path import copy, ensure_dir, make_exe, symlink @add_metaclass(ABCMeta) @@ -42,9 +42,7 @@ class ViaGlobalRefVirtualenvBuiltin(VirtualenvBuiltin): self.pyenv_cfg["base-executable"] = self.interpreter.system_executable def ensure_directories(self): - dirs = {self.dest_dir, self.bin_dir, self.lib_dir} - dirs.update(self.site_packages) - return dirs + return {self.dest_dir, self.bin_dir, self.script_dir, self.stdlib} | set(self.libs) def setup_python(self): aliases = method = self.add_exe_method() @@ -79,19 +77,3 @@ class ViaGlobalRefVirtualenvBuiltin(VirtualenvBuiltin): def symlink_exe(src, dest): symlink(src, dest) make_exe(dest) - - @property - def lib_base(self): - raise NotImplementedError - - @property - def system_stdlib(self): - return Path(self.interpreter.system_prefix) / self.lib_base - - @property - def lib_dir(self): - return self.dest_dir / self.lib_base - - @abc.abstractmethod - def lib_name(self): - raise NotImplementedError diff --git a/src/virtualenv/interpreters/discovery/py_info.py b/src/virtualenv/interpreters/discovery/py_info.py index 9feecb4..0aebece 100644 --- a/src/virtualenv/interpreters/discovery/py_info.py +++ b/src/virtualenv/interpreters/discovery/py_info.py @@ -10,8 +10,12 @@ import logging import os import pipes import platform +import re import sys +import sysconfig from collections import OrderedDict, namedtuple +from distutils.command.install import SCHEME_KEYS +from distutils.dist import Distribution VersionInfo = namedtuple("VersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]) @@ -21,36 +25,41 @@ def _get_path_extensions(): EXTENSIONS = _get_path_extensions() +_CONF_VAR_RE = re.compile(r"\{\w+\}") class PythonInfo(object): """Contains information for a Python interpreter""" def __init__(self): + def u(v): + return v.decode("utf-8") if isinstance(v, bytes) else v + # qualifies the python - self.platform = sys.platform - self.implementation = platform.python_implementation() - self.pypy_version_info = tuple(sys.pypy_version_info) if self.implementation == "PyPy" else None + self.platform = u(sys.platform) + self.implementation = u(platform.python_implementation()) + if self.implementation == "PyPy": + self.pypy_version_info = tuple(u(i) for i in sys.pypy_version_info) # this is a tuple in earlier, struct later, unify to our own named tuple - self.version_info = VersionInfo(*list(sys.version_info)) + self.version_info = VersionInfo(*list(u(i) for i in sys.version_info)) self.architecture = 64 if sys.maxsize > 2 ** 32 else 32 - self.executable = sys.executable # executable we were called with - self.original_executable = self.executable - self.base_executable = getattr(sys, "_base_executable", None) # some platforms may set this + self.executable = u(sys.executable) # executable we were called with + self.original_executable = u(self.executable) + self.base_executable = u(getattr(sys, "_base_executable", None)) # some platforms may set this - self.version = sys.version - self.os = os.name + self.version = u(sys.version) + self.os = u(os.name) # information about the prefix - determines python home - self.prefix = getattr(sys, "prefix", None) # prefix we think - self.base_prefix = getattr(sys, "base_prefix", None) # venv - self.real_prefix = getattr(sys, "real_prefix", None) # old virtualenv + self.prefix = u(getattr(sys, "prefix", None)) # prefix we think + self.base_prefix = u(getattr(sys, "base_prefix", None)) # venv + self.real_prefix = u(getattr(sys, "real_prefix", None)) # old virtualenv # information about the exec prefix - dynamic stdlib modules - self.base_exec_prefix = getattr(sys, "base_exec_prefix", None) - self.exec_prefix = getattr(sys, "exec_prefix", None) + self.base_exec_prefix = u(getattr(sys, "base_exec_prefix", None)) + self.exec_prefix = u(getattr(sys, "exec_prefix", None)) try: __import__("venv") @@ -58,9 +67,28 @@ class PythonInfo(object): except ImportError: has = False self.has_venv = has - self.path = sys.path - self.file_system_encoding = sys.getfilesystemencoding() - self.stdout_encoding = getattr(sys.stdout, "encoding", None) + self.path = [u(i) for i in sys.path] + self.file_system_encoding = u(sys.getfilesystemencoding()) + self.stdout_encoding = u(getattr(sys.stdout, "encoding", None)) + + self.sysconfig_paths = {u(i): u(sysconfig.get_path(i, expand=False)) for i in sysconfig.get_path_names()} + config_var_keys = set() + for element in self.sysconfig_paths.values(): + for k in _CONF_VAR_RE.findall(element): + config_var_keys.add(u(k[1:-1])) + self.sysconfig_config_vars = {u(i): u(sysconfig.get_config_var(i)) for i in config_var_keys} + + self.distutils_install = {u(k): u(v) for k, v in self._distutils_install().items()} + + def _distutils_install(self): + # follow https://github.com/pypa/pip/blob/master/src/pip/_internal/locations.py#L95 + d = Distribution({"script_args": "--no-user-cfg"}) + d.parse_config_files() + i = d.get_command_obj("install", create=True) + i.prefix = "a" + i.finalize_options() + result = {key: (getattr(i, "install_{}".format(key))[1:]).lstrip(os.sep) for key in SCHEME_KEYS} + return result @property def version_str(self): @@ -132,8 +160,7 @@ class PythonInfo(object): data = json.loads(payload) data["version_info"] = VersionInfo(**data["version_info"]) # restore this to a named tuple structure result = cls() - for var in vars(result): - setattr(result, var, data[var]) + result.__dict__ = {k: v for k, v in data.items()} return result @property @@ -249,7 +276,7 @@ class PythonInfo(object): cmd = cls._get_exe_cmd(exe) # noinspection DuplicatedCode # this is duplicated here because this file is executed on its own, so cannot be refactored otherwise - logging.debug("get interpreter info via cmd: %s", Cmd(cmd)) + logging.debug(u"get interpreter info via cmd: %s", Cmd(cmd)) try: process = Popen( cmd, universal_newlines=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE @@ -307,6 +334,16 @@ class PythonInfo(object): return False return True + def sysconfig_path(self, key, config_var=None, sep=os.sep): + pattern = self.sysconfig_paths[key] + if config_var is None: + config_var = self.sysconfig_config_vars + else: + base = {k: v for k, v in self.sysconfig_config_vars.items()} + base.update(config_var) + config_var = base + return pattern.format(**config_var).replace(u"/", sep) + class Cmd(object): def __init__(self, cmd, env=None): @@ -314,9 +351,12 @@ class Cmd(object): self.env = env def __repr__(self): - cmd_repr = " ".join(pipes.quote(c) for c in self.cmd) + def e(v): + return v.decode("utf-8") if isinstance(v, bytes) else v + + cmd_repr = e(" ").join(pipes.quote(e(c)) for c in self.cmd) if self.env is not None: - cmd_repr += " env of {!r}".format(self.env) + cmd_repr += e(" env of {!r}").format(self.env) return cmd_repr diff --git a/src/virtualenv/pyenv_cfg.py b/src/virtualenv/pyenv_cfg.py index 05daa5a..218c759 100644 --- a/src/virtualenv/pyenv_cfg.py +++ b/src/virtualenv/pyenv_cfg.py @@ -54,3 +54,6 @@ class PyEnvCfg(object): def update(self, other): self.content.update(other) return self + + def __repr__(self): + return "{}(path={})".format(self.__class__.__name__, self.path) diff --git a/src/virtualenv/seed/via_app_data/pip_install/base.py b/src/virtualenv/seed/via_app_data/pip_install/base.py index ba65e9a..7082cf6 100644 --- a/src/virtualenv/seed/via_app_data/pip_install/base.py +++ b/src/virtualenv/seed/via_app_data/pip_install/base.py @@ -34,9 +34,8 @@ class PipInstall(object): def install(self): self._extracted = True # sync image - site_package = self._creator.site_packages[0] for filename in self._image_dir.iterdir(): - into = site_package / filename.name + into = self._creator.purelib / filename.name logging.debug("%s %s from %s", self.__class__.__name__, into, filename) if into.exists(): if into.is_dir() and not into.is_symlink(): @@ -46,9 +45,9 @@ class PipInstall(object): self._sync(filename, into) # generate console executables consoles = set() - bin_dir = self._creator.bin_dir + script_dir = self._creator.script_dir for name, module in self._console_scripts.items(): - consoles.update(self._create_console_entry_point(name, module, bin_dir)) + consoles.update(self._create_console_entry_point(name, module, script_dir)) logging.debug("generated console scripts %s", " ".join(i.name for i in consoles)) def build_image(self): @@ -83,7 +82,7 @@ class PipInstall(object): try: to_folder = Path(folder) rel = os.path.relpath( - six.ensure_text(str(self._creator.bin_dir)), six.ensure_text(str(self._creator.site_packages[0])) + six.ensure_text(str(self._creator.script_dir)), six.ensure_text(str(self._creator.purelib)) ) for name, module in self._console_scripts.items(): new_files.update( diff --git a/src/virtualenv/util/path/_permission.py b/src/virtualenv/util/path/_permission.py index 1356b6d..85f93b8 100644 --- a/src/virtualenv/util/path/_permission.py +++ b/src/virtualenv/util/path/_permission.py @@ -13,7 +13,7 @@ def make_exe(filename): mode |= level filename.chmod(mode) break - except PermissionError: + except OSError: continue diff --git a/src/virtualenv/util/path/_sync.py b/src/virtualenv/util/path/_sync.py index 860aee1..4c5aa86 100644 --- a/src/virtualenv/util/path/_sync.py +++ b/src/virtualenv/util/path/_sync.py @@ -23,6 +23,10 @@ def symlink_or_copy(do_copy, src, dst, relative_symlinks_ok=False): """ Try symlinking a target, and if that fails, fall back to copying. """ + if not src.exists(): + raise RuntimeError("source {} does not exists".format(src)) + if src == dst: + raise RuntimeError("source {} is same as destination ".format(src)) def norm(val): if IS_PYPY and six.PY3: diff --git a/src/virtualenv/util/subprocess/_win_subprocess.py b/src/virtualenv/util/subprocess/_win_subprocess.py index e8fdaf0..f30344d 100644 --- a/src/virtualenv/util/subprocess/_win_subprocess.py +++ b/src/virtualenv/util/subprocess/_win_subprocess.py @@ -5,7 +5,6 @@ import ctypes import os import subprocess -import sys from ctypes import Structure, WinError, byref, c_char_p, c_void_p, c_wchar, c_wchar_p, sizeof, windll from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPVOID, LPWSTR, WORD diff --git a/tests/conftest.py b/tests/conftest.py index 606c9c8..678eb55 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -218,7 +218,7 @@ class EnableCoverage(object): def __enter__(self, creator): assert not self.cov_pth.exists() - site_packages = creator.site_packages[0] + site_packages = creator.purelib p_th = site_packages / self._SUBPROCESS_TRIGGER_PTH_NAME if not str(p_th).startswith(str(self._COV_SITE_PACKAGES)): diff --git a/tests/unit/activation/conftest.py b/tests/unit/activation/conftest.py index 79ff5d9..06980a5 100644 --- a/tests/unit/activation/conftest.py +++ b/tests/unit/activation/conftest.py @@ -206,7 +206,7 @@ def raise_on_non_source_class(): def activation_python(tmp_path_factory, special_char_name): dest = os.path.join(six.ensure_text(str(tmp_path_factory.mktemp("activation-tester-env"))), special_char_name) session = run_via_cli(["--seed", "none", dest, "--prompt", special_char_name, "--creator", "builtin"]) - pydoc_test = session.creator.site_packages[0] / "pydoc_test.py" + pydoc_test = session.creator.purelib / "pydoc_test.py" with open(six.ensure_text(str(pydoc_test)), "wb") as file_handler: file_handler.write(b'"""This is pydoc_test.py"""') yield session diff --git a/tests/unit/activation/test_python_activator.py b/tests/unit/activation/test_python_activator.py index 3db0b93..f696bf8 100644 --- a/tests/unit/activation/test_python_activator.py +++ b/tests/unit/activation/test_python_activator.py @@ -1,8 +1,8 @@ from __future__ import absolute_import, unicode_literals -import inspect import os import sys +from textwrap import dedent import pytest import six @@ -36,17 +36,9 @@ def test_python(raise_on_non_source_class, activation_tester): env[str("PATH")] = os.pathsep.join([str(tmp_path), str(tmp_path / "other")]) return env - def _get_test_lines(self, activate_script): - raw = inspect.getsource(self.activate_this_test) - return [ - i[12:] - for i in raw.replace('"__FILENAME__"', repr(six.ensure_text(str(activate_script)))).splitlines()[2:] - ] - - # noinspection PyUnresolvedReferences @staticmethod - def activate_this_test(): - """Used as template for the test - unicode literals don't apply""" + def _get_test_lines(activate_script): + raw = """ import os import sys @@ -60,10 +52,10 @@ def test_python(raise_on_non_source_class, activation_tester): print_path(os.environ.get("VIRTUAL_ENV")) print_path(os.environ.get("PATH")) print_path(os.pathsep.join(sys.path)) - file_at = "__FILENAME__" + file_at = {} with open(file_at, "rb") as file_handler: content = file_handler.read() - exec(content, {"__file__": file_at}) + exec(content, {{"__file__": file_at}}) print_path(os.environ.get("VIRTUAL_ENV")) print_path(os.environ.get("PATH")) print_path(os.pathsep.join(sys.path)) @@ -71,6 +63,11 @@ def test_python(raise_on_non_source_class, activation_tester): import pydoc_test print_path(inspect.getsourcefile(pydoc_test)) + """.format( + repr(six.ensure_text(str(activate_script))) + ) + result = dedent(raw).splitlines() + return result def assert_output(self, out, raw, tmp_path): assert out[0] == "None" # start with VIRTUAL_ENV None @@ -87,10 +84,13 @@ def test_python(raise_on_non_source_class, activation_tester): # sys path contains the site package at its start new_sys_path = out[5].split(os.path.pathsep) - assert ([six.ensure_text(str(i)) for i in self._creator.site_packages] + prev_sys_path) == new_sys_path + + new_lib_paths = {six.ensure_text(str(i)) for i in self._creator.libs} + assert prev_sys_path == new_sys_path[len(new_lib_paths) :] + assert new_lib_paths == set(new_sys_path[: len(new_lib_paths)]) # manage to import from activate site package - assert self.norm_path(out[6]) == self.norm_path(self._creator.site_packages[0] / "pydoc_test.py") + assert self.norm_path(out[6]) == self.norm_path(self._creator.purelib / "pydoc_test.py") def non_source_activate(self, activate_script): return self._invoke_script + [ diff --git a/tests/unit/interpreters/boostrap/test_boostrap_link_via_app_data.py b/tests/unit/interpreters/boostrap/test_boostrap_link_via_app_data.py index 618d281..2ae45c0 100644 --- a/tests/unit/interpreters/boostrap/test_boostrap_link_via_app_data.py +++ b/tests/unit/interpreters/boostrap/test_boostrap_link_via_app_data.py @@ -38,17 +38,15 @@ def test_base_bootstrap_link_via_app_data(tmp_path, coverage_env, mocker): assert result # uninstalling pip/setuptools now should leave us with a clean env - site_package = result.creator.site_packages[0] + site_package = result.creator.purelib pip = site_package / "pip" setuptools = site_package / "setuptools" files_post_first_create = list(site_package.iterdir()) assert pip in files_post_first_create assert setuptools in files_post_first_create - - env_exe = result.creator.exe for pip_exe in [ - env_exe.with_name("pip{}{}".format(suffix, env_exe.suffix)) + result.creator.script_dir / "pip{}{}".format(suffix, result.creator.exe.suffix) for suffix in ( "", "{}".format(CURRENT.version_info.major), @@ -61,7 +59,7 @@ def test_base_bootstrap_link_via_app_data(tmp_path, coverage_env, mocker): assert not process.returncode remove_cmd = [ - str(env_exe), + str(result.creator.exe), "-m", "pip", "--verbose", diff --git a/tests/unit/interpreters/boostrap/test_pip_invoke.py b/tests/unit/interpreters/boostrap/test_pip_invoke.py index 8904c13..645f1fc 100644 --- a/tests/unit/interpreters/boostrap/test_pip_invoke.py +++ b/tests/unit/interpreters/boostrap/test_pip_invoke.py @@ -27,7 +27,7 @@ def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env): assert result # uninstalling pip/setuptools now should leave us with a clean env - site_package = result.creator.site_packages[0] + site_package = result.creator.purelib pip = site_package / "pip" setuptools = site_package / "setuptools" wheel = site_package / "wheel" diff --git a/tests/unit/interpreters/create/test_creator.py b/tests/unit/interpreters/create/test_creator.py index a3d0dec..02bfdb2 100644 --- a/tests/unit/interpreters/create/test_creator.py +++ b/tests/unit/interpreters/create/test_creator.py @@ -10,7 +10,7 @@ import pytest import six from virtualenv.__main__ import run -from virtualenv.info import IS_PYPY +from virtualenv.info import IS_PYPY, IS_WIN from virtualenv.interpreters.create.creator import DEBUG_SCRIPT, get_env_debug_info from virtualenv.interpreters.discovery.builtin import get_interpreter from virtualenv.interpreters.discovery.py_info import CURRENT, PythonInfo @@ -77,11 +77,10 @@ def system(): return get_env_debug_info(Path(CURRENT.system_executable), DEBUG_SCRIPT) -@pytest.mark.parametrize("global_access", [False, True], ids=["no_global", "ok_global"]) -@pytest.mark.parametrize( - "use_venv", [False, True] if six.PY3 else [False], ids=["no_venv", "venv"] if six.PY3 else ["no_venv"] -) -def test_create_no_seed(python, use_venv, global_access, system, coverage_env, special_name_dir): +@pytest.mark.parametrize("isolated", [True, False], ids=["isolated", "with_global_site"]) +@pytest.mark.parametrize("method", (["copies"] + ([] if IS_WIN else ["symlinks"]))) +@pytest.mark.parametrize("creator", (["builtin"] + (["venv"] if six.PY3 else []))) +def test_create_no_seed(python, creator, isolated, system, coverage_env, special_name_dir, method): dest = special_name_dir cmd = [ "-v", @@ -93,9 +92,10 @@ def test_create_no_seed(python, use_venv, global_access, system, coverage_env, s "--activators", "", "--creator", - "venv" if use_venv else "builtin", + creator, + "--{}".format(method), ] - if global_access: + if not isolated: cmd.append("--system-site-packages") result = run_via_cli(cmd) coverage_env() @@ -103,9 +103,8 @@ def test_create_no_seed(python, use_venv, global_access, system, coverage_env, s # pypy cleans up file descriptors periodically so our (many) subprocess calls impact file descriptor limits # force a cleanup of these on system where the limit is low-ish (e.g. MacOS 256) gc.collect() - for site_package in result.creator.site_packages: - content = list(site_package.iterdir()) - assert not content, "\n".join(str(i) for i in content) + content = list(result.creator.purelib.iterdir()) + assert not content, "\n".join(six.ensure_text(str(i)) for i in content) assert result.creator.env_name == six.ensure_text(dest.name) debug = result.creator.debug sys_path = cleanup_sys_path(debug["sys"]["path"]) @@ -128,7 +127,9 @@ def test_create_no_seed(python, use_venv, global_access, system, coverage_env, s # ensure the global site package is added or not, depending on flag last_from_system_path = next(i for i in reversed(system_sys_path) if str(i).startswith(system["sys"]["prefix"])) - if global_access: + if isolated: + assert last_from_system_path not in sys_path + else: common = [] for left, right in zip(reversed(system_sys_path), reversed(sys_path)): if left == right: @@ -140,8 +141,6 @@ def test_create_no_seed(python, use_venv, global_access, system, coverage_env, s return [six.ensure_text(str(i)) for i in iterable] assert common, "\n".join(difflib.unified_diff(list_to_str(sys_path), list_to_str(system_sys_path))) - else: - assert last_from_system_path not in sys_path @pytest.mark.skipif(not CURRENT.has_venv, reason="requires interpreter with venv") @@ -174,7 +173,7 @@ def test_debug_bad_virtualenv(tmp_path): cmd = [str(tmp_path), "--without-pip"] result = run_via_cli(cmd) # if the site.py is removed/altered the debug should fail as no one is around to fix the paths - site_py = result.creator.lib_dir / "site.py" + site_py = result.creator.stdlib / "site.py" site_py.unlink() # insert something that writes something on the stdout site_py.write_text('import sys; sys.stdout.write(repr("std-out")); sys.stderr.write("std-err"); raise ValueError') |