Repository: pdm-project/pdm

Issue:
Allow PDM init to receive a Python version
## Is your feature request related to a problem? Please describe.

Yes. It is impossible to create a new project for a specific python version without interaction.
If I use `pdm ini --non-interactive` then I cannot change version using CLI, because it is not compatible with `requires-python` setting that was automatically selected.

## Describe the solution you'd like

A solution would be to allow the init command to receive the version of python that I want to use:

```bash
pdm init 3.8.10
```

Or allow the user to force change it later:

```bash
pdm use 3.8.10 --force
```


Discussion:

Patch:
diff --git a/news/1412.feature.md b/news/1412.feature.md
new file mode 100644
index 00000000..dccc1444
--- /dev/null
+++ b/news/1412.feature.md
@@ -0,0 +1,1 @@
+Allow `pdm init` to receive a Python path or version via `--python` option.
diff --git a/news/1418.bugfix.md b/news/1418.bugfix.md
new file mode 100644
index 00000000..46f6daca
--- /dev/null
+++ b/news/1418.bugfix.md
@@ -0,0 +1,1 @@
+Show the Python path, instead of the real executable, in the Python selection menu.
diff --git a/src/pdm/cli/actions.py b/src/pdm/cli/actions.py
index ebd39cfd..8931e5ec 100644
--- a/src/pdm/cli/actions.py
+++ b/src/pdm/cli/actions.py
@@ -658,7 +658,7 @@ def do_use(
         if not matching_interpreters:
             project.core.ui.echo("Interpreters found but not matching:", err=True)
             for py in found_interpreters:
-                project.core.ui.echo(f"  - {py.executable} ({py.identifier})", err=True)
+                project.core.ui.echo(f"  - {py.path} ({py.identifier})", err=True)
             raise NoPythonVersion(
                 "No python is found meeting the requirement "
                 f"[green]python {str(project.python_requires)}[/]"
@@ -669,7 +669,7 @@ def do_use(
             project.core.ui.echo("Please enter the Python interpreter to use")
             for i, py_version in enumerate(matching_interpreters):
                 project.core.ui.echo(
-                    f"{i}. [green]{str(py_version.executable)}[/] "
+                    f"{i}. [green]{str(py_version.path)}[/] "
                     f"({py_version.identifier})"
                 )
             selection = termui.ask(
@@ -684,7 +684,7 @@ def do_use(
             use_cache.set(python, selected_python.path.as_posix())
 
     if not selected_python.valid:
-        path = str(selected_python.executable)
+        path = str(selected_python.path)
         raise InvalidPyVersion(f"Invalid Python interpreter: {path}")
     if not save:
         return selected_python
@@ -695,13 +695,13 @@ def do_use(
     )
     project.core.ui.echo(
         "Using Python interpreter: "
-        f"[green]{str(selected_python.executable)}[/] "
+        f"[green]{str(selected_python.path)}[/] "
         f"({selected_python.identifier})"
     )
     project.python = selected_python
     if (
         old_python
-        and old_python.path != selected_python.path
+        and old_python.executable != selected_python.executable
         and not project.environment.is_global
     ):
         project.core.ui.echo("Updating executable scripts...", style="cyan")
diff --git a/src/pdm/cli/commands/init.py b/src/pdm/cli/commands/init.py
index ab4304b3..aa42e1de 100644
--- a/src/pdm/cli/commands/init.py
+++ b/src/pdm/cli/commands/init.py
@@ -33,6 +33,7 @@ class Command(BaseCommand):
             action="store_true",
             help="Don't ask questions but use default values",
         )
+        parser.add_argument("--python", help="Specify the Python version/path to use")
         parser.set_defaults(search_parent=False)
 
     def handle(self, project: Project, options: argparse.Namespace) -> None:
@@ -48,7 +49,13 @@ class Command(BaseCommand):
         self.set_interactive(not options.non_interactive)
 
         if self.interactive:
-            python = actions.do_use(project, ignore_requires_python=True, hooks=hooks)
+            python = actions.do_use(
+                project,
+                options.python or "",
+                first=bool(options.python),
+                ignore_requires_python=True,
+                hooks=hooks,
+            )
             if (
                 project.config["python.use_venv"]
                 and get_venv_like_prefix(python.executable) is None
@@ -72,7 +79,12 @@ class Command(BaseCommand):
                         )
         else:
             python = actions.do_use(
-                project, "3", True, ignore_requires_python=True, save=False, hooks=hooks
+                project,
+                options.python or "3",
+                True,
+                ignore_requires_python=True,
+                save=False,
+                hooks=hooks,
             )
         if get_venv_like_prefix(python.executable) is None:
             project.core.ui.echo(


Test patch:
diff --git a/tests/cli/test_init.py b/tests/cli/test_init.py
index 4de793b5..dbfce5ce 100644
--- a/tests/cli/test_init.py
+++ b/tests/cli/test_init.py
@@ -6,6 +6,8 @@ import pytest
 from pdm.cli import actions
 from pdm.models.python import PythonInfo
 
+PYTHON_VERSION = f"{sys.version_info[0]}.{sys.version_info[1]}"
+
 
 def test_init_validate_python_requires(project_no_init):
     with pytest.raises(ValueError):
@@ -103,6 +105,21 @@ def test_init_auto_create_venv(project_no_init, invoke, mocker):
     )
 
 
+def test_init_auto_create_venv_specify_python(project_no_init, invoke, mocker):
+    mocker.patch("pdm.cli.commands.init.get_venv_like_prefix", return_value=None)
+    project_no_init.project_config["python.use_venv"] = True
+    result = invoke(
+        ["init", f"--python={PYTHON_VERSION}"],
+        input="\n\n\n\n\n\n",
+        obj=project_no_init,
+    )
+    assert result.exit_code == 0
+    assert (
+        project_no_init.python.executable.parent.parent
+        == project_no_init.root / ".venv"
+    )
+
+
 def test_init_auto_create_venv_answer_no(project_no_init, invoke, mocker):
     mocker.patch("pdm.cli.commands.init.get_venv_like_prefix", return_value=None)
     creator = mocker.patch("pdm.cli.commands.venv.backends.Backend.create")