Repository: pdm-project/pdm

Issue:
An option to specify constant command arguments via pyproject.toml or pdm.toml
## Is your feature request related to a problem? Please describe.

In some cases installing current package in editable mode is not desired. It is tedious to specify `--no-editable` each time you run `pdm add`/`pdm install`. In my case the problem persists on any platform/os so i also need to not forget to tell team members that they need to specify this option too. I'd like to see a way to specify constant (i.e. those that are applied to every run) command options.

## Describe the solution you'd like

Allow specifying constant options to pass via pyproject.toml or pdm.toml.

I suggest adding a table like this:
```toml
[tool.pdm.args]
add = ["--prerelease"]
install = ["--no-editable"]
```


Discussion:
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/pdm-project/pdm/pull/1959?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) Report
Patch coverage: **`100.00`**% and project coverage change: **`+0.03`** :tada:
> Comparison is base [(`c8099a9`)](https://app.codecov.io/gh/pdm-project/pdm/commit/c8099a94b37ae85f9b5f2f1c3e809bff5dd3693b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) 85.26% compared to head [(`731a271`)](https://app.codecov.io/gh/pdm-project/pdm/pull/1959?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) 85.30%.

:exclamation: Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please [install the Github App Integration](https://github.com/apps/codecov) for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?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    #1959      +/-   ##
==========================================
+ Coverage   85.26%   85.30%   +0.03%     
==========================================
  Files          99       99              
  Lines        9253     9262       +9     
  Branches     2018     2021       +3     
==========================================
+ Hits         7890     7901      +11     
+ Misses        926      924       -2     
  Partials      437      437              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `85.11% <100.00%> (+0.03%)` | :arrow_up: |

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.

| [Impacted Files](https://app.codecov.io/gh/pdm-project/pdm/pull/1959?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | Coverage Δ | |
|---|---|---|
| [src/pdm/core.py](https://app.codecov.io/gh/pdm-project/pdm/pull/1959?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9jb3JlLnB5) | `92.76% <100.00%> (+0.45%)` | :arrow_up: |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/pdm-project/pdm/pull/1959/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pdm-project/pdm/pull/1959?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](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).


Patch:
diff --git a/docs/docs/usage/config.md b/docs/docs/usage/config.md
index 1af90f79..1991d818 100644
--- a/docs/docs/usage/config.md
+++ b/docs/docs/usage/config.md
@@ -264,3 +264,19 @@ In this example, PDM will resolve the above packages into the given versions no
 !!! warning
     By using `[tool.pdm.resolution.overrides]` setting, you are at your own risk of any incompatibilities from that resolution. It can only be used if there is no valid resolution for your requirements and you know the specific version works.
     Most of the time, you can just add any transient constraints to the `dependencies` array.
+
+## Passing constant arguments to every pdm invocation
+
+_New in version 2.7.0_
+
+You can add extra options passed to individual pdm commands by `tool.pdm.options` configuration:
+
+```toml
+[tool.pdm.options]
+add = ["--no-isolation", "--no-self"]
+install = ["--no-self"]
+lock = ["--no-cross-platform"]
+```
+
+These options will be added right after the command name. For instance, based on the configuration above,
+`pdm add requests` is equivalent to `pdm add --no-isolation --no-self requests`.
diff --git a/news/1923.feature.md b/news/1923.feature.md
new file mode 100644
index 00000000..1cf6bfd0
--- /dev/null
+++ b/news/1923.feature.md
@@ -0,0 +1,1 @@
+Add a configuration to specify constant command arguments for every pdm invocation.
diff --git a/src/pdm/core.py b/src/pdm/core.py
index 0478753b..33e558de 100644
--- a/src/pdm/core.py
+++ b/src/pdm/core.py
@@ -156,6 +156,17 @@ class Core:
             self.parser.error("No command given")
         command.handle(project, options)
 
+    def _get_cli_args(self, args: list[str], obj: Project | None) -> list[str]:
+        project = self.create_project(is_global=False) if obj is None else obj
+        if project.is_global:
+            return args
+        config = project.pyproject.settings.get("options", {})
+        (pos, command) = next(((i, arg) for i, arg in enumerate(args) if not arg.startswith("-")), (-1, None))
+        if command and command in config:
+            # add args after the command
+            args[pos + 1 : pos + 1] = list(config[command])
+        return args
+
     def main(
         self,
         args: list[str] | None = None,
@@ -166,7 +177,7 @@ class Core:
         """The main entry function"""
 
         # Ensure same behavior while testing and using the CLI
-        args = args or sys.argv[1:]
+        args = self._get_cli_args(args or sys.argv[1:], obj)
         # Keep it for after project parsing to check if its a defined script
         root_script = None
         try:


Test patch:
diff --git a/tests/test_project.py b/tests/test_project.py
index 2833916f..65604467 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -314,3 +314,19 @@ def test_configured_source_overwriting(project):
     assert custom_source.verify_ssl is False
     assert custom_source.username == "foo"
     assert custom_source.password == "bar"
+
+
+def test_invoke_pdm_adding_configured_args(project, pdm, mocker):
+    project.pyproject.settings["options"] = {
+        "install": ["--no-self", "--no-editable"],
+        "add": ["--no-isolation"],
+        "lock": ["--no-cross-platform"],
+    }
+    project.pyproject.write()
+    parser = mocker.patch("argparse.ArgumentParser.parse_args")
+    pdm(["add", "requests"], obj=project)
+    parser.assert_called_with(["add", "--no-isolation", "requests"])
+    pdm(["install", "--check"], obj=project)
+    parser.assert_called_with(["install", "--no-self", "--no-editable", "--check"])
+    pdm(["lock", "--lockfile", "pdm.2.lock"], obj=project)
+    parser.assert_called_with(["lock", "--no-cross-platform", "--lockfile", "pdm.2.lock"])