diff --git a/docs/changelog/1663.bugfix.rst b/docs/changelog/1663.bugfix.rst new file mode 100644 index 000000000..9a6f779bd --- /dev/null +++ b/docs/changelog/1663.bugfix.rst @@ -0,0 +1 @@ +Support Python 3 Framework distributed via XCode in macOs Catalina and before - by :user:`gaborbernat`. diff --git a/docs/changelog/1709.bugfix.rst b/docs/changelog/1709.bugfix.rst new file mode 100644 index 000000000..588641993 --- /dev/null +++ b/docs/changelog/1709.bugfix.rst @@ -0,0 +1,2 @@ +Fix Windows Store Python support, do not allow creation via symlink as that's not going to work by design +- by :user:`gaborbernat`. diff --git a/docs/changelog/1716.feature.rst b/docs/changelog/1716.feature.rst new file mode 100644 index 000000000..674b65daa --- /dev/null +++ b/docs/changelog/1716.feature.rst @@ -0,0 +1,5 @@ +Improve error message when the host python does not satisfy invariants needed to create virtual environments (now we +print which host files are incompatible/missing and for which creators when no supported creator can be matched, however +we found creators that can describe the given Python interpreter - will still print no supported creator for Jython, +but print exactly what host files do not allow creation of virtual environments in case of CPython/PyPy) +- by :user:`gaborbernat`. diff --git a/docs/installation.rst b/docs/installation.rst index dffd3f5fa..bf319cdcb 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -116,8 +116,7 @@ In case of macOs we support: Windows ~~~~~~~ - Installations from `python.org `_ -- Windows Store Python - note only `version 3.8+ `_ (``3.7`` - was marked experimental and contains many bugs that would make it very hard for us to support it) +- Windows Store Python - note only `version 3.7+ `_ Packaging variants ~~~~~~~~~~~~~~~~~~ diff --git a/docs/render_cli.py b/docs/render_cli.py index 88549ec3b..568235d95 100644 --- a/docs/render_cli.py +++ b/docs/render_cli.py @@ -76,12 +76,16 @@ def a(*args, **kwargs): return True elif key == "creator": if name == "venv": - from virtualenv.create.via_global_ref.venv import Meta + from virtualenv.create.via_global_ref.venv import ViaGlobalRefMeta - return Meta(True, True) - from virtualenv.create.via_global_ref.builtin.via_global_self_do import Meta + meta = ViaGlobalRefMeta() + meta.symlink_error = None + return meta + from virtualenv.create.via_global_ref.builtin.via_global_self_do import BuiltinViaGlobalRefMeta - return Meta([], True, True) + meta = BuiltinViaGlobalRefMeta() + meta.symlink_error = None + return meta raise RuntimeError setattr(class_n, func_name, a) diff --git a/src/virtualenv/__main__.py b/src/virtualenv/__main__.py index ae85c680b..edc7f9512 100644 --- a/src/virtualenv/__main__.py +++ b/src/virtualenv/__main__.py @@ -20,7 +20,7 @@ def run(args=None, options=None): session = cli_run(args, options) logging.warning(LogSession(session, start)) except ProcessCallFailed as exception: - print("subprocess call failed for {}".format(exception.cmd)) + print("subprocess call failed for {} with code {}".format(exception.cmd, exception.code)) print(exception.out, file=sys.stdout, end="") print(exception.err, file=sys.stderr, end="") raise SystemExit(exception.code) diff --git a/src/virtualenv/create/creator.py b/src/virtualenv/create/creator.py index 4c920dc64..d4a2b9360 100644 --- a/src/virtualenv/create/creator.py +++ b/src/virtualenv/create/creator.py @@ -24,6 +24,11 @@ DEBUG_SCRIPT = HERE / "debug.py" +class CreatorMeta(object): + def __init__(self): + self.error = None + + @add_metaclass(ABCMeta) class Creator(object): """A class that given a python Interpreter creates a virtual environment""" diff --git a/src/virtualenv/create/via_global_ref/api.py b/src/virtualenv/create/via_global_ref/api.py index c01301ff3..aa864c83e 100644 --- a/src/virtualenv/create/via_global_ref/api.py +++ b/src/virtualenv/create/via_global_ref/api.py @@ -6,10 +6,28 @@ from six import add_metaclass +from virtualenv.info import fs_supports_symlink from virtualenv.util.path import Path from virtualenv.util.zipapp import ensure_file_on_disk -from ..creator import Creator +from ..creator import Creator, CreatorMeta + + +class ViaGlobalRefMeta(CreatorMeta): + def __init__(self): + super(ViaGlobalRefMeta, self).__init__() + self.copy_error = None + self.symlink_error = None + if not fs_supports_symlink(): + self.symlink = "the filesystem does not supports symlink" + + @property + def can_copy(self): + return not self.copy_error + + @property + def can_symlink(self): + return not self.symlink_error @add_metaclass(ABCMeta) diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py index 5571520d2..589544c1e 100644 --- a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py +++ b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py @@ -6,6 +6,7 @@ from virtualenv.create.describe import Python3Supports from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest +from virtualenv.create.via_global_ref.store import is_store_python from virtualenv.util.path import Path from .common import CPython, CPythonPosix, CPythonWindows, is_mac_os_framework @@ -25,6 +26,12 @@ def can_describe(cls, interpreter): class CPython3Windows(CPythonWindows, CPython3): """""" + @classmethod + def setup_meta(cls, interpreter): + if is_store_python(interpreter): # store python is not supported here + return None + return super(CPython3Windows, cls).setup_meta(interpreter) + @classmethod def sources(cls, interpreter): for src in super(CPython3Windows, cls).sources(interpreter): diff --git a/src/virtualenv/create/via_global_ref/builtin/ref.py b/src/virtualenv/create/via_global_ref/builtin/ref.py index 15a644aa7..902299496 100644 --- a/src/virtualenv/create/via_global_ref/builtin/ref.py +++ b/src/virtualenv/create/via_global_ref/builtin/ref.py @@ -28,7 +28,10 @@ def __init__(self, src, must_symlink, must_copy): self.must_symlink = must_symlink self.must_copy = must_copy self.src = src - self.exists = src.exists() + try: + self.exists = src.exists() + except OSError: + self.exists = False self._can_read = None if self.exists else False self._can_copy = None if self.exists else False self._can_symlink = None if self.exists else False @@ -141,7 +144,8 @@ def run(self, creator, symlinks): dest = bin_dir / self.base method = self.method(symlinks) method(self.src, dest) - make_exe(dest) + if not symlinks: + make_exe(dest) for extra in self.aliases: link_file = bin_dir / extra if link_file.exists(): @@ -150,4 +154,8 @@ def run(self, creator, symlinks): link_file.symlink_to(self.base) else: copy(self.src, link_file) - make_exe(link_file) + if not symlinks: + make_exe(link_file) + + def __repr__(self): + return "{}(src={}, alias={})".format(self.__class__.__name__, self.src, self.aliases) diff --git a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py index 922a74dad..97bf533d8 100644 --- a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py +++ b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py @@ -1,19 +1,20 @@ from __future__ import absolute_import, unicode_literals -import logging from abc import ABCMeta -from collections import namedtuple from six import add_metaclass from virtualenv.create.via_global_ref.builtin.ref import ExePathRefToDest -from virtualenv.info import fs_supports_symlink from virtualenv.util.path import ensure_dir -from ..api import ViaGlobalRefApi +from ..api import ViaGlobalRefApi, ViaGlobalRefMeta from .builtin_way import VirtualenvBuiltin -Meta = namedtuple("Meta", ["sources", "can_copy", "can_symlink"]) + +class BuiltinViaGlobalRefMeta(ViaGlobalRefMeta): + def __init__(self): + super(BuiltinViaGlobalRefMeta, self).__init__() + self.sources = [] @add_metaclass(ABCMeta) @@ -27,27 +28,30 @@ def can_create(cls, interpreter): """By default all built-in methods assume that if we can describe it we can create it""" # first we must be able to describe it if cls.can_describe(interpreter): - sources = [] - can_copy = True - can_symlink = fs_supports_symlink() - for src in cls.sources(interpreter): - if src.exists: - if can_copy and not src.can_copy: - can_copy = False - logging.debug("%s cannot copy %s", cls.__name__, src) - if can_symlink and not src.can_symlink: - can_symlink = False - logging.debug("%s cannot symlink %s", cls.__name__, src) - if not (can_copy or can_symlink): + meta = cls.setup_meta(interpreter) + if meta is not None and meta: + for src in cls.sources(interpreter): + if src.exists: + if meta.can_copy and not src.can_copy: + meta.copy_error = "cannot copy {}".format(src) + if meta.can_symlink and not src.can_symlink: + meta.symlink_error = "cannot symlink {}".format(src) + if not meta.can_copy and not meta.can_symlink: + meta.error = "neither copy or symlink supported: {}".format( + meta.copy_error, meta.symlink_error + ) + else: + meta.error = "missing required file {}".format(src) + if meta.error: break - else: - logging.debug("%s missing %s", cls.__name__, src) - break - sources.append(src) - else: - return Meta(sources, can_copy, can_symlink) + meta.sources.append(src) + return meta return None + @classmethod + def setup_meta(cls, interpreter): + return BuiltinViaGlobalRefMeta() + @classmethod def sources(cls, interpreter): is_py2 = interpreter.version_info.major == 2 diff --git a/src/virtualenv/create/via_global_ref/store.py b/src/virtualenv/create/via_global_ref/store.py new file mode 100644 index 000000000..55d941347 --- /dev/null +++ b/src/virtualenv/create/via_global_ref/store.py @@ -0,0 +1,21 @@ +from virtualenv.util.path import Path + + +def handle_store_python(meta, interpreter): + if is_store_python(interpreter): + meta.symlink_error = "Windows Store Python does not support virtual environments via symlink" + return meta + + +def is_store_python(interpreter): + parts = Path(interpreter.system_executable).parts + return ( + len(parts) > 4 + and parts[-4] == "Microsoft" + and parts[-3] == "WindowsApps" + and parts[-2].startswith("PythonSoftwareFoundation.Python.3.") + and parts[-1].startswith("python") + ) + + +__all__ = ("handle_store_python", "is_store_python") diff --git a/src/virtualenv/create/via_global_ref/venv.py b/src/virtualenv/create/via_global_ref/venv.py index 5b774ef2d..88e74e36a 100644 --- a/src/virtualenv/create/via_global_ref/venv.py +++ b/src/virtualenv/create/via_global_ref/venv.py @@ -1,18 +1,15 @@ from __future__ import absolute_import, unicode_literals import logging -from collections import namedtuple from copy import copy +from virtualenv.create.via_global_ref.store import handle_store_python from virtualenv.discovery.py_info import PythonInfo from virtualenv.error import ProcessCallFailed -from virtualenv.info import fs_supports_symlink from virtualenv.util.path import ensure_dir from virtualenv.util.subprocess import run_cmd -from .api import ViaGlobalRefApi - -Meta = namedtuple("Meta", ["can_symlink", "can_copy"]) +from .api import ViaGlobalRefApi, ViaGlobalRefMeta class Venv(ViaGlobalRefApi): @@ -30,7 +27,10 @@ def _args(self): @classmethod def can_create(cls, interpreter): if interpreter.has_venv: - return Meta(can_symlink=fs_supports_symlink(), can_copy=True) + meta = ViaGlobalRefMeta() + if interpreter.platform == "win32" and interpreter.version_info.major == 3: + meta = handle_store_python(meta, interpreter) + return meta return None def create(self): diff --git a/src/virtualenv/discovery/builtin.py b/src/virtualenv/discovery/builtin.py index 35734ce5c..24985555a 100644 --- a/src/virtualenv/discovery/builtin.py +++ b/src/virtualenv/discovery/builtin.py @@ -55,22 +55,29 @@ def get_interpreter(key, app_data=None): def propose_interpreters(spec, app_data): - # 1. if it's an absolute path and exists, use that - if spec.is_abs and os.path.exists(spec.path): - yield PythonInfo.from_exe(spec.path, app_data), True - - # 2. try with the current - yield PythonInfo.current_system(app_data), True - - # 3. otherwise fallback to platform default logic - if IS_WIN: - from .windows import propose_interpreters + # 1. if it's a path and exists + if spec.path is not None: + try: + os.lstat(spec.path) # Windows Store Python does not work with os.path.exists, but does for os.lstat + except OSError: + if spec.is_abs: + raise + else: + yield PythonInfo.from_exe(os.path.abspath(spec.path), app_data), True + if spec.is_abs: + return + else: + # 2. otherwise try with the current + yield PythonInfo.current_system(app_data), True - for interpreter in propose_interpreters(spec, app_data): - yield interpreter, True + # 3. otherwise fallback to platform default logic + if IS_WIN: + from .windows import propose_interpreters + for interpreter in propose_interpreters(spec, app_data): + yield interpreter, True + # finally just find on path, the path order matters (as the candidates are less easy to control by end user) paths = get_paths() - # find on path, the path order matters (as the candidates are less easy to control by end user) tested_exes = set() for pos, path in enumerate(paths): path = ensure_text(path) diff --git a/src/virtualenv/discovery/cached_py_info.py b/src/virtualenv/discovery/cached_py_info.py index e2f1d54fd..baed2e38e 100644 --- a/src/virtualenv/discovery/cached_py_info.py +++ b/src/virtualenv/discovery/cached_py_info.py @@ -65,11 +65,14 @@ def _get_via_file_cache(cls, py_info_cache, app_data, resolved_path, exe): key = sha256(str(resolved_path).encode("utf-8") if PY3 else str(resolved_path)).hexdigest() py_info = None resolved_path_text = ensure_text(str(resolved_path)) - resolved_path_modified_timestamp = resolved_path.stat().st_mtime + try: + resolved_path_modified_timestamp = resolved_path.stat().st_mtime + except OSError: + resolved_path_modified_timestamp = -1 data_file = py_info_cache / "{}.json".format(key) with py_info_cache.lock_for_key(key): data_file_path = data_file.path - if data_file_path.exists(): # if exists and matches load + if data_file_path.exists() and resolved_path_modified_timestamp != 1: # if exists and matches load try: data = json.loads(data_file_path.read_text()) if data["path"] == resolved_path_text and data["st_mtime"] == resolved_path_modified_timestamp: diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index 98c0b978a..e94528bcc 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -419,7 +419,7 @@ def _find_possible_folders(self, inside_folder): # or at root level candidate_folder[inside_folder] = None - return list(candidate_folder.keys()) + return list(i for i in candidate_folder.keys() if os.path.exists(i)) def _find_possible_exe_names(self): name_candidate = OrderedDict() diff --git a/src/virtualenv/discovery/py_spec.py b/src/virtualenv/discovery/py_spec.py index 9b9f56e51..71712d1ec 100644 --- a/src/virtualenv/discovery/py_spec.py +++ b/src/virtualenv/discovery/py_spec.py @@ -64,11 +64,13 @@ def _int_or_none(val): arch = _int_or_none(groups["arch"]) if not ok: - path = os.path.abspath(string_spec) + path = string_spec return cls(string_spec, impl, major, minor, micro, arch, path) def generate_names(self): + if self.implementation is None: + return impls = OrderedDict() if self.implementation: # first consider implementation as it is diff --git a/src/virtualenv/run/plugin/creators.py b/src/virtualenv/run/plugin/creators.py index cbf0a5d22..4c349cdb6 100644 --- a/src/virtualenv/run/plugin/creators.py +++ b/src/virtualenv/run/plugin/creators.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from collections import OrderedDict, namedtuple +from collections import OrderedDict, defaultdict, namedtuple from virtualenv.create.describe import Describe from virtualenv.create.via_global_ref.builtin.builtin_way import VirtualenvBuiltin @@ -13,26 +13,37 @@ class CreatorSelector(ComponentBuilder): def __init__(self, interpreter, parser): creators, self.key_to_meta, self.describe, self.builtin_key = self.for_interpreter(interpreter) - if not creators: - raise RuntimeError("No virtualenv implementation for {}".format(interpreter)) super(CreatorSelector, self).__init__(interpreter, parser, "creator", creators) @classmethod def for_interpreter(cls, interpreter): key_to_class, key_to_meta, builtin_key, describe = OrderedDict(), {}, None, None + errored = defaultdict(list) for key, creator_class in cls.options("virtualenv.create").items(): if key == "builtin": raise RuntimeError("builtin creator is a reserved name") meta = creator_class.can_create(interpreter) if meta: - if "builtin" not in key_to_class and issubclass(creator_class, VirtualenvBuiltin): - builtin_key = key - key_to_class["builtin"] = creator_class - key_to_meta["builtin"] = meta - key_to_class[key] = creator_class - key_to_meta[key] = meta + if meta.error: + errored[meta.error].append(creator_class) + else: + if "builtin" not in key_to_class and issubclass(creator_class, VirtualenvBuiltin): + builtin_key = key + key_to_class["builtin"] = creator_class + key_to_meta["builtin"] = meta + key_to_class[key] = creator_class + key_to_meta[key] = meta if describe is None and issubclass(creator_class, Describe) and creator_class.can_describe(interpreter): describe = creator_class + if not key_to_meta: + if errored: + raise RuntimeError( + "\n".join( + "{} for creators {}".format(k, ", ".join(i.__name__ for i in v)) for k, v in errored.items() + ) + ) + else: + raise RuntimeError("No virtualenv implementation for {}".format(interpreter)) return CreatorInfo( key_to_class=key_to_class, key_to_meta=key_to_meta, describe=describe, builtin_key=builtin_key ) diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py index cf30f2bc3..4d86ffd95 100644 --- a/tests/unit/create/test_creator.py +++ b/tests/unit/create/test_creator.py @@ -21,7 +21,7 @@ from virtualenv.create.creator import DEBUG_SCRIPT, Creator, get_env_debug_info from virtualenv.discovery.builtin import get_interpreter from virtualenv.discovery.py_info import PythonInfo -from virtualenv.info import IS_PYPY, IS_WIN, PY2, PY3, fs_is_case_sensitive, fs_supports_symlink +from virtualenv.info import IS_PYPY, IS_WIN, PY2, PY3, fs_is_case_sensitive from virtualenv.pyenv_cfg import PyEnvCfg from virtualenv.run import cli_run, session_via_cli from virtualenv.util.path import Path @@ -88,6 +88,13 @@ def system(session_app_data): CURRENT_CREATORS = list(i for i in CURRENT.creators().key_to_class.keys() if i != "builtin") +CREATE_METHODS = [] +for k, v in CURRENT.creators().key_to_meta.items(): + if k in CURRENT_CREATORS: + if v.can_copy: + CREATE_METHODS.append((k, "copies")) + if v.can_symlink: + CREATE_METHODS.append((k, "symlinks")) _VENV_BUG_ON = ( IS_PYPY and CURRENT.version_info[0:3] == (3, 6, 9) @@ -97,7 +104,7 @@ def system(session_app_data): @pytest.mark.parametrize( - "creator, method, isolated", + "creator, isolated", [ pytest.param( *i, @@ -106,15 +113,15 @@ def system(session_app_data): strict=True, ) ) - if _VENV_BUG_ON and i[0] == "venv" and i[1] == "copies" + if _VENV_BUG_ON and i[0][0] == "venv" and i[0][1] == "copies" else i - for i in product( - CURRENT_CREATORS, (["copies"] + (["symlinks"] if fs_supports_symlink() else [])), ["isolated", "global"] - ) + for i in product(CREATE_METHODS, ["isolated", "global"]) ], + ids=lambda i: "-".join(i) if isinstance(i, tuple) else i, ) -def test_create_no_seed(python, creator, isolated, system, coverage_env, special_name_dir, method): +def test_create_no_seed(python, creator, isolated, system, coverage_env, special_name_dir): dest = special_name_dir + creator_key, method = creator cmd = [ "-v", "-v", @@ -125,7 +132,7 @@ def test_create_no_seed(python, creator, isolated, system, coverage_env, special "--activators", "", "--creator", - creator, + creator_key, "--{}".format(method), ] if isolated == "global": @@ -188,11 +195,11 @@ def list_to_str(iterable): else: exes = ("python", "python{}".format(*sys.version_info), "python{}.{}".format(*sys.version_info)) # pypy3<=7.3: https://bitbucket.org/pypy/pypy/pull-requests/697 - if IS_PYPY and CURRENT.pypy_version_info[:3] <= [7, 3, 0] and creator == "venv": + if IS_PYPY and creator_key == "venv": exes = exes[:-1] for exe in exes: exe_path = result.creator.bin_dir / exe - assert exe_path.exists() + assert exe_path.exists(), "\n".join(str(i) for i in result.creator.bin_dir.iterdir()) if not exe_path.is_symlink(): # option 1: a real file continue # it was a file link = os.readlink(str(exe_path)) diff --git a/tests/unit/discovery/py_info/test_py_info.py b/tests/unit/discovery/py_info/test_py_info.py index ad9d399e2..e673fcdd2 100644 --- a/tests/unit/discovery/py_info/test_py_info.py +++ b/tests/unit/discovery/py_info/test_py_info.py @@ -15,6 +15,7 @@ from virtualenv.discovery.py_info import PythonInfo, VersionInfo from virtualenv.discovery.py_spec import PythonSpec from virtualenv.info import fs_supports_symlink +from virtualenv.util.path import Path CURRENT = PythonInfo.current_system() @@ -149,6 +150,9 @@ def test_py_info_cached_symlink(mocker, tmp_path, session_app_data): new_exe = tmp_path / "a" new_exe.symlink_to(sys.executable) + pyvenv = Path(sys.executable).parents[1] / "pyvenv.cfg" + if pyvenv.exists(): + (tmp_path / pyvenv.name).write_text(pyvenv.read_text()) new_exe_str = str(new_exe) second_result = PythonInfo.from_exe(new_exe_str, session_app_data) assert second_result.executable == new_exe_str diff --git a/tests/unit/discovery/py_info/test_py_info_exe_based_of.py b/tests/unit/discovery/py_info/test_py_info_exe_based_of.py index 9b87be02c..3bc3e7939 100644 --- a/tests/unit/discovery/py_info/test_py_info_exe_based_of.py +++ b/tests/unit/discovery/py_info/test_py_info_exe_based_of.py @@ -7,6 +7,7 @@ from virtualenv.discovery.py_info import EXTENSIONS, PythonInfo from virtualenv.info import IS_WIN, fs_is_case_sensitive, fs_supports_symlink +from virtualenv.util.path import Path CURRENT = PythonInfo.current() @@ -31,6 +32,9 @@ def test_discover_ok(tmp_path, monkeypatch, suffix, impl, version, arch, into, c folder.mkdir(parents=True, exist_ok=True) dest = folder / "{}{}".format(impl, version, arch, suffix) os.symlink(CURRENT.executable, str(dest)) + pyvenv = Path(CURRENT.executable).parents[1] / "pyvenv.cfg" + if pyvenv.exists(): + (folder / pyvenv.name).write_text(pyvenv.read_text()) inside_folder = str(tmp_path) base = CURRENT.discover_exe(session_app_data, inside_folder) found = base.executable diff --git a/tests/unit/discovery/test_discovery.py b/tests/unit/discovery/test_discovery.py index 260052d4b..275623a31 100644 --- a/tests/unit/discovery/test_discovery.py +++ b/tests/unit/discovery/test_discovery.py @@ -10,6 +10,7 @@ from virtualenv.discovery.builtin import get_interpreter from virtualenv.discovery.py_info import PythonInfo from virtualenv.info import fs_supports_symlink +from virtualenv.util.path import Path from virtualenv.util.six import ensure_text @@ -25,10 +26,14 @@ def test_discovery_via_path(monkeypatch, case, special_name_dir, caplog, session elif case == "upper": name = name.upper() exe_name = "{}{}{}".format(name, current.version_info.major, ".exe" if sys.platform == "win32" else "") - special_name_dir.mkdir() - executable = special_name_dir / exe_name + target = special_name_dir / current.distutils_install["scripts"] + target.mkdir(parents=True) + executable = target / exe_name os.symlink(sys.executable, ensure_text(str(executable))) - new_path = os.pathsep.join([str(special_name_dir)] + os.environ.get(str("PATH"), str("")).split(os.pathsep)) + pyvenv_cfg = Path(sys.executable).parents[1] / "pyvenv.cfg" + if pyvenv_cfg.exists(): + (target / pyvenv_cfg.name).write_bytes(pyvenv_cfg.read_bytes()) + new_path = os.pathsep.join([str(target)] + os.environ.get(str("PATH"), str("")).split(os.pathsep)) monkeypatch.setenv(str("PATH"), new_path) interpreter = get_interpreter(core) diff --git a/tests/unit/discovery/test_py_spec.py b/tests/unit/discovery/test_py_spec.py index f4496e980..93473b53f 100644 --- a/tests/unit/discovery/test_py_spec.py +++ b/tests/unit/discovery/test_py_spec.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, unicode_literals import itertools -import os import sys from copy import copy @@ -15,7 +14,7 @@ def test_bad_py_spec(): spec = PythonSpec.from_string_spec(text) assert text in repr(spec) assert spec.str_spec == text - assert spec.path == os.path.abspath(text) + assert spec.path == text content = vars(spec) del content[str("str_spec")] del content[str("path")] @@ -112,4 +111,4 @@ def test_relative_spec(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) a_relative_path = str((tmp_path / "a" / "b").relative_to(tmp_path)) spec = PythonSpec.from_string_spec(a_relative_path) - assert spec.path == os.path.abspath(str(tmp_path / a_relative_path)) + assert spec.path == a_relative_path