Skip to content

Commit 5ede3ca

Browse files
committed
feat: add --with option to "pipx run" command
Closes #1607
1 parent 651c9e7 commit 5ede3ca

File tree

4 files changed

+54
-8
lines changed

4 files changed

+54
-8
lines changed

changelog.d/1607.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `--with` flag to `pipx run` to allow injecting dependencies

src/pipx/commands/run.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
import urllib.request
99
from pathlib import Path
1010
from shutil import which
11-
from typing import List, NoReturn, Optional, Union
11+
from typing import List, NoReturn, Optional, Tuple, Union
1212

1313
from packaging.requirements import InvalidRequirement, Requirement
1414

1515
from pipx import paths
1616
from pipx.commands.common import package_name_from_spec
17+
from pipx.commands.inject import inject_dep
1718
from pipx.constants import TEMP_VENV_EXPIRATION_THRESHOLD_DAYS, WINDOWS
1819
from pipx.emojis import hazard
1920
from pipx.util import (
@@ -111,6 +112,7 @@ def run_script(
111112
def run_package(
112113
app: str,
113114
package_or_url: str,
115+
dependencies: List[str],
114116
app_args: List[str],
115117
python: str,
116118
pip_args: List[str],
@@ -157,26 +159,38 @@ def run_package(
157159

158160
if venv.has_app(app, app_filename):
159161
logger.info(f"Reusing cached venv {venv_dir}")
160-
venv.run_app(app, app_filename, app_args)
161162
else:
162163
logger.info(f"venv location is {venv_dir}")
163-
_download_and_run(
164+
venv, app, app_filename = _prepare_venv(
164165
Path(venv_dir),
165166
package_or_url,
166167
app,
167168
app_filename,
168-
app_args,
169169
python,
170170
pip_args,
171171
venv_args,
172172
use_cache,
173173
verbose,
174174
)
175175

176+
for dep in dependencies:
177+
inject_dep(
178+
venv_dir=venv_dir,
179+
package_name=None,
180+
package_spec=dep,
181+
pip_args=pip_args,
182+
verbose=verbose,
183+
include_apps=False,
184+
include_dependencies=False,
185+
force=False,
186+
)
187+
venv.run_app(app, app_filename, app_args)
188+
176189

177190
def run(
178191
app: str,
179192
spec: str,
193+
dependencies: List[str],
180194
is_path: bool,
181195
app_args: List[str],
182196
python: str,
@@ -206,6 +220,7 @@ def run(
206220
run_package(
207221
package_name,
208222
package_or_url,
223+
dependencies,
209224
app_args,
210225
python,
211226
pip_args,
@@ -216,18 +231,17 @@ def run(
216231
)
217232

218233

219-
def _download_and_run(
234+
def _prepare_venv(
220235
venv_dir: Path,
221236
package_or_url: str,
222237
app: str,
223238
app_filename: str,
224-
app_args: List[str],
225239
python: str,
226240
pip_args: List[str],
227241
venv_args: List[str],
228242
use_cache: bool,
229243
verbose: bool,
230-
) -> NoReturn:
244+
) -> Tuple[Venv, str, str]:
231245
venv = Venv(venv_dir, python=python, verbose=verbose)
232246
venv.check_upgrade_shared_libs(pip_args=pip_args, verbose=verbose)
233247

@@ -275,7 +289,7 @@ def _download_and_run(
275289
# Let future _remove_all_expired_venvs know to remove this
276290
(venv_dir / VENV_EXPIRED_FILENAME).touch()
277291

278-
venv.run_app(app, app_filename, app_args)
292+
return venv, app, app_filename
279293

280294

281295
def _get_temporary_venv_path(requirements: List[str], python: str, pip_args: List[str], venv_args: List[str]) -> Path:

src/pipx/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar
267267
commands.run(
268268
args.app_with_args[0],
269269
args.spec,
270+
args.with_,
270271
args.path,
271272
args.app_with_args[1:],
272273
args.python,
@@ -845,6 +846,13 @@ def _add_run(subparsers: argparse._SubParsersAction, shared_parser: argparse.Arg
845846
action="store_true",
846847
help="Require app to be run from local __pypackages__ directory",
847848
)
849+
p.add_argument(
850+
"--with",
851+
dest="with_",
852+
action="append",
853+
default=[],
854+
help="Extra dependencies to add to the temporary environment",
855+
)
848856
p.add_argument("--spec", help=SPEC_HELP)
849857
add_python_options(p)
850858
add_pip_venv_args(p)

tests/test_run.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,3 +443,26 @@ def test_run_local_path_entry_point(pipx_temp_env, caplog, root):
443443
run_pipx_cli_exit(["run", empty_project_path])
444444

445445
assert "Using discovered entry point for 'pipx run'" in caplog.text
446+
447+
448+
@mock.patch("os.execvpe", new=execvpe_mock)
449+
def test_run_with(capsys):
450+
run_pipx_cli_exit(["run", "--with", "black", "pycowsay", "--help"])
451+
captured = capsys.readouterr()
452+
assert "injected package black into venv pycowsay" in captured.out
453+
454+
455+
@mock.patch("os.execvpe", new=execvpe_mock)
456+
def test_run_with_cache(capsys, caplog):
457+
# Maybe there's a better way to remove the previous venv cache?
458+
run_pipx_cli_exit(["run", "--no-cache", "pycowsay", "cowsay", "args"])
459+
run_pipx_cli_exit(["run", "pycowsay", "cowsay", "args"])
460+
caplog.set_level(logging.DEBUG)
461+
caplog.clear()
462+
run_pipx_cli_exit(["run", "--verbose", "pycowsay", "cowsay", "args"], assert_exit=0)
463+
assert "Reusing cached venv" in caplog.text
464+
465+
run_pipx_cli_exit(["run", "--verbose", "--with", "black", "pycowsay", "args"], assert_exit=0)
466+
captured = capsys.readouterr()
467+
assert "Reusing cached venv" in caplog.text
468+
assert "injected package black into venv pycowsay" in captured.out

0 commit comments

Comments
 (0)