Repository: pdm-project/pdm
Pull request #3462 (2025-04-11 00:21:42)

feat: New command: `pdm new`
Signed-off-by: Frost Ming <me@frostming.com>

## Pull Request Checklist

- [ ] A news fragment is added in `news/` describing what is new.
- [ ] Test cases added for changed code.

## Describe what you have changed in this PR.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pdm-project/pdm/pull/3462?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 85.56%. Comparing base [(`89058b3`)](https://app.codecov.io/gh/pdm-project/pdm/commit/89058b37ccdc5bdb8c55bc3f37224d3aa8111541?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) to head [(`e407e7b`)](https://app.codecov.io/gh/pdm-project/pdm/commit/e407e7b9c5d571f092d50d9e09630cc2a7c32aeb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3462      +/-   ##
==========================================
- Coverage   85.58%   85.56%   -0.02%     
==========================================
  Files         112      113       +1     
  Lines       11565    11578      +13     
  Branches     2540     2540              
==========================================
+ Hits         9898     9907       +9     
- Misses       1136     1139       +3     
- Partials      531      532       +1     
```

| [Flag](https://app.codecov.io/gh/pdm-project/pdm/pull/3462/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/pdm-project/pdm/pull/3462/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | `85.37% <100.00%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pdm-project/pdm/pull/3462?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/README.md b/README.md
index f2d1b092..9da9483f 100644
--- a/README.md
+++ b/README.md
@@ -159,10 +159,10 @@ asdf install pdm latest
 
 ## Quickstart
 
-**Initialize a new PDM project**
+**Create a new PDM project**
 
 ```bash
-pdm init
+pdm new my-project
 ```
 
 Answer the questions following the guide, and a PDM project with a `pyproject.toml` file will be ready to use.
diff --git a/README_zh.md b/README_zh.md
index 9ee589c6..69552922 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -152,7 +152,7 @@ asdf install pdm latest
 **初始化一个新的 PDM 项目**
 
 ```bash
-pdm init
+pdm new my_project
 ```
 
 按照指引回答提示的问题，一个 PDM 项目和对应的`pyproject.toml`文件就创建好了。
diff --git a/docs/usage/project.md b/docs/usage/project.md
index 7bae9af0..538f14c4 100644
--- a/docs/usage/project.md
+++ b/docs/usage/project.md
@@ -1,14 +1,22 @@
 # New Project
 
