Skip to content

Commit ad36b1c

Browse files
authored
feat: support requirements specifications in pipx run (#783)
* feat: support requirements specifications in pipx run * docs: update examples with version pinning/extras
1 parent a59383f commit ad36b1c

File tree

5 files changed

+21
-7
lines changed

5 files changed

+21
-7
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,14 @@ Any arguments after the application name will be passed directly to the applicat
219219

220220
Re-running the same app is quick because pipx caches Virtual Environments on a per-app basis. The caches only last a few days, and when they expire, pipx will again use the latest version of the package. This way you can be sure you're always running a new version of the package without having to manually upgrade.
221221

222-
If the app name does not match that package name, you can use the `--spec` argument:
222+
If the app name does not match that package name, you can use the `--spec` argument to specify the package to install and app to run separately:
223223
```
224224
pipx run --spec PACKAGE APP
225225
```
226226

227-
You can also use the `--spec` argument to run a specific version, or use any other `pip`-specifier:
227+
You can also specify specific versions, version ranges, or extras:
228228
```
229-
pipx run --spec PACKAGE==1.0.0 APP
229+
pipx run APP==1.0.0
230230
```
231231

232232
### Running from Source Control

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+
- Support `pipx run` with version constraints and extras. (#697)
34

45
0.16.5
56

docs/examples.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ pipx run pycowsay moo
2626
pipx --version # prints pipx version
2727
pipx run pycowsay --version # prints pycowsay version
2828
pipx run --python pythonX pycowsay
29-
pipx run --spec pycowsay==2.0 pycowsay --version
29+
pipx run pycowsay==2.0 --version
30+
pipx run pycowsay[dev] --version
3031
pipx run --spec git+https://github.com/psf/black.git black
3132
pipx run --spec git+https://github.com/psf/black.git@branch-name black
3233
pipx run --spec git+https://github.com/psf/black.git@git-hash black

src/pipx/main.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from typing import Any, Callable, Dict, List
1717

1818
import argcomplete # type: ignore
19+
from packaging.requirements import InvalidRequirement, Requirement
1920
from packaging.utils import canonicalize_name
2021

2122
import pipx.constants
@@ -175,9 +176,17 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901
175176
if ("spec" in args and args.spec is not None)
176177
else args.app_with_args[0]
177178
)
179+
# For any package, we need to just use the name
180+
try:
181+
package_name = Requirement(args.app_with_args[0]).name
182+
except InvalidRequirement:
183+
# Raw URLs to scripts are supported, too, so continue if
184+
# we can't parse this as a package
185+
package_name = args.app_with_args[0]
186+
178187
use_cache = not args.no_cache
179188
commands.run(
180-
args.app_with_args[0],
189+
package_name,
181190
package_or_url,
182191
args.app_with_args[1:],
183192
args.python,

tests/test_run.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@ def run_pipx_cli_exit(pipx_cmd_list, assert_exit=None):
4242
assert sys_exit.value.code == assert_exit
4343

4444

45+
@pytest.mark.parametrize(
46+
"package_name", ["pycowsay", "pycowsay==0.0.0.1", "pycowsay>=0.0.0.1"]
47+
)
4548
@mock.patch("os.execvpe", new=execvpe_mock)
46-
def test_simple_run(pipx_temp_env, monkeypatch, capsys):
47-
run_pipx_cli_exit(["run", "pycowsay", "--help"])
49+
def test_simple_run(pipx_temp_env, monkeypatch, capsys, package_name):
50+
run_pipx_cli_exit(["run", package_name, "--help"])
4851
captured = capsys.readouterr()
4952
assert "Download the latest version of a package" not in captured.out
5053

0 commit comments

Comments
 (0)