Skip to content

Commit

Permalink
use shutil.which() to detect the active python (#7771)
Browse files Browse the repository at this point in the history
Co-authored-by: Randy Döring <[email protected]>
  • Loading branch information
dimbleby and radoering authored Apr 8, 2023
1 parent f6e1f93 commit dfb4904
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 36 deletions.
72 changes: 42 additions & 30 deletions src/poetry/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import platform
import plistlib
import re
import shutil
import subprocess
import sys
import sysconfig
Expand Down Expand Up @@ -472,6 +473,11 @@ def __init__(self, e: CalledProcessError, input: str | None = None) -> None:
super().__init__("\n\n".join(message_parts))


class PythonVersionNotFound(EnvError):
def __init__(self, expected: str) -> None:
super().__init__(f"Could not find the python executable {expected}")


class NoCompatiblePythonVersionFound(EnvError):
def __init__(self, expected: str, given: str | None = None) -> None:
if given:
Expand Down Expand Up @@ -517,41 +523,47 @@ def __init__(self, poetry: Poetry, io: None | IO = None) -> None:
self._io = io or NullIO()

@staticmethod
def _full_python_path(python: str) -> Path:
def _full_python_path(python: str) -> Path | None:
# eg first find pythonXY.bat on windows.
path_python = shutil.which(python)
if path_python is None:
return None

try:
executable = decode(
subprocess.check_output(
[python, "-c", "import sys; print(sys.executable)"],
[path_python, "-c", "import sys; print(sys.executable)"],
).strip()
)
except CalledProcessError as e:
raise EnvCommandError(e)
return Path(executable)

return Path(executable)
except CalledProcessError:
return None

@staticmethod
def _detect_active_python(io: None | IO = None) -> Path | None:
io = io or NullIO()
executable = None
io.write_error_line(
(
"Trying to detect current active python executable as specified in"
" the config."
),
verbosity=Verbosity.VERBOSE,
)

try:
io.write_error_line(
(
"Trying to detect current active python executable as specified in"
" the config."
),
verbosity=Verbosity.VERBOSE,
)
executable = EnvManager._full_python_path("python")
executable = EnvManager._full_python_path("python")

if executable is not None:
io.write_error_line(f"Found: {executable}", verbosity=Verbosity.VERBOSE)
except EnvCommandError:
else:
io.write_error_line(
(
"Unable to detect the current active python executable. Falling"
" back to default."
),
verbosity=Verbosity.VERBOSE,
)

return executable

@staticmethod
Expand Down Expand Up @@ -592,6 +604,8 @@ def activate(self, python: str) -> Env:
pass

python_path = self._full_python_path(python)
if python_path is None:
raise PythonVersionNotFound(python)

try:
python_version_string = decode(
Expand Down Expand Up @@ -949,25 +963,26 @@ def create_venv(
"Trying to find and use a compatible version.</warning> "
)

for python_to_try in sorted(
for suffix in sorted(
self._poetry.package.AVAILABLE_PYTHONS,
key=lambda v: (v.startswith("3"), -len(v), v),
reverse=True,
):
if len(python_to_try) == 1:
if not parse_constraint(f"^{python_to_try}.0").allows_any(
if len(suffix) == 1:
if not parse_constraint(f"^{suffix}.0").allows_any(
supported_python
):
continue
elif not supported_python.allows_any(
parse_constraint(python_to_try + ".*")
):
elif not supported_python.allows_any(parse_constraint(suffix + ".*")):
continue

python = "python" + python_to_try

python_name = f"python{suffix}"
if self._io.is_debug():
self._io.write_error_line(f"<debug>Trying {python}</debug>")
self._io.write_error_line(f"<debug>Trying {python_name}</debug>")

python = self._full_python_path(python_name)
if python is None:
continue

try:
python_patch = decode(
Expand All @@ -979,14 +994,11 @@ def create_venv(
except CalledProcessError:
continue

if not python_patch:
continue

if supported_python.allows(Version.parse(python_patch)):
self._io.write_error_line(
f"Using <c1>{python}</c1> ({python_patch})"
f"Using <c1>{python_name}</c1> ({python_patch})"
)
executable = self._full_python_path(python)
executable = python
python_minor = ".".join(python_patch.split(".")[:2])
break

Expand Down
8 changes: 6 additions & 2 deletions tests/console/commands/env/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import os

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
Expand Down Expand Up @@ -28,9 +30,11 @@ def check_output(cmd: list[str], *args: Any, **kwargs: Any) -> str:
elif "sys.version_info[:2]" in python_cmd:
return f"{version.major}.{version.minor}"
elif "import sys; print(sys.executable)" in python_cmd:
return f"/usr/bin/{cmd[0]}"
executable = cmd[0]
basename = os.path.basename(executable)
return f"/usr/bin/{basename}"
else:
assert "import sys; print(sys.prefix)" in python_cmd
return str(Path("/prefix"))
return "/prefix"

return check_output
5 changes: 5 additions & 0 deletions tests/console/commands/env/test_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(
venv_name: str,
venvs_in_cache_config: None,
) -> None:
mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}")
mocker.patch(
"subprocess.check_output",
side_effect=check_output_wrapper(),
Expand Down Expand Up @@ -94,6 +95,7 @@ def test_activate_activates_non_existing_virtualenv_no_envs_file(


def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
mocker: MockerFixture,
tester: CommandTester,
current_python: tuple[int, int, int],
venv_cache: Path,
Expand All @@ -112,6 +114,8 @@ def test_get_prefers_explicitly_activated_virtualenvs_over_env_var(
doc[venv_name] = {"minor": python_minor, "patch": python_patch}
envs_file.write(doc)

mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}")

tester.execute(python_minor)

expected = f"""\
Expand All @@ -134,6 +138,7 @@ def test_get_prefers_explicitly_activated_non_existing_virtualenvs_over_env_var(
python_minor = ".".join(str(v) for v in current_python[:2])
venv_dir = venv_cache / f"{venv_name}-py{python_minor}"

mocker.patch("shutil.which", side_effect=lambda py: f"/usr/bin/{py}")
mocker.patch(
"poetry.utils.env.EnvManager._env",
new_callable=mocker.PropertyMock,
Expand Down
Loading

0 comments on commit dfb4904

Please sign in to comment.