diff --git a/news/54.feature b/news/54.feature new file mode 100644 index 0000000000000000000000000000000000000000..8502d08c70cc0cdec61c7d4c09a9380ff5a3d935 --- /dev/null +++ b/news/54.feature @@ -0,0 +1 @@ +Improve the user interface of selecting Python interpreter. diff --git a/pdm/cli/actions.py b/pdm/cli/actions.py index 703e0329951346dfde11139271d03be2896aad80..2bd9888cf40142c18c1561953036f0685471f9d6 100644 --- a/pdm/cli/actions.py +++ b/pdm/cli/actions.py @@ -443,22 +443,41 @@ def do_init( project.write_pyproject() -def do_use(project: Project, python: str) -> None: +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. """ - if Path(python).is_absolute(): - python_path = python - else: - python_path = shutil.which(python) + if not all(c.isdigit() for c in python.split(".")): + if Path(python).exists(): + python_path = Path(python).absolute().as_posix() + else: + python_path = shutil.which(python) if not python_path: - finder = pythonfinder.Finder() - try: - python_path = finder.find_python_version(python).path.as_posix() - except AttributeError: - raise NoPythonVersion(f"Python {python} is not found on the system.") + raise NoPythonVersion(f"{python} is not a valid Python.") + python_version = get_python_version(python_path, True) + else: + finder = pythonfinder.Finder() + pythons = [] + args = [int(v) for v in python.split(".")] + for i, entry in enumerate(finder.find_all_python_versions(*args)): + python_version = get_python_version(entry.path.as_posix(), True) + pythons.append((entry.path.as_posix(), python_version)) + 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) in enumerate(pythons): + context.io.echo(f"{i}: {context.io.green(path)} ({python_version})") + 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 = pythons[int(selection)] - python_version = get_python_version(python_path, True) if not project.python_requires.contains(python_version): raise NoPythonVersion( "The target Python version {} doesn't satisfy " diff --git a/pdm/cli/commands.py b/pdm/cli/commands.py index dce972da2b579b85e448bd14362ed2be2108ea8b..0964cc12236e82ab6256b9b219a4c856a8cc72e7 100644 --- a/pdm/cli/commands.py +++ b/pdm/cli/commands.py @@ -285,17 +285,12 @@ def build(project, sdist, wheel, dest, clean): @cli.command() @verbose_option -@click.option( - "-p", - "--python", - help="Specify the Python interperter version or path to use.", - metavar="PYTHON", -) @pass_project -def init(project, python): +def init(project): """Initialize a pyproject.toml for PDM.""" - if python: - actions.do_use(project, python) + python = click.prompt("Please enter the Python interpreter to use") + actions.do_use(project, python) + if project.pyproject_file.exists(): context.io.echo( "{}".format( @@ -324,11 +319,14 @@ def init(project, python): @cli.command() +@click.option( + "-f", "--first", is_flag=True, help="Select the first matched interpreter." +) @click.argument("python") @pass_project -def use(project, python): +def use(project, first, python): """Use the given python version or path as base interpreter.""" - actions.do_use(project, python) + actions.do_use(project, python, first) @cli.command() diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 1f344e2dbabb86b8743900e46ab56eb7ef6b3408..3c2cb377e09d0111c509119f8ffbef8494dd3f12 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -116,12 +116,12 @@ def test_uncaught_error(invoke, mocker): def test_use_command(project, invoke): python_path = Path(shutil.which("python")).as_posix() - result = invoke(["use", "python"], obj=project) + result = invoke(["use", "-f", "python"], obj=project) assert result.exit_code == 0 config_content = project.root.joinpath(".pdm.toml").read_text() assert python_path in config_content - result = invoke(["use", python_path], obj=project) + result = invoke(["use", "-f", python_path], obj=project) assert result.exit_code == 0 project.tool_settings["python_requires"] = ">=3.6" @@ -130,6 +130,12 @@ def test_use_command(project, invoke): assert result.exit_code == 1 +def test_use_python_by_version(project, invoke): + python_version = ".".join(map(str, sys.version_info[:2])) + result = invoke(["use", "-f", python_version], obj=project) + assert result.exit_code == 0 + + def test_install_with_lockfile(project, invoke, working_set, repository): result = invoke(["lock"], obj=project) assert result.exit_code == 0 @@ -149,7 +155,10 @@ def test_init_command(project_no_init, invoke, mocker): return_value=("Testing", "me@example.org"), ) do_init = mocker.patch.object(actions, "do_init") - result = invoke(["init"], input="test-project\n\n\n\n\n\n", obj=project_no_init) + result = invoke( + ["init"], input="python\ntest-project\n\n\n\n\n\n", obj=project_no_init + ) + print(result.output) assert result.exit_code == 0 python_version = ".".join( map(str, get_python_version(project_no_init.environment.python_executable)[:2]) @@ -163,27 +172,3 @@ def test_init_command(project_no_init, invoke, mocker): "me@example.org", f">={python_version}", ) - - -def test_init_command_python_option(project_no_init, invoke, mocker): - mocker.patch( - "pdm.cli.commands.get_user_email_from_git", - return_value=("Testing", "me@example.org"), - ) - do_init = mocker.patch.object(actions, "do_init") - result = invoke( - ["init", "--python", sys.executable], - input="test-project\n\n\n\n\n\n", - obj=project_no_init, - ) - assert result.exit_code == 0 - python_version = ".".join(map(str, sys.version_info[:2])) - do_init.assert_called_with( - project_no_init, - "test-project", - "0.0.0", - "MIT", - "Testing", - "me@example.org", - f">={python_version}", - )