Repository: pypa/pipx
Pull request #1422 (2024-05-21 11:36:37)

Fix `pipx run` entry point discovery with local path
<!-- add an 'x' in the brackets below -->

- [x] I have added a news fragment under `changelog.d/` (if the patch affects the end users)

## Summary of changes

Resolves https://github.com/pypa/pipx/pull/615#discussion_r1606569053.

## Test plan

<!-- provide evidence of testing, preferably with command(s) that can be copy+pasted by others -->

Tested by running

```
pipx run -vv testdata/empty_project/
```



Patch:
diff --git a/changelog.d/1422.bugfix.md b/changelog.d/1422.bugfix.md
new file mode 100644
index 0000000..ee01906
--- /dev/null
+++ b/changelog.d/1422.bugfix.md
@@ -0,0 +1,1 @@
+Fix discovery of a `pipx run` entry point if a local path was given as package.
diff --git a/src/pipx/venv.py b/src/pipx/venv.py
index 2549204..bc7939f 100644
--- a/src/pipx/venv.py
+++ b/src/pipx/venv.py
@@ -400,8 +400,13 @@ class Venv:
         dists = Distribution.discover(name=self.main_package_name, path=[str(get_site_packages(self.python_path))])
         for dist in dists:
             for ep in dist.entry_points:
-                if ep.group == "pipx.run" and ep.name == app:
-                    return ep
+                if ep.group == "pipx.run":
+                    if ep.name == app:
+                        return ep
+                    # Try to infer app name from dist's metadata if given
+                    # local path
+                    if Path(app).exists() and dist.metadata["Name"] == ep.name:
+                        return ep
         return None
 
     def run_app(self, app: str, filename: str, app_args: List[str]) -> NoReturn:
@@ -416,6 +421,7 @@ class Venv:
         # "entry_point.module" and "entry_point.attr" instead.
         match = _entry_point_value_pattern.match(entry_point.value)
         assert match is not None, "invalid entry point"
+        logger.info("Using discovered entry point for 'pipx run'")
         module, attr = match.group("module", "attr")
         code = f"import sys, {module}\nsys.argv[0] = {entry_point.name!r}\nsys.exit({module}.{attr}())\n"
         exec_app([str(self.python_path), "-c", code] + app_args)


Test patch:
diff --git a/testdata/empty_project/pyproject.toml b/testdata/empty_project/pyproject.toml
index 7ce466a..9b59eaa 100644
--- a/testdata/empty_project/pyproject.toml
+++ b/testdata/empty_project/pyproject.toml
@@ -1,7 +1,8 @@
 [build-system]
+build-backend = "setuptools.build_meta"
+
 requires = [
   "setuptools",
-  "wheel",
 ]
 
 [project]
@@ -21,3 +22,4 @@ classifiers = [
   "Programming Language :: Python :: 3.12",
 ]
 scripts.empty-project = "empty_project.main:cli"
+entry-points."pipx.run".empty-project = "empty_project.main:cli"
diff --git a/tests/test_run.py b/tests/test_run.py
index f084f0d..752253c 100644
--- a/tests/test_run.py
+++ b/tests/test_run.py
@@ -3,6 +3,7 @@ import os
 import subprocess
 import sys
 import textwrap
+from pathlib import Path
 from unittest import mock
 
 import pytest  # type: ignore[import-not-found]
@@ -401,3 +402,15 @@ def test_run_shared_lib_as_app(pipx_temp_env, monkeypatch, capfd):
     run_pipx_cli_exit(["run", "pip", "--help"])
     captured = capfd.readouterr()
     assert "pip <command> [options]" in captured.out
+
+
+@mock.patch("os.execvpe", new=execvpe_mock)
+def test_run_local_path_entry_point(pipx_temp_env, caplog, root):
+    empty_project_path = (Path("testdata") / "empty_project").as_posix()
+    os.chdir(root)
+
+    caplog.set_level(logging.INFO)
+
+    run_pipx_cli_exit(["run", empty_project_path])
+
+    assert "Using discovered entry point for 'pipx run'" in caplog.text