Repository: pypa/pipx
Pull request #1209 (2024-01-15 18:13:51)

[pre-commit.ci] pre-commit autoupdate
<!--pre-commit.ci start-->
updates:
- [github.com/tox-dev/pyproject-fmt: 1.5.3 → 1.6.0](https://github.com/tox-dev/pyproject-fmt/compare/1.5.3...1.6.0)
- [github.com/astral-sh/ruff-pre-commit: v0.1.11 → v0.1.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.11...v0.1.13)
<!--pre-commit.ci end-->


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 521f92f..7cae495 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,11 +7,11 @@ repos:
   - id: trailing-whitespace
   - id: check-yaml
 - repo: https://github.com/tox-dev/pyproject-fmt
-  rev: "1.5.3"
+  rev: "1.6.0"
   hooks:
     - id: pyproject-fmt
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.1.11
+  rev: v0.1.13
   hooks:
   - id: ruff-format
   - id: ruff
diff --git a/changelog.d/1191.fix.md b/changelog.d/1191.fix.md
new file mode 100644
index 0000000..7ea5acd
--- /dev/null
+++ b/changelog.d/1191.fix.md
@@ -0,0 +1,1 @@
+Report correct filename in tracebacks with `pipx run <scriptname>`
diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py
index eff9af0..1e036de 100644
--- a/src/pipx/commands/run.py
+++ b/src/pipx/commands/run.py
@@ -8,7 +8,7 @@ import urllib.parse
 import urllib.request
 from pathlib import Path
 from shutil import which
-from typing import List, NoReturn, Optional
+from typing import List, NoReturn, Optional, Union
 
 from packaging.requirements import InvalidRequirement, Requirement
 
@@ -42,14 +42,14 @@ Available executable scripts:
     {app_lines}"""
 
 
-def maybe_script_content(app: str, is_path: bool) -> Optional[str]:
+def maybe_script_content(app: str, is_path: bool) -> Optional[Union[str, Path]]:
     # If the app is a script, return its content.
     # Return None if it should be treated as a package name.
 
     # Look for a local file first.
     app_path = Path(app)
     if app_path.is_file():
-        return app_path.read_text(encoding="utf-8")
+        return app_path
     elif is_path:
         raise PipxError(f"The specified path {app} does not exist")
 
@@ -71,7 +71,7 @@ def maybe_script_content(app: str, is_path: bool) -> Optional[str]:
 
 
 def run_script(
-    content: str,
+    content: Union[str, Path],
     app_args: List[str],
     python: str,
     pip_args: List[str],
@@ -81,7 +81,7 @@ def run_script(
 ) -> NoReturn:
     requirements = _get_requirements_from_script(content)
     if requirements is None:
-        exec_app([python, "-c", content, *app_args])
+        python_path = Path(python)
     else:
         # Note that the environment name is based on the identified
         # requirements, and *not* on the script name. This is deliberate, as
@@ -99,7 +99,12 @@ def run_script(
             venv = Venv(venv_dir, python=python, verbose=verbose)
             venv.create_venv(venv_args, pip_args)
             venv.install_unmanaged_packages(requirements, pip_args)
-        exec_app([venv.python_path, "-c", content, *app_args])
+        python_path = venv.python_path
+
+    if isinstance(content, Path):
+        exec_app([python_path, content, *app_args])
+    else:
+        exec_app([python_path, "-c", content, *app_args])
 
 
 def run_package(
@@ -323,11 +328,14 @@ def _http_get_request(url: str) -> str:
 INLINE_SCRIPT_METADATA = re.compile(r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$")
 
 
-def _get_requirements_from_script(content: str) -> Optional[List[str]]:
+def _get_requirements_from_script(content: Union[str, Path]) -> Optional[List[str]]:
     """
     Supports inline script metadata.
     """
 
+    if isinstance(content, Path):
+        content = content.read_text(encoding="utf-8")
+
     name = "script"
 
     # Windows is currently getting un-normalized line endings, so normalize


Test patch:
diff --git a/tests/test_run.py b/tests/test_run.py
index 0de3920..7d93556 100644
--- a/tests/test_run.py
+++ b/tests/test_run.py
@@ -243,6 +243,24 @@ def test_run_with_requirements_old(caplog, pipx_temp_env, tmp_path):
         run_pipx_cli_exit(["run", script.as_uri()])
 
 
+@mock.patch("os.execvpe", new=execvpe_mock)
+def test_run_correct_traceback(capfd, pipx_temp_env, tmp_path):
+    script = tmp_path / "test.py"
+    script.write_text(
+        textwrap.dedent(
+            """
+                raise RuntimeError("Should fail")
+            """
+        ).strip()
+    )
+
+    with pytest.raises(SystemExit):
+        run_pipx_cli(["run", str(script)])
+
+    captured = capfd.readouterr()
+    assert "test.py" in captured.err
+
+
 @mock.patch("os.execvpe", new=execvpe_mock)
 def test_run_with_args(caplog, pipx_temp_env, tmp_path):
     script = tmp_path / "test.py"