Skip to content

Commit

Permalink
Fix executables discovery for generic environments
Browse files Browse the repository at this point in the history
  • Loading branch information
sdispater committed Sep 6, 2021
1 parent d526348 commit 7ca2060
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 34 deletions.
155 changes: 121 additions & 34 deletions poetry/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -1042,20 +1042,21 @@ def get_system_env(cls, naive: bool = False) -> Union["SystemEnv", "GenericEnv"]
(e.g. plugin installation or self update).
"""
prefix, base_prefix = Path(sys.prefix), Path(cls.get_base_prefix())
env = SystemEnv(prefix)
if not naive:
if prefix.joinpath("poetry_env").exists():
return GenericEnv(base_prefix)

from poetry.locations import data_dir

try:
prefix.relative_to(data_dir())
except ValueError:
pass
env = GenericEnv(base_prefix, child_env=env)
else:
return GenericEnv(base_prefix)
from poetry.locations import data_dir

return SystemEnv(prefix)
try:
prefix.relative_to(data_dir())
except ValueError:
pass
else:
env = GenericEnv(base_prefix, child_env=env)

return env

@classmethod
def get_base_prefix(cls) -> Path:
Expand Down Expand Up @@ -1093,29 +1094,10 @@ def __init__(self, path: Path, base: Optional[Path] = None) -> None:
self._path = path
self._bin_dir = self._path / bin_dir

try:
python_executables = sorted(
[
p.name
for p in self._bin_dir.glob("python*")
if re.match(r"python(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name)
]
)
self._executable = python_executables[0].rstrip(".exe")
except IndexError:
self._executable = "python" + (".exe" if self._is_windows else "")
self._executable = "python"
self._pip_executable = "pip"

try:
pip_executables = sorted(
[
p.name
for p in self._bin_dir.glob("pip*")
if re.match(r"pip(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name)
]
)
self._pip_executable = pip_executables[0].rstrip(".exe")
except IndexError:
self._pip_executable = "pip" + (".exe" if self._is_windows else "")
self.find_executables()

self._base = base or path

Expand Down Expand Up @@ -1160,6 +1142,39 @@ def marker_env(self) -> Dict[str, Any]:

return self._marker_env

@property
def parent_env(self) -> "GenericEnv":
return GenericEnv(self.base, child_env=self)

def find_executables(self) -> None:
python_executables = sorted(
[
p.name
for p in self._bin_dir.glob("python*")
if re.match(r"python(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name)
]
)
if python_executables:
executable = python_executables[0]
if executable.endswith(".exe"):
executable = executable[:-4]

self._executable = executable

pip_executables = sorted(
[
p.name
for p in self._bin_dir.glob("pip*")
if re.match(r"pip(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name)
]
)
if pip_executables:
pip_executable = pip_executables[0]
if pip_executable.endswith(".exe"):
pip_executable = pip_executable[:-4]

self._pip_executable = pip_executable

def get_embedded_wheel(self, distribution):
return get_embed_wheel(
distribution, "{}.{}".format(self.version_info[0], self.version_info[1])
Expand Down Expand Up @@ -1390,7 +1405,11 @@ def _bin(self, bin: str) -> str:
"""
Return path to the given executable.
"""
bin_path = (self._bin_dir / bin).with_suffix(".exe" if self._is_windows else "")
if self._is_windows and not bin.endswith(".exe"):
bin_path = self._bin_dir / (bin + ".exe")
else:
bin_path = self._bin_dir / bin

if not bin_path.exists():
# On Windows, some executables can be in the base path
# This is especially true when installing Python with
Expand All @@ -1401,7 +1420,11 @@ def _bin(self, bin: str) -> str:
# that creates a fake virtual environment pointing to
# a base Python install.
if self._is_windows:
bin_path = (self._path / bin).with_suffix(".exe")
if not bin.endswith(".exe"):
bin_path = self._bin_dir / (bin + ".exe")
else:
bin_path = self._path / bin

if bin_path.exists():
return str(bin_path)

Expand Down Expand Up @@ -1649,6 +1672,70 @@ def _updated_path(self) -> str:


class GenericEnv(VirtualEnv):
def __init__(
self, path: Path, base: Optional[Path] = None, child_env: Optional["Env"] = None
) -> None:
self._child_env = child_env

super().__init__(path, base=base)

def find_executables(self) -> None:
patterns = [("python*", "pip*")]

if self._child_env:
minor_version = "{}.{}".format(
self._child_env.version_info[0], self._child_env.version_info[1]
)
major_version = "{}".format(self._child_env.version_info[0])
patterns = [
("python{}".format(minor_version), "pip{}".format(minor_version)),
("python{}".format(major_version), "pip{}".format(major_version)),
]

python_executable = None
pip_executable = None

for python_pattern, pip_pattern in patterns:
if python_executable and pip_executable:
break

if not python_executable:
python_executables = sorted(
[
p.name
for p in self._bin_dir.glob(python_pattern)
if re.match(r"python(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name)
]
)

if python_executables:
executable = python_executables[0]
if executable.endswith(".exe"):
executable = executable[:-4]

python_executable = executable

if not pip_executable:
pip_executables = sorted(
[
p.name
for p in self._bin_dir.glob(pip_pattern)
if re.match(r"pip(?:\d+(?:\.\d+)?)?(?:\.exe)?$", p.name)
]
)
if pip_executables:
pip_executable = pip_executables[0]
if pip_executable.endswith(".exe"):
pip_executable = pip_executable[:-4]

pip_executable = pip_executable

if python_executable:
self._executable = python_executable

if pip_executable:
self._pip_executable = pip_executable

def get_paths(self) -> Dict[str, str]:
output = self.run_python_script(GET_PATHS_FOR_GENERIC_ENVS)

Expand Down
78 changes: 78 additions & 0 deletions tests/utils/test_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from poetry.utils.env import GET_BASE_PREFIX
from poetry.utils.env import EnvCommandError
from poetry.utils.env import EnvManager
from poetry.utils.env import GenericEnv
from poetry.utils.env import NoCompatiblePythonVersionFound
from poetry.utils.env import SystemEnv
from poetry.utils.env import VirtualEnv
Expand Down Expand Up @@ -1001,3 +1002,80 @@ def test_env_finds_the_correct_executables(tmp_dir, manager):

assert Path(venv.python).name == expected_executable
assert Path(venv.pip).name == expected_pip_executable


def test_env_finds_the_correct_executables_for_generic_env(tmp_dir, manager):
venv_path = Path(tmp_dir) / "Virtual Env"
child_venv_path = Path(tmp_dir) / "Child Virtual Env"
manager.build_venv(str(venv_path), with_pip=True)
parent_venv = VirtualEnv(venv_path)
manager.build_venv(str(child_venv_path), executable=parent_venv.python, with_pip=True)
venv = GenericEnv(parent_venv.path, child_env=VirtualEnv(child_venv_path))

expected_executable = "python{}.{}{}".format(
sys.version_info[0], sys.version_info[1], ".exe" if WINDOWS else ""
)
expected_pip_executable = "pip{}.{}{}".format(
sys.version_info[0], sys.version_info[1], ".exe" if WINDOWS else ""
)

assert Path(venv.python).name == expected_executable
assert Path(venv.pip).name == expected_pip_executable


def test_env_finds_fallback_executables_for_generic_env(tmp_dir, manager):
venv_path = Path(tmp_dir) / "Virtual Env"
child_venv_path = Path(tmp_dir) / "Child Virtual Env"
manager.build_venv(str(venv_path), with_pip=True)
parent_venv = VirtualEnv(venv_path)
manager.build_venv(str(child_venv_path), executable=parent_venv.python, with_pip=True)
venv = GenericEnv(parent_venv.path, child_env=VirtualEnv(child_venv_path))

default_executable = "python" + (".exe" if WINDOWS else "")
major_executable = "python{}{}".format(
sys.version_info[0], ".exe" if WINDOWS else ""
)
minor_executable = "python{}.{}{}".format(
sys.version_info[0], sys.version_info[1], ".exe" if WINDOWS else ""
)
expected_executable = minor_executable
if (
venv._bin_dir.joinpath(expected_executable).exists()
and venv._bin_dir.joinpath(major_executable).exists()
):
venv._bin_dir.joinpath(expected_executable).unlink()
expected_executable = major_executable

if (
venv._bin_dir.joinpath(expected_executable).exists()
and venv._bin_dir.joinpath(default_executable).exists()
):
venv._bin_dir.joinpath(expected_executable).unlink()
expected_executable = default_executable

default_pip_executable = "pip" + (".exe" if WINDOWS else "")
major_pip_executable = "pip{}{}".format(
sys.version_info[0], ".exe" if WINDOWS else ""
)
minor_pip_executable = "pip{}.{}{}".format(
sys.version_info[0], sys.version_info[1], ".exe" if WINDOWS else ""
)
expected_pip_executable = minor_pip_executable
if (
venv._bin_dir.joinpath(expected_pip_executable).exists()
and venv._bin_dir.joinpath(major_pip_executable).exists()
):
venv._bin_dir.joinpath(expected_pip_executable).unlink()
expected_pip_executable = major_pip_executable

if (
venv._bin_dir.joinpath(expected_pip_executable).exists()
and venv._bin_dir.joinpath(default_pip_executable).exists()
):
venv._bin_dir.joinpath(expected_pip_executable).unlink()
expected_pip_executable = default_pip_executable

venv = GenericEnv(parent_venv.path, child_env=VirtualEnv(child_venv_path))

assert Path(venv.python).name == expected_executable
assert Path(venv.pip).name == expected_pip_executable

0 comments on commit 7ca2060

Please sign in to comment.