Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to pass a string of extra arguments to pip #5283

Merged
merged 11 commits into from
Sep 4, 2022
16 changes: 15 additions & 1 deletion docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Keep in mind that environment variables are expanded in runtime, leaving the ent
☤ Injecting credentials through keychain support
------------------------------------------------

Private regirstries on Google Cloud, Azure and AWS support dynamic credentials using
Private registries on Google Cloud, Azure and AWS support dynamic credentials using
the keychain implementation. Due to the way the keychain is structured, it might ask
the user for input. Asking the user for input is disabled. This will disable the keychain
support completely, unfortunately.
Expand Down Expand Up @@ -153,6 +153,20 @@ input. Otherwise the process will hang forever!::

Above example will install ``flask`` and a private package ``private-test-package`` from GCP.

☤ Supplying additional arguments to pip
------------------------------------------------

There may be cases where you wish to supply additional arguments to pip to be used during the install phase.
For example, you may want to enable the new pip experimental feature for using
oz123 marked this conversation as resolved.
Show resolved Hide resolved
`system certificate stores <https://pip.pypa.io/en/latest/topics/https-certificates/#using-system-certificate-stores>`_

In this case you can supply these additional arguments to ``pipenv sync`` or ``pipenv install`` by passing additional
argument ``--extra-pip-args="--use-feature=truststore"``. It is possible to supply multiple arguments in the ``--extra-pip-args``,
for example::

pipenv sync --extra-pip-args="--use-feature=truststore --proxy=127.0.0.1"


☤ Specifying Basically Anything
-------------------------------

Expand Down
2 changes: 2 additions & 0 deletions news/5283.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
It is now possible to supply additional arguments to ``pip`` install by supplying ``--extra-pip-args="<arg1> <arg2>"``
See the updated documentation ``Supplying additional arguments to pip`` for more details.
3 changes: 3 additions & 0 deletions pipenv/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ def install(state, **kwargs):
packages=state.installstate.packages,
editable_packages=state.installstate.editables,
site_packages=state.site_packages,
extra_pip_args=state.installstate.extra_pip_args,
)


Expand Down Expand Up @@ -561,6 +562,7 @@ def update(ctx, state, bare=False, dry_run=None, outdated=False, **kwargs):
unused=False,
sequential=state.installstate.sequential,
pypi_mirror=state.pypi_mirror,
extra_pip_args=state.installstate.extra_pip_args,
)


Expand Down Expand Up @@ -652,6 +654,7 @@ def sync(ctx, state, bare=False, user=False, unused=False, **kwargs):
sequential=state.installstate.sequential,
pypi_mirror=state.pypi_mirror,
system=state.system,
extra_pip_args=state.installstate.extra_pip_args,
)
if retcode:
ctx.abort()
Expand Down
20 changes: 20 additions & 0 deletions pipenv/cli/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def __init__(self):
self.deploy = False
self.packages = []
self.editables = []
self.extra_pip_args = []


class LockOptions:
Expand Down Expand Up @@ -286,6 +287,24 @@ def callback(ctx, param, value):
)(f)


def extra_pip_args(f):
def callback(ctx, param, value):
state = ctx.ensure_object(State)
if value:
for opt in value.split(" "):
state.installstate.extra_pip_args.append(opt)
return value

return option(
"--extra-pip-args",
nargs=1,
required=False,
callback=callback,
expose_value=True,
type=click_types.STRING,
)(f)


def three_option(f):
def callback(ctx, param, value):
state = ctx.ensure_object(State)
Expand Down Expand Up @@ -570,6 +589,7 @@ def install_options(f):
f = ignore_pipfile_option(f)
f = editable_option(f)
f = package_arg(f)
f = extra_pip_args(f)
return f


