From e63159e90c4b7958e39ad6c2910575c212bfe315 Mon Sep 17 00:00:00 2001 From: Tom Solberg Date: Thu, 16 Nov 2023 13:31:04 +0100 Subject: [PATCH] fix: add support for `{pdm}` placeholder in scripts (#2408) * add support for `{pdm}` placeholder in scripts * check should_interpolate * fix docs * add news item * Fix feedback and Windows --- docs/docs/usage/scripts.md | 25 +++++++++++++++++++++++++ news/2408.feature.md | 1 + src/pdm/cli/commands/run.py | 24 +++++++++++++++++++++++- tests/cli/test_run.py | 14 ++++++++++++++ 4 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 news/2408.feature.md diff --git a/docs/docs/usage/scripts.md b/docs/docs/usage/scripts.md index 8852abcbb5..b99efc4171 100644 --- a/docs/docs/usage/scripts.md +++ b/docs/docs/usage/scripts.md @@ -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 /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: diff --git a/news/2408.feature.md b/news/2408.feature.md new file mode 100644 index 0000000000..42d6aa510f --- /dev/null +++ b/news/2408.feature.md @@ -0,0 +1 @@ +Add support for {pdm} placeholder in script definitions to call the same PDM entrypoint diff --git a/src/pdm/cli/commands/run.py b/src/pdm/cli/commands/run.py index 96d22ef1c8..e727e9b1af 100644 --- a/src/pdm/cli/commands/run.py +++ b/src/pdm/cli/commands/run.py @@ -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 @@ -44,9 +45,10 @@ def exec_opts(*options: TaskOptions | None) -> dict[str, Any]: RE_ARGS_PLACEHOLDER = re.compile(r"{args(?::(?P[^}]*))?}") +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 @@ -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 @@ -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: diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 4ce9765a49..2ac983fd7f 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -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"] = {