Skip to content

Commit 7ce040f

Browse files
authored
Run a command from a [pipx.run] entry point
1 parent be9edb3 commit 7ce040f

File tree

3 files changed

+55
-4
lines changed

3 files changed

+55
-4
lines changed

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
dev
22

3+
- Introduce the `pipx.run` entry point group as an alternative way to declare an application for `pipx run`.
34
- Fix cursor show/hide to work with older versions of Windows. (#610)
45
- Support text colors on Windows. (#612)
56
- Better platform unicode detection to avoid errors and allow showing emojis when possible. (#614)

src/pipx/commands/run.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def run(
9494
bin_path = venv.bin_path / app_filename
9595
_prepare_venv_cache(venv, bin_path, use_cache)
9696

97-
if bin_path.exists():
97+
if venv.has_app(app, app_filename):
9898
logger.info(f"Reusing cached venv {venv_dir}")
9999
venv.run_app(app, app_filename, app_args)
100100
else:
@@ -144,7 +144,7 @@ def _download_and_run(
144144
is_main_package=True,
145145
)
146146

147-
if not (venv.bin_path / app_filename).exists():
147+
if not venv.has_app(app, app_filename):
148148
apps = venv.pipx_metadata.main_package.apps
149149
raise PipxError(
150150
f"""

src/pipx/venv.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import json
22
import logging
3+
import re
34
import time
45
from pathlib import Path
56
from subprocess import CompletedProcess
6-
from typing import Dict, Generator, List, NoReturn, Set
7+
from typing import Dict, Generator, List, NoReturn, Optional, Set
8+
9+
try:
10+
from importlib.metadata import Distribution, EntryPoint
11+
except ImportError:
12+
from importlib_metadata import Distribution, EntryPoint # type: ignore
713

814
from packaging.utils import canonicalize_name
915

@@ -35,6 +41,15 @@
3541

3642
logger = logging.getLogger(__name__)
3743

44+
_entry_point_value_pattern = re.compile(
45+
r"""
46+
^(?P<module>[\w.]+)\s*
47+
(:\s*(?P<attr>[\w.]+))?\s*
48+
(?P<extras>\[.*\])?\s*$
49+
""",
50+
re.VERBOSE,
51+
)
52+
3853

3954
class VenvContainer:
4055
"""A collection of venvs managed by pipx."""
@@ -336,8 +351,43 @@ def list_installed_packages(self) -> Set[str]:
336351
pip_list = json.loads(cmd_run.stdout.strip())
337352
return set([x["name"] for x in pip_list])
338353

354+
def _find_entry_point(self, app: str) -> Optional[EntryPoint]:
355+
if not self.python_path.exists():
356+
return None
357+
dists = Distribution.discover(
358+
name=self.main_package_name,
359+
path=[str(get_site_packages(self.python_path))],
360+
)
361+
for dist in dists:
362+
for ep in dist.entry_points:
363+
if ep.group == "pipx.run" and ep.name == app:
364+
return ep
365+
return None
366+
339367
def run_app(self, app: str, filename: str, app_args: List[str]) -> NoReturn:
340-
exec_app([str(self.bin_path / filename)] + app_args)
368+
entry_point = self._find_entry_point(app)
369+
370+
# No [pipx.run] entry point; default to run console script.
371+
if entry_point is None:
372+
exec_app([str(self.bin_path / filename)] + app_args)
373+
374+
# Evaluate and execute the entry point.
375+
# TODO: After dropping support for Python < 3.9, use
376+
# "entry_point.module" and "entry_point.attr" instead.
377+
match = _entry_point_value_pattern.match(entry_point.value)
378+
assert match is not None, "invalid entry point"
379+
module, attr = match.group("module", "attr")
380+
code = (
381+
f"import sys, {module}\n"
382+
f"sys.argv[0] = {entry_point.name!r}\n"
383+
f"sys.exit({module}.{attr}())\n"
384+
)
385+
exec_app([str(self.python_path), "-c", code] + app_args)
386+
387+
def has_app(self, app: str, filename: str) -> bool:
388+
if self._find_entry_point(app) is not None:
389+
return True
390+
return (self.bin_path / filename).is_file()
341391

342392
def _upgrade_package_no_metadata(self, package: str, pip_args: List[str]) -> None:
343393
with animate(

0 commit comments

Comments
 (0)