Skip to content

Commit

Permalink
fix: add support for {pdm} placeholder in scripts (pdm-project#2408)
Browse files Browse the repository at this point in the history
* add support for `{pdm}` placeholder in scripts

* check should_interpolate

* fix docs

* add news item

* Fix feedback and Windows
  • Loading branch information
tgolsson authored and frostming committed Dec 1, 2023
1 parent 2eef607 commit 5b259c0
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 1 deletion.
25 changes: 25 additions & 0 deletions docs/docs/usage/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,31 @@ $ pdm run test
`call` scripts don't support the `{args}` placeholder as they have
access to `sys.argv` directly to handle such complexe cases and more.

### `{pdm}` placeholder

Sometimes you may have multiple PDM installations, or `pdm` installed with a different name. This
could for example occur in a CI/CD situation, or when working with different PDM versions in
different repos. To make your scripts more robust you can use `{pdm}` to use the PDM entrypoint
executing the script. This will expand to `{sys.executable} -m pdm`.

```toml
[tool.pdm.scripts]
whoami = { shell = "echo `{pdm} -V` was called as '{pdm} -V'" }
```
will produce the following output:
```shell
$ pdm whoami
PDM, version 0.1.dev2501+g73651b7.d20231115 was called as /usr/bin/python3 -m pdm -V

$ pdm2.8 whoami
PDM, version 2.8.0 was called as <snip>/venvs/pdm2-8/bin/python -m pdm -V
```

!!! note
While the above example uses PDM 2.8, this functionality was introduced in the 2.10 series and only backported for the showcase.



## Show the List of Scripts

Use `pdm run --list/-l` to show the list of available script shortcuts:
Expand Down
1 change: 1 addition & 0 deletions news/2408.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for {pdm} placeholder in script definitions to call the same PDM entrypoint
24 changes: 23 additions & 1 deletion src/pdm/cli/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import signal
import subprocess
import sys
from pathlib import Path
from types import FrameType
from typing import TYPE_CHECKING, Mapping, NamedTuple, Sequence, cast

Expand Down Expand Up @@ -44,9 +45,10 @@ def exec_opts(*options: TaskOptions | None) -> dict[str, Any]:


RE_ARGS_PLACEHOLDER = re.compile(r"{args(?::(?P<default>[^}]*))?}")
RE_PDM_PLACEHOLDER = re.compile(r"{pdm}")


def interpolate(script: str, args: Sequence[str]) -> tuple[str, bool]:
def _interpolate_args(script: str, args: Sequence[str]) -> tuple[str, bool]:
"""Interpolate the `{args:[defaults]} placeholder in a string"""
import shlex

Expand All @@ -58,6 +60,25 @@ def replace(m: re.Match[str]) -> str:
return interpolated, count > 0


def _interpolate_pdm(script: str) -> str:
"""Interpolate the `{pdm} placeholder in a string"""
executable_path = Path(sys.executable)

def replace(m: re.Match[str]) -> str:
return sh_join([executable_path.as_posix(), "-m", "pdm"])

interpolated = RE_PDM_PLACEHOLDER.sub(replace, script)
return interpolated


def interpolate(script: str, args: Sequence[str]) -> tuple[str, bool]:
"""Interpolate the `{args:[defaults]} placeholder in a string"""

script, args_interpolated = _interpolate_args(script, args)
script = _interpolate_pdm(script)
return script, args_interpolated


class Task(NamedTuple):
kind: str
name: str
Expand Down Expand Up @@ -251,6 +272,7 @@ def run_task(self, task: Task, args: Sequence[str] = (), opts: TaskOptions | Non
if kind == "composite":
args = list(args)
should_interpolate = any(RE_ARGS_PLACEHOLDER.search(script) for script in value)
should_interpolate = should_interpolate or any(RE_PDM_PLACEHOLDER.search(script) for script in value)
code = 0
for script in value:
if should_interpolate:
Expand Down
14 changes: 14 additions & 0 deletions tests/cli/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,20 @@ def test_run_script_with_args_placeholder_with_default(project, pdm, capfd, scri
assert out.strip().splitlines()[1:] == expected


def test_run_shell_script_with_pdm_placeholder(project, pdm):
project.pyproject.settings["scripts"] = {
"test_script": {
"shell": "{pdm} -V > output.txt",
"help": "test it won't fail",
}
}
project.pyproject.write()
with cd(project.root):
result = pdm(["run", "test_script"], obj=project)
assert result.exit_code == 0
assert (project.root / "output.txt").read_text().strip().startswith("PDM, version")


def test_run_expand_env_vars(project, pdm, capfd, monkeypatch):
(project.root / "test_script.py").write_text("import os; print(os.getenv('FOO'))")
project.pyproject.settings["scripts"] = {
Expand Down

0 comments on commit 5b259c0

Please sign in to comment.