From 97af31c90a1f94c8b4eb4cd487b37e67aa603758 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 4 Sep 2022 16:20:59 -0400 Subject: [PATCH] Allow users to pass a string of extra arguments to pip (#5283) * Allow users to pass a string of extra arguments to pip install --- docs/advanced.rst | 16 +++++++++++++++- news/5283.feature.rst | 2 ++ pipenv/cli/command.py | 3 +++ pipenv/cli/options.py | 20 ++++++++++++++++++++ pipenv/core.py | 20 ++++++++++++++++++++ tests/integration/test_install_basic.py | 15 ++++++++++++++- 6 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 news/5283.feature.rst diff --git a/docs/advanced.rst b/docs/advanced.rst index 4d76774270..0f4e301058 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -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. @@ -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 pip feature for 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``. +Example usage:: + + pipenv sync --extra-pip-args="--use-feature=truststore --proxy=127.0.0.1" + + ☤ Specifying Basically Anything ------------------------------- diff --git a/news/5283.feature.rst b/news/5283.feature.rst new file mode 100644 index 0000000000..78c4aba636 --- /dev/null +++ b/news/5283.feature.rst @@ -0,0 +1,2 @@ +It is now possible to supply additional arguments to ``pip`` install by supplying ``--extra-pip-args=" "`` +See the updated documentation ``Supplying additional arguments to pip`` for more details. diff --git a/pipenv/cli/command.py b/pipenv/cli/command.py index 68d11c939a..242de26bce 100644 --- a/pipenv/cli/command.py +++ b/pipenv/cli/command.py @@ -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, ) @@ -581,6 +582,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, ) @@ -672,6 +674,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() diff --git a/pipenv/cli/options.py b/pipenv/cli/options.py index bd2196486b..ce2f23e655 100644 --- a/pipenv/cli/options.py +++ b/pipenv/cli/options.py @@ -87,6 +87,7 @@ def __init__(self): self.deploy = False self.packages = [] self.editables = [] + self.extra_pip_args = [] class LockOptions: @@ -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) @@ -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 diff --git a/pipenv/core.py b/pipenv/core.py index 4fadd90ce1..fdfc546f6b 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -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, @@ -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: @@ -792,6 +794,7 @@ def do_install_dependencies( concurrent=True, requirements_dir=None, pypi_mirror=None, + extra_pip_args=None, ): """ Executes the installation functionality. @@ -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( @@ -1228,6 +1232,7 @@ def do_init( keep_outdated=False, requirements_dir=None, pypi_mirror=None, + extra_pip_args=None, ): """Executes the init functionality.""" @@ -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. @@ -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"], @@ -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)) @@ -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: @@ -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: @@ -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 = [] @@ -1662,6 +1674,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, @@ -2058,6 +2071,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-" @@ -2213,6 +2227,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 @@ -2233,6 +2248,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( @@ -2279,6 +2295,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( @@ -2381,6 +2398,7 @@ def do_install( deploy=deploy, pypi_mirror=pypi_mirror, skip_lock=skip_lock, + extra_pip_args=extra_pip_args, ) sys.exit(0) @@ -3076,6 +3094,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: @@ -3110,6 +3129,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")) diff --git a/tests/integration/test_install_basic.py b/tests/integration/test_install_basic.py index a0e8767056..daa75fd9ac 100644 --- a/tests/integration/test_install_basic.py +++ b/tests/integration/test_install_basic.py @@ -1,4 +1,5 @@ import os +import mock from pathlib import Path from tempfile import TemporaryDirectory @@ -534,7 +535,6 @@ def test_install_dev_use_default_constraints(PipenvInstance): assert c.returncode != 0 -@pytest.mark.dev @pytest.mark.basic @pytest.mark.install @pytest.mark.needs_internet @@ -547,6 +547,19 @@ def test_install_does_not_exclude_packaging(PipenvInstance): assert c.returncode == 0 +@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 dataclasses-json --extra-pip-args=""--use-feature=truststore --proxy=test""") + assert c.returncode == 1 + assert "truststore feature" in c.stderr + + +@pytest.mark.basic +@pytest.mark.install +@pytest.mark.needs_internet def test_install_tarball_is_actually_installed(PipenvInstance): """ Test case for Issue 5326""" with PipenvInstance(chdir=True) as p: