Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use shutil.which() to detect the active python #7771

Merged
merged 2 commits into from
Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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