diff --git a/.gitignore b/.gitignore index 4a831aff4abccff1dd698c010fc54e37aec42780..bbab10afd089cebbb727394650ef8fd4a145a096 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ dmypy.json caches/ .idea/ __pypackages__ +.pdm.toml diff --git a/pdm/cli/actions.py b/pdm/cli/actions.py index 05ab8dc290c12cb29b0e177a9bc20918f0298d87..4f88e44f21a46baf369c39874b5ad656b38479aa 100644 --- a/pdm/cli/actions.py +++ b/pdm/cli/actions.py @@ -1,4 +1,5 @@ import itertools +import os import shutil from typing import Dict, Iterable, Optional, Sequence @@ -6,10 +7,11 @@ from pkg_resources import safe_name import click import halo +import pythonfinder import tomlkit from pdm.builders import SdistBuilder, WheelBuilder from pdm.context import context -from pdm.exceptions import ProjectError +from pdm.exceptions import NoPythonVersion, ProjectError from pdm.installers import Synchronizer, format_dist from pdm.models.candidates import Candidate, identify from pdm.models.requirements import parse_requirement, strip_extras @@ -17,6 +19,7 @@ from pdm.models.specifiers import bump_version, get_specifier from pdm.project import Project from pdm.resolver import BaseProvider, EagerUpdateProvider, ReusePinProvider, resolve from pdm.resolver.reporters import SpinnerReporter +from pdm.utils import get_python_version def format_lockfile(mapping, fetched_dependencies, summary_collection): @@ -386,3 +389,33 @@ def do_init( project._pyproject.setdefault("tool", {})["pdm"] = data["tool"]["pdm"] project._pyproject["build-system"] = data["build-system"] project.write_pyproject() + + +def do_use(project: Project, python: str) -> None: + """Use the specified python version and save in project config. + The python can be a version string or interpreter path. + """ + if os.path.isabs(python): + python_path = python + 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.") + + python_version = ".".join(map(str, get_python_version(python_path))) + if not project.python_requires.contains(python_version): + raise NoPythonVersion( + "The target Python version {} doesn't satisfy " + "the Python requirement: {}".format(python_version, project.python_requires) + ) + context.io.echo( + "Using Python interpreter: {} ({})".format( + context.io.green(python_path), python_version + ) + ) + project.config["python"] = python_path + project.config.save_config() diff --git a/pdm/cli/commands.py b/pdm/cli/commands.py index 88189849c6aa728e7dfd70a2f86d1c9ea700db97..3c81db7658e7728bcf187f8689959fb101629e4b 100644 --- a/pdm/cli/commands.py +++ b/pdm/cli/commands.py @@ -264,3 +264,11 @@ def init(project): author = click.prompt(f"Author name", default=git_user) email = click.prompt(f"Author email", default=git_email) actions.do_init(project, name, version, license, author, email) + + +@cli.command() +@click.argument("python") +@pass_project +def use(project, python): + """Use the given python version as base interpreter.""" + actions.do_use(project, python) diff --git a/pdm/models/environment.py b/pdm/models/environment.py index 33b73bc6b116d4d585cae065a6a04249e3310c70..e1d381ad1694297059ade770cfafee972c8cd909 100644 --- a/pdm/models/environment.py +++ b/pdm/models/environment.py @@ -84,6 +84,7 @@ class Environment: @cached_property def python_executable(self) -> str: """Get the Python interpreter path.""" + path = None if self.config["python"]: path = self.config["python"] try: @@ -96,11 +97,23 @@ class Environment: for python in finder.find_all_python_versions(): version = ".".join(map(str, get_python_version(python.path.as_posix()))) if self.python_requires.contains(version): - return python.path.as_posix() + path = python.path.as_posix() if self.python_requires.contains(".".join(map(str, sys.version_info[:3]))): - return sys.executable + path = sys.executable + if path: + python_version = ".".join(map(str, get_python_version(path))) + context.io.echo( + "Using Python interpreter: {} ({})".format( + context.io.green(path), python_version + ) + ) + self.config["python"] = path + self.config.save_config() + return path raise NoPythonVersion( - "No python matching {} is found on the system.".format(self.python_requires) + "No Python that satisfies {} is found on the system.".format( + self.python_requires + ) ) def get_paths(self) -> Dict[str, str]: diff --git a/pdm/project/config.py b/pdm/project/config.py index e465b139923a960518497fa52c1deeb813dee746..093952ee0db52326ecba0d30ab54292ef7eb361b 100644 --- a/pdm/project/config.py +++ b/pdm/project/config.py @@ -56,7 +56,7 @@ class Config(MutableMapping): temp = temp.setdefault(part, {}) temp[last] = value - with file_path.open(encoding="utf-8") as fp: + with file_path.open("w", encoding="utf-8") as fp: fp.write(tomlkit.dumps(toml_data)) self._dirty.clear() diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 9b82592c63513559420ada8ff61073485ee05302..9f07b9015292b5098ca7c9354763ea046386363c 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -1,5 +1,6 @@ import functools import os +import shutil import pytest from click.testing import CliRunner @@ -91,3 +92,19 @@ def test_uncaught_error(invoke, mocker): result = invoke(["list", "-v"]) assert isinstance(result.exception, RuntimeError) + + +def test_use_command(project, invoke): + python_path = shutil.which("python") + result = invoke(["use", "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) + assert result.exit_code == 0 + + project.tool_settings["python_requires"] = ">=3.6" + project.write_pyproject() + result = invoke(["use", "2.7"], obj=project) + assert result.exit_code == 1