Repository: pdm-project/pdm

Issue:
Composite script with multiple invocations triggers recursive script error
- [x] I have searched the issue tracker and believe that this is not a duplicate.

**Make sure you run commands with `-v` flag before pasting the output.**

## Steps to reproduce

- Script snippet for `pyproject.toml`:
  ```
  [tool.pdm.scripts]
  foo.composite = ["bar hello", "bar world"]
  bar.composite = ["baz {args}"]
  baz.shell = "echo {args}"
  ```
- Run the "foo" script:
  `pdm run -v foo`

## Actual behavior

```
Running <task foo>: []
Running <task bar>: ['hello']
Running <task baz>: echo hello
hello
[PdmUsageError]: Script bar is recursive.
```
Exit code: 1

## Expected behavior

```
Running <task foo>: []
Running <task bar>: ['hello']
Running <task baz>: echo hello
hello
Running <task bar>: ['world']
Running <task baz>: echo world
world
```
Exit code: 0

## Environment Information

```bash
PDM version:
  2.14.0
Python Interpreter:
  /redacted/.venv/bin/python (3.12)
Project Root:
  /redacted
Local Packages:
  
{
  "implementation_name": "cpython",
  "implementation_version": "3.12.0",
  "os_name": "posix",
  "platform_machine": "x86_64",
  "platform_release": "5.15.133.1-microsoft-standard-WSL2",
  "platform_system": "Linux",
  "platform_version": "#1 SMP Thu Oct 5 21:02:42 UTC 2023",
  "python_full_version": "3.12.0",
  "platform_python_implementation": "CPython",
  "python_version": "3.12",
  "sys_platform": "linux"
}
```



Discussion:
IMO PDM shouldn't prevent running scripts if they call themselves recursively. The stop condition could be based on the file-system (presence/absence of files). If they reach the recursion limit of Python, I'd call this a user error.

Also yes, the logic for detecting recursion is currently flawed.
# Workaround
When invoking a nested composite script, prepend the command with `{pdm} run`.

Example:
```
[tool.pdm.scripts]
foo.composite = ["{pdm} run bar hello", "{pdm} run bar world"]
bar.composite = ["baz {args}"]
baz.shell = "echo {args}"
```



Patch:
diff --git a/news/2829.bugfix.md b/news/2829.bugfix.md
new file mode 100644
index 0000000000..1e9ab9d285
--- /dev/null
+++ b/news/2829.bugfix.md
@@ -0,0 +1,1 @@
+Fix recursive script detection on multiple invocations.
diff --git a/src/pdm/cli/commands/run.py b/src/pdm/cli/commands/run.py
index 4a3681253a..69f64f2452 100644
--- a/src/pdm/cli/commands/run.py
+++ b/src/pdm/cli/commands/run.py
@@ -308,6 +308,7 @@ def run(
             if task.kind == "composite":
                 if command in seen:
                     raise PdmUsageError(f"Script {command} is recursive.")
+                seen = seen.copy()
                 seen.add(command)
 
             self.hooks.try_emit("pre_script", script=command, args=args)


Test patch:
diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py
index 1326cd135e..e97e5c0f55 100644
--- a/tests/cli/test_run.py
+++ b/tests/cli/test_run.py
@@ -670,7 +670,8 @@ def test_composite_fails_on_recursive_script(project, pdm):
         "first": {"composite": ["first"]},
         "second": {"composite": ["third"]},
         "third": {"composite": ["second"]},
-        "forth": {"composite": ["python -V", "python -V"]},
+        "fourth": {"composite": ["python -V", "python -V"]},
+        "fifth": {"composite": ["fourth", "fourth"]},
     }
     project.pyproject.write()
     result = pdm(["run", "first"], obj=project)
@@ -681,7 +682,10 @@ def test_composite_fails_on_recursive_script(project, pdm):
     assert result.exit_code == 1
     assert "Script second is recursive" in result.stderr
 
-    result = pdm(["run", "forth"], obj=project)
+    result = pdm(["run", "fourth"], obj=project)
+    assert result.exit_code == 0
+
+    result = pdm(["run", "fifth"], obj=project)
     assert result.exit_code == 0