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

Extract the logic of finding interpreters to a method #326

Merged
merged 6 commits into from Mar 22, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions news/326.feature.md
@@ -0,0 +1 @@
Refactor: Extract the logic of finding interpreters to method for the sake of subclass overriding.
54 changes: 19 additions & 35 deletions pdm/cli/actions.py
Expand Up @@ -14,7 +14,6 @@
from pdm.cli.utils import (
check_project_file,
find_importable_files,
find_python_in_path,
format_lockfile,
format_resolution_impossible,
save_version_specifiers,
Expand Down Expand Up @@ -417,42 +416,27 @@ def do_use(project: Project, python: str, first: bool = False) -> None:
"""Use the specified python version and save in project config.
The python can be a version string or interpreter path.
"""
import pythonfinder

python = python.strip()
if python and not all(c.isdigit() for c in python.split(".")):
if Path(python).exists():
python_path = find_python_in_path(python)
else:
python_path = shutil.which(python)
if not python_path:
raise NoPythonVersion(f"{python} is not a valid Python.")
python_version, is_64bit = get_python_version(python_path, True)
found_interpreters = list(dict.fromkeys(project.find_interpreters(python)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

found_interpreters = set(project.find_interpreters(python))?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, the difference is dict.fromkeys() preserves the order while set doesn't.

if not found_interpreters:
raise NoPythonVersion(
f"Python interpreter {python} is not found on the system."
)
if first or len(found_interpreters) == 1:
python_path = found_interpreters[0]
else:
finder = pythonfinder.Finder()
pythons = []
args = [int(v) for v in python.split(".") if v != ""]
for i, entry in enumerate(finder.find_all_python_versions(*args)):
python_version, is_64bit = get_python_version(entry.path.as_posix(), True)
pythons.append((entry.path.as_posix(), python_version, is_64bit))
if not pythons:
raise NoPythonVersion(f"Python {python} is not available on the system.")

if not first and len(pythons) > 1:
for i, (path, python_version, is_64bit) in enumerate(pythons):
project.core.ui.echo(
f"{i}. {termui.green(path)} "
f"({get_python_version_string(python_version, is_64bit)})"
)
selection = click.prompt(
"Please select:",
type=click.Choice([str(i) for i in range(len(pythons))]),
default="0",
show_choices=False,
)
else:
selection = 0
python_path, python_version, is_64bit = pythons[int(selection)]
for i, path in enumerate(found_interpreters):
python_version, is_64bit = get_python_version(path, True)
version_string = get_python_version_string(python_version, is_64bit)
project.core.ui.echo(f"{i}. {termui.green(path)} ({version_string})")
selection = click.prompt(
"Please select:",
type=click.Choice([str(i) for i in range(len(found_interpreters))]),
default="0",
show_choices=False,
)
python_path = found_interpreters[int(selection)]
python_version, is_64bit = get_python_version(python_path, True)

if not project.python_requires.contains(python_version):
raise NoPythonVersion(
Expand Down
27 changes: 1 addition & 26 deletions pdm/cli/utils.py
Expand Up @@ -2,7 +2,6 @@

import argparse
import os
import re
from collections import ChainMap
from pathlib import Path
from typing import TYPE_CHECKING
Expand All @@ -13,7 +12,7 @@
from resolvelib.structs import DirectedGraph

from pdm import termui
from pdm.exceptions import NoPythonVersion, ProjectError
from pdm.exceptions import ProjectError
from pdm.formats import FORMATS
from pdm.formats.base import make_array, make_inline_table
from pdm.models.environment import WorkingSet
Expand Down Expand Up @@ -443,27 +442,3 @@ def format_resolution_impossible(err: ResolutionImpossible) -> str:
"set a narrower `requires-python` range in the pyproject.toml."
)
return "\n".join(result)


def find_python_in_path(path: os.PathLike) -> str:
"""Find a python interpreter from the given path, the input argument could be:

- A valid path to the interpreter
- A Python root diretory that contains the interpreter
"""
pathlib_path = Path(path).absolute()
if pathlib_path.is_file():
return pathlib_path.as_posix()

if os.name == "nt":
for root_dir in (pathlib_path, pathlib_path / "Scripts"):
if root_dir.joinpath("python.exe").exists():
return root_dir.joinpath("python.exe").as_posix()
else:
executable_pattern = re.compile(r"python(?:\d(?:\.\d+m?)?)?$")

for python in pathlib_path.joinpath("bin").glob("python*"):
if executable_pattern.match(python.name):
return python.as_posix()

raise NoPythonVersion(f"No Python interpreter is found at {path!r}")
25 changes: 25 additions & 0 deletions pdm/project/core.py
Expand Up @@ -33,6 +33,7 @@
cached_property,
cd,
find_project_root,
find_python_in_path,
get_venv_python,
is_venv_python,
setdefault,
Expand Down Expand Up @@ -515,3 +516,27 @@ def make_candidate_info_cache(self) -> CandidateInfoCache:

def make_hash_cache(self) -> HashCache:
return HashCache(directory=self.cache("hashes").as_posix())

def find_interpreters(self, python_spec: str) -> Iterable[str]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why return a str here instead of a Path?

"""Return an iterable of interpreter paths that matches the given specifier,
which can be:
1. a version specifier like 3.7
2. an absolute path
3. a short name like python3
"""
import pythonfinder

if python_spec and not all(c.isdigit() for c in python_spec.split(".")):
if Path(python_spec).exists():
python_path = find_python_in_path(python_spec)
if python_path:
yield os.path.abspath(python_path)
else:
python_path = shutil.which(python_spec)
if python_path:
yield python_path
return
args = [int(v) for v in python_spec.split(".") if v != ""]
finder = pythonfinder.Finder()
for entry in finder.find_all_python_versions(*args):
yield entry.path.as_posix()
24 changes: 24 additions & 0 deletions pdm/utils.py
Expand Up @@ -412,3 +412,27 @@ def is_venv_python(interpreter: os.PathLike) -> bool:
else:
return True
return False


def find_python_in_path(path: os.PathLike) -> Optional[Path]:
"""Find a python interpreter from the given path, the input argument could be:

- A valid path to the interpreter
- A Python root directory that contains the interpreter
"""
pathlib_path = Path(path).absolute()
if pathlib_path.is_file():
return pathlib_path

if os.name == "nt":
for root_dir in (pathlib_path, pathlib_path / "Scripts"):
if root_dir.joinpath("python.exe").exists():
return root_dir.joinpath("python.exe")
else:
executable_pattern = re.compile(r"python(?:\d(?:\.\d+m?)?)?$")

for python in pathlib_path.joinpath("bin").glob("python*"):
if executable_pattern.match(python.name):
return python

return None
1 change: 1 addition & 0 deletions pyproject.toml
Expand Up @@ -43,6 +43,7 @@ classifiers = [
"Development Status :: 4 - Beta",
"Topic :: Software Development :: Build Tools",
]
includes = ["pdm/"]

[project.urls]
homepage = "https://pdm.fming.dev"
Expand Down
13 changes: 4 additions & 9 deletions tests/test_utils.py
Expand Up @@ -4,8 +4,6 @@
import pytest

from pdm import utils
from pdm.cli import utils as cli_utils
from pdm.exceptions import PdmException


@pytest.mark.parametrize(
Expand Down Expand Up @@ -51,14 +49,11 @@ def test_expend_env_vars_in_auth(given, expected, monkeypatch):

def test_find_python_in_path(tmp_path):

assert utils.find_python_in_path(sys.executable) == pathlib.Path(sys.executable)
assert (
cli_utils.find_python_in_path(sys.executable)
== pathlib.Path(sys.executable).as_posix()
)
assert (
cli_utils.find_python_in_path(sys.prefix)
utils.find_python_in_path(sys.prefix)
.as_posix()
.lower()
.startswith(pathlib.Path(sys.executable).as_posix().lower())
)
with pytest.raises(PdmException):
cli_utils.find_python_in_path(tmp_path)
assert not utils.find_python_in_path(tmp_path)