Repository: pypa/pipx
Pull request #1206 (2024-01-15 16:07:06)

ci: update to artifacts v2
This is #1199 but targeting `main` instead of #1172, which dependabot deleted the branch for.



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/changelog.d/1208.bugfix.md b/changelog.d/1208.bugfix.md
new file mode 100644
index 0000000..c71b1f1
--- /dev/null
+++ b/changelog.d/1208.bugfix.md
@@ -0,0 +1,1 @@
+Support building pipx wheels with setuptools-scm<7, such as on FreeBSD.
diff --git a/pyproject.toml b/pyproject.toml
index 23255bd..5c21a57 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -84,5 +84,5 @@ issue_format = "[#{issue}](https://github.com/pypa/pipx/issues/{issue})"
 package = "pipx"
 
 [[tool.mypy.overrides]]
-module = ["pipx.version", "pycowsay.*"]
+module = ["pycowsay.*"]
 ignore_missing_imports = true
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
diff --git a/src/pipx/main.py b/src/pipx/main.py
index aa7ff86..86fe38a 100644
--- a/src/pipx/main.py
+++ b/src/pipx/main.py
@@ -28,7 +28,7 @@ from pipx.emojis import hazard
 from pipx.interpreter import DEFAULT_PYTHON, InterpreterResolutionError, find_python_interpreter
 from pipx.util import PipxError, mkdir, pipx_wrap, rmdir
 from pipx.venv import VenvContainer
-from pipx.version import __version__
+from pipx.version import version as __version__
 
 logger = logging.getLogger(__name__)
 
diff --git a/src/pipx/version.pyi b/src/pipx/version.pyi
new file mode 100644
index 0000000..d05fd88
--- /dev/null
+++ b/src/pipx/version.pyi
@@ -0,0 +1,5 @@
+version: str
+version_tuple: tuple[int, int, int, str, str] | tuple[int, int, int]
+
+# Note that newer versions of setuptools_scm also add __version__, but we are
+# not forcing new versions of setuptools_scm, so only these imports are allowed.


Test patch:
diff --git a/.github/workflows/create_tests_package_lists.yml b/.github/workflows/create_tests_package_lists.yml
index 8e4edf3..74709b0 100644
--- a/.github/workflows/create_tests_package_lists.yml
+++ b/.github/workflows/create_tests_package_lists.yml
@@ -30,7 +30,7 @@ jobs:
         run:
           nox --non-interactive --session create_test_package_list-${{ matrix.python-version }} -- ./new_tests_packages
       - name: Store reports as artifacts
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
-          name: lists
+          name: lists-${{ matrix.os }}-${{ matrix.python-version }}
           path: ./new_tests_packages
diff --git a/.github/workflows/exhaustive_package_test.yml b/.github/workflows/exhaustive_package_test.yml
index 9fa850c..d768892 100644
--- a/.github/workflows/exhaustive_package_test.yml
+++ b/.github/workflows/exhaustive_package_test.yml
@@ -27,9 +27,9 @@ jobs:
         continue-on-error: true
         run: nox --non-interactive --session test_all_packages-${{ matrix.python-version }}
       - name: Store reports as artifacts
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
-          name: reports-raw
+          name: reports-raw-${{ matrix.os }}-${{ matrix.python-version }}
           path: reports
 
   report_all_packages:
@@ -39,9 +39,10 @@ jobs:
 
     steps:
       - name: Get report artifacts
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
-          name: reports-raw
+          pattern: reports-raw-*
+          merge-multiple: true
       - name: Collate reports
         run: |
           ls # DEBUG
@@ -57,7 +58,7 @@ jobs:
           cat all_packages_deps_errors_* > all_deps_errors_lf.txt
           tr -d '\r' < all_deps_errors_lf.txt > reports/all_deps_errors.txt
       - name: Store collated and raw reports as artifacts
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: reports-final
           path: reports
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index e17ddb7..acc0365 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -78,7 +78,7 @@ jobs:
         run: nox --error-on-missing-interpreters --non-interactive --session zipapp
       - name: Test zipapp by installing black
         run: python ./pipx.pyz install black
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         with:
           name: pipx.pyz
           path: pipx.pyz
@@ -115,7 +115,7 @@ jobs:
     needs: [tests, man, zipapp]
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/download-artifact@v3
+      - uses: actions/download-artifact@v4
         with:
           name: pipx.pyz
       - name: Upload to release
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"