Expand Down
20 changes: 20 additions & 0 deletions pipenv/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ def batch_install(
pypi_mirror=None,
retry=True,
sequential_deps=None,
extra_pip_args=None,
):
from .vendor.requirementslib.models.utils import (
strip_extras_markers_from_requirement,
Expand Down Expand Up @@ -774,6 +775,7 @@ def batch_install(
trusted_hosts=trusted_hosts,
use_pep517=use_pep517,
use_constraint=False, # no need to use constraints, it's written in lockfile
extra_pip_args=extra_pip_args,
)

for c in cmds:
Expand All @@ -792,6 +794,7 @@ def do_install_dependencies(
concurrent=True,
requirements_dir=None,
pypi_mirror=None,
extra_pip_args=None,
):
"""
Executes the installation functionality.
Expand Down Expand Up @@ -841,6 +844,7 @@ def do_install_dependencies(
"allow_global": allow_global,
"pypi_mirror": pypi_mirror,
"sequential_deps": editable_or_vcs_deps,
"extra_pip_args": extra_pip_args,
}

batch_install(
Expand Down Expand Up @@ -1228,6 +1232,7 @@ def do_init(
keep_outdated=False,
requirements_dir=None,
pypi_mirror=None,
extra_pip_args=None,
):
"""Executes the init functionality."""

Expand Down Expand Up @@ -1327,6 +1332,7 @@ def do_init(
concurrent=concurrent,
requirements_dir=requirements_dir,
pypi_mirror=pypi_mirror,
extra_pip_args=extra_pip_args,
)

# Hint the user what to do to activate the virtualenv.
Expand All @@ -1352,6 +1358,7 @@ def get_pip_args(
no_deps: bool = False,
selective_upgrade: bool = False,
src_dir: Optional[str] = None,
extra_pip_args: Optional[List] = None,
) -> List[str]:
arg_map = {
"pre": ["--pre"],
Expand All @@ -1373,6 +1380,8 @@ def get_pip_args(
arg_set.extend(arg_map.get(key))
elif key == "selective_upgrade" and not locals().get(key):
arg_set.append("--exists-action=i")
for extra_pip_arg in extra_pip_args:
arg_set.append(extra_pip_arg)
return list(dict.fromkeys(arg_set))


Expand Down Expand Up @@ -1445,6 +1454,7 @@ def pip_install(
trusted_hosts=None,
use_pep517=True,
use_constraint=False,
extra_pip_args: Optional[List] = None,
):
piplogger = logging.getLogger("pipenv.patched.pip._internal.commands.install")
if not trusted_hosts:
Expand Down Expand Up @@ -1521,6 +1531,7 @@ def pip_install(
no_use_pep517=not use_pep517,
no_deps=no_deps,
require_hashes=not ignore_hashes,
extra_pip_args=extra_pip_args,
)
pip_command.extend(pip_args)
if r:
Expand Down Expand Up @@ -1574,6 +1585,7 @@ def pip_install_deps(
trusted_hosts=None,
use_pep517=True,
use_constraint=False,
extra_pip_args: Optional[List] = None,
):
if not trusted_hosts:
trusted_hosts = []
Expand Down Expand Up @@ -1647,6 +1659,7 @@ def pip_install_deps(
selective_upgrade=selective_upgrade,
no_use_pep517=not use_pep517,
no_deps=no_deps,
extra_pip_args=extra_pip_args,
)
sources = get_source_list(
project,
Expand Down Expand Up @@ -2043,6 +2056,7 @@ def do_install(
keep_outdated=False,
selective_upgrade=False,
site_packages=None,
extra_pip_args=None,
):
requirements_directory = vistir.path.create_tracked_tempdir(
suffix="-requirements", prefix="pipenv-"
Expand Down Expand Up @@ -2198,6 +2212,7 @@ def do_install(
requirements_dir=requirements_directory,
pypi_mirror=pypi_mirror,
keep_outdated=keep_outdated,
extra_pip_args=extra_pip_args,
)

# This is for if the user passed in dependencies, then we want to make sure we
Expand All @@ -2218,6 +2233,7 @@ def do_install(
deploy=deploy,
pypi_mirror=pypi_mirror,
skip_lock=skip_lock,
extra_pip_args=extra_pip_args,
)
for pkg_line in pkg_list:
click.secho(
Expand Down Expand Up @@ -2264,6 +2280,7 @@ def do_install(
index=index_url,
pypi_mirror=pypi_mirror,
use_constraint=True,
extra_pip_args=extra_pip_args,
)
if c.returncode:
sp.write_err(
Expand Down Expand Up @@ -2366,6 +2383,7 @@ def do_install(
deploy=deploy,
pypi_mirror=pypi_mirror,
skip_lock=skip_lock,
extra_pip_args=extra_pip_args,
)
sys.exit(0)

Expand Down Expand Up @@ -3041,6 +3059,7 @@ def do_sync(
pypi_mirror=None,
system=False,
deploy=False,
extra_pip_args=None,
):
# The lock file needs to exist because sync won't write to it.
if not project.lockfile_exists:
Expand Down Expand Up @@ -3075,6 +3094,7 @@ def do_sync(
pypi_mirror=pypi_mirror,
deploy=deploy,
system=system,
extra_pip_args=extra_pip_args,
)
if not bare:
click.echo(click.style("All dependencies are now up-to-date!", fg="green"))
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/test_install_basic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import mock

from pathlib import Path
from tempfile import TemporaryDirectory
Expand Down Expand Up @@ -545,3 +546,14 @@ def test_install_does_not_exclude_packaging(PipenvInstance):
assert c.returncode == 0
c = p.pipenv("run python -c 'from dataclasses_json import DataClassJsonMixin'")
assert c.returncode == 0


@pytest.mark.dev
@pytest.mark.basic
@pytest.mark.install
@pytest.mark.needs_internet
def test_install_will_supply_extra_pip_args(PipenvInstance):
with PipenvInstance(chdir=True) as p:
c = p.pipenv("""install requests --extra-pip-args=""--use-feature=truststore --proxy=test""")
assert c.returncode == 1
assert "truststore feature" in c.stderr