Repository: pypa/pipx
Pull request #1180 (2023-12-31 18:04:59)

fix: use modern PEP 723 syntax, fix mistake in name
<!-- add an 'x' in the brackets below -->

- [ ] I have added an entry to `docs/changelog.md`

## Summary of changes

PEP 723 has been updated to use a `scripts` block and no `run` section. This updates to the new syntax, ~~but still runs with the old syntax, producing a warning.~~ This can be upgraded to an error (but with a nice message) later (or now, if you prefer, as it's still undocumented).

Edit: the old form used the wrong name (requirements instead of dependencies) so I went ahead and made it an error now. I can still do the soft deprecation instead though if that’s preferred.

## Test plan

<!-- provide evidence of testing, preferably with command(s) that can be copy+pasted by others -->

Tested by running

```
nox -s tests-3.11
```

There were 18 failures before this PR, and 18 after, so hoped that was good! :)


dukecat0: Please add a changelog entry.
flying-sheep: @henryiii would it be possible to make the tracebacks friendlier? Currently they show

```pytb
Traceback (most recent call last):
  File "<string>", line 11, in <module>
ModuleNotFoundError: No module named 'requests'
```

Would be nice if that read “File "actual-file-name.py", line 11, in __main__`
henryiii: > would it be possible to make the tracebacks friendlier?

I can look into that.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a34c371..bbc91a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
 ## dev
 
 - Allow skipping maintenance tasks during list command
- 
+
 ## 1.4.1
 
 - Set default logging level to WARNING, so debug log messages won't be shown without passing additional flags such as `--verbose`
@@ -15,6 +15,7 @@
 - Add `--quiet` and `--verbose` options for the `pipx` subcommands
 - [docs] Add Scoop installation instructions
 - Add ability to install multiple packages at once
+- Update `pipx run` on scripts using `/// script` and no `run` table following PEP 723 (#1180)
 
 ## 1.3.3
 
diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py
index cdb95f4..3138e14 100644
--- a/src/pipx/commands/run.py
+++ b/src/pipx/commands/run.py
@@ -328,7 +328,7 @@ def _get_requirements_from_script(content: str) -> Optional[List[str]]:
     Supports inline script metadata.
     """
 
-    name = "pyproject"
+    name = "script"
 
     # Windows is currently getting un-normalized line endings, so normalize
     content = content.replace("\r\n", "\n")
@@ -336,6 +336,20 @@ def _get_requirements_from_script(content: str) -> Optional[List[str]]:
     matches = [m for m in INLINE_SCRIPT_METADATA.finditer(content) if m.group("type") == name]
 
     if not matches:
+        pyproject_matches = [m for m in INLINE_SCRIPT_METADATA.finditer(content) if m.group("type") == "pyproject"]
+        if pyproject_matches:
+            logger.error(
+                pipx_wrap(
+                    f"""
+                    {hazard}  Using old form of requirements table. Use updated PEP
+                    723 syntax by replacing `# /// pyproject` with `# /// script`
+                    and `run.dependencies` (or `run.requirements`) with
+                    `dependencies`.
+                    """,
+                    subsequent_indent=" " * 4,
+                )
+            )
+            raise ValueError("Old 'pyproject' table found")
         return None
 
     if len(matches) > 1:
@@ -348,7 +362,7 @@ def _get_requirements_from_script(content: str) -> Optional[List[str]]:
     pyproject = tomllib.loads(content)
 
     requirements = []
-    for requirement in pyproject.get("run", {}).get("requirements", []):
+    for requirement in pyproject.get("dependencies", []):
         # Validate the requirement
         try:
             req = Requirement(requirement)


Test patch:
diff --git a/tests/test_run.py b/tests/test_run.py
index 934ecce..4d11010 100644
--- a/tests/test_run.py
+++ b/tests/test_run.py
@@ -198,8 +198,8 @@ def test_run_with_requirements(caplog, pipx_temp_env, tmp_path):
     script.write_text(
         textwrap.dedent(
             f"""
-                # /// pyproject
-                # run.requirements = ["requests==2.31.0"]
+                # /// script
+                # dependencies = ["requests==2.31.0"]
                 # ///
 
                 # Check requests can be imported
@@ -217,6 +217,32 @@ def test_run_with_requirements(caplog, pipx_temp_env, tmp_path):
     assert out.read_text() == "2.31.0"
 
 
+@mock.patch("os.execvpe", new=execvpe_mock)
+def test_run_with_requirements_old(caplog, pipx_temp_env, tmp_path):
+    script = tmp_path / "test.py"
+    out = tmp_path / "output.txt"
+    script.write_text(
+        textwrap.dedent(
+            f"""
+                # /// pyproject
+                # run.requirements = ["requests==2.31.0"]
+                # ///
+
+                # Check requests can be imported
+                import requests
+                # Check dependencies of requests can be imported
+                import certifi
+                # Check the installed version
+                from pathlib import Path
+                Path({repr(str(out))}).write_text(requests.__version__)
+            """
+        ).strip(),
+        encoding="utf-8",
+    )
+    with pytest.raises(ValueError):
+        run_pipx_cli_exit(["run", script.as_uri()])
+
+
 @mock.patch("os.execvpe", new=execvpe_mock)
 def test_run_with_args(caplog, pipx_temp_env, tmp_path):
     script = tmp_path / "test.py"
@@ -241,8 +267,8 @@ def test_run_with_requirements_and_args(caplog, pipx_temp_env, tmp_path):
     script.write_text(
         textwrap.dedent(
             f"""
-                # /// pyproject
-                # run.requirements = ["packaging"]
+                # /// script
+                # dependencies = ["packaging"]
                 # ///
                 import packaging
                 import sys
@@ -261,8 +287,8 @@ def test_run_with_invalid_requirement(capsys, pipx_temp_env, tmp_path):
     script.write_text(
         textwrap.dedent(
             """
-                # /// pyproject
-                # run.requirements = ["this is an invalid requirement"]
+                # /// script
+                # dependencies = ["this is an invalid requirement"]
                 # ///
                 print()
             """