-To start with, create a new project with [`pdm init`](../reference/cli.md#init):
+To start with, create a new project with [`pdm new`](../reference/cli.md#new):
 
 ```bash
-mkdir my-project && cd my-project
-pdm init
+pdm new my-project
 ```
 
 You will need to answer a few questions, to help PDM to create a `pyproject.toml` file for you.
-For more usages of `pdm init`, please read [Create your project from a template](./template.md).
+For more usages of `pdm new`, please read [Create your project from a template](./template.md).
+
+## Create pyproject.toml for an existing project
+
+If you already have a project and want to create a `pyproject.toml` file for it, you can use [`pdm init`](../reference/cli.md#init):
+
+```bash
+cd my-project
+pdm init
+```
 
 ## Choose a Python interpreter
 
diff --git a/docs/usage/template.md b/docs/usage/template.md
index 0cbf8b89..9e51a732 100644
--- a/docs/usage/template.md
+++ b/docs/usage/template.md
@@ -1,17 +1,17 @@
 # Create Project From a Template
 
 Similar to `yarn create` and `npm create`, PDM also supports initializing or creating a project from a template.
-The template is given as a positional argument of `pdm init`, in one of the following forms:
+The template is given as a positional argument of `pdm new`, in one of the following forms:
 
-- `pdm init django` - Initialize the project from the template `https://github.com/pdm-project/template-django`
-- `pdm init https://github.com/frostming/pdm-template-django` - Initialize the project from a Git URL. Both HTTPS and SSH URL are acceptable.
-- `pdm init django@v2` - To check out the specific branch or tag. Full Git URL also supports it.
-- `pdm init /path/to/template` - Initialize the project from a template directory on local filesystem.
-- `pdm init minimal` - Initialize with the builtin "minimal" template, that only generates a `pyproject.toml`.
+- `pdm new django my-project` - Create a new project `my-project` from the template `https://github.com/pdm-project/template-django`
+- `pdm new https://github.com/frostming/pdm-template-django my-project` - Initialize the project from a Git URL. Both HTTPS and SSH URL are acceptable.
+- `pdm new django@v2 my-project` - To check out the specific branch or tag. Full Git URL also supports it.
+- `pdm new /path/to/template my-project` - Initialize the project from a template directory on local filesystem.
+- `pdm new minimal my-project` - Initialize with the builtin "minimal" template, that only generates a `pyproject.toml`.
 
-And `pdm init` will use the default template built in.
+And `pdm new my-project` will use the default template built in and create a project at the given path.
 
-The project will be initialized at the current directory, existing files with the same name will be overwritten. You can also use the `-p <path>` option to create a project at a new path.
+`pdm init` command also supports the same template argument. The project will be initialized at the current directory, existing files with the same name will be overwritten.
 
 ## Contribute a template
 
diff --git a/news/3423.bug.md b/news/3423.bugfix.md
similarity index 100%
rename from news/3423.bug.md
rename to news/3423.bugfix.md
diff --git a/news/3462.feature.md b/news/3462.feature.md
new file mode 100644
index 00000000..eff271c1
--- /dev/null
+++ b/news/3462.feature.md
@@ -0,0 +1,1 @@
+New command `pdm new` that behaves like `pdm init` but creates a new project.
diff --git a/src/pdm/cli/commands/new.py b/src/pdm/cli/commands/new.py
new file mode 100644
index 00000000..7f049137
--- /dev/null
+++ b/src/pdm/cli/commands/new.py
@@ -0,0 +1,22 @@
+import argparse
+import os
+
+from pdm.cli.commands.base import verbose_option
+from pdm.cli.commands.init import Command as InitCommand
+from pdm.project.core import Project
+
+
+class Command(InitCommand):
+    """Create a new Python project at <project_path>"""
+
+    arguments = (verbose_option,)
+
+    def add_arguments(self, parser: argparse.ArgumentParser) -> None:
+        super().add_arguments(parser)
+        parser.add_argument("project_path", help="The path to create the new project")
+
+    def handle(self, project: Project, options: argparse.Namespace) -> None:
+        new_project = project.core.create_project(
+            options.project_path, global_config=options.config or os.getenv("PDM_CONFIG_FILE")
+        )
+        return super().handle(new_project, options)


Test patch:
diff --git a/tests/cli/test_init.py b/tests/cli/test_init.py
index 0f55bf78..4067484b 100644
--- a/tests/cli/test_init.py
+++ b/tests/cli/test_init.py
@@ -5,6 +5,7 @@ import pytest
 
 from pdm.compat import tomllib
 from pdm.models.python import PythonInfo
+from pdm.utils import cd
 
 PYTHON_VERSION = f"{sys.version_info[0]}.{sys.version_info[1]}"
 
@@ -48,6 +49,32 @@ def test_init_command(project_no_init, pdm, mocker):
         assert tomllib.load(fp) == data
 
 
+def test_new_command(project_no_init, pdm, mocker):
+    mocker.patch(
+        "pdm.cli.commands.init.get_user_email_from_git",
+        return_value=("Testing", "me@example.org"),
+    )
+    with cd(project_no_init.root):
+        pdm(["new", "myproject"], input="\ntest-project\n\n\n\n\n\n\n", strict=True)
+    python_version = f"{project_no_init.python.major}.{project_no_init.python.minor}"
+    data = {
+        "project": {
+            "authors": [{"email": "me@example.org", "name": "Testing"}],
+            "dependencies": [],
+            "description": "Default template for PDM package",
+            "license": {"text": "MIT"},
+            "name": "test-project",
+            "requires-python": f"=={python_version}.*",
+            "readme": "README.md",
+            "version": "0.1.0",
+        },
+        "tool": {"pdm": {"distribution": False}},
+    }
+
+    with open(project_no_init.root.joinpath("myproject/pyproject.toml"), "rb") as fp:
+        assert tomllib.load(fp) == data
+
+
 def test_init_command_library(project_no_init, pdm, mocker):
     mocker.patch(
         "pdm.cli.commands.init.get_user_email_from_git",