Repository: pypa/pipenv
Issue: --pypi-mirror not applied during virtualenv creation

When starting a new project using pipenv with --pypi-mirror specified, virtualenv uses pip without respecting the --pypi-mirror setting. 

If PyPI is inaccessible during this process, virtualenv falls back to using local copies of necessary packages and pipenv silently proceeds. However, if connections to PyPI time out (e.g. if packets are dropped instead of rejected by a firewall), pip (and virtualenv) will hang, and pipenv will timeout. 

Changed files:
pipenv/core.py, pipenv/cli.py

diff --git a/pipenv/cli.py b/pipenv/cli.py
index 5e7b470b..bb41c103 100644
--- a/pipenv/cli.py
+++ b/pipenv/cli.py
@@ -145,6 +145,13 @@ def validate_pypi_mirror(ctx, param, value):
     default=False,
     help="Enable site-packages for the virtualenv.",
 )
+@option(
+    '--pypi-mirror',
+    default=environments.PIPENV_PYPI_MIRROR,
+    nargs=1,
+    callback=validate_pypi_mirror,
+    help="Specify a PyPI mirror.",
+)
 @version_option(
     prog_name=crayons.normal('pipenv', bold=True), version=__version__
 )
@@ -163,6 +170,7 @@ def cli(
     envs=False,
     man=False,
     completion=False,
+    pypi_mirror=None,
 ):
     if completion:  # Handle this ASAP to make shell startup fast.
         from . import shells
@@ -272,7 +280,7 @@ def cli(
     # --two / --three was passed...
     if (python or three is not None) or site_packages:
         ensure_project(
-            three=three, python=python, warn=True, site_packages=site_packages
+            three=three, python=python, warn=True, site_packages=site_packages, pypi_mirror=pypi_mirror
         )
     # Check this again before exiting for empty ``pipenv`` command.
     elif ctx.invoked_subcommand is None:
@@ -571,7 +579,7 @@ def lock(
     from .core import ensure_project, do_init, do_lock
 
     # Ensure that virtualenv is available.
-    ensure_project(three=three, python=python)
+    ensure_project(three=three, python=python, pypi_mirror=pypi_mirror)
     if requirements:
         do_init(dev=dev, requirements=requirements, pypi_mirror=pypi_mirror)
     do_lock(
@@ -608,9 +616,16 @@ def lock(
     default=False,
     help="Always spawn a subshell, even if one is already spawned.",
 )
+@option(
+    '--pypi-mirror',
+    default=environments.PIPENV_PYPI_MIRROR,
+    nargs=1,
+    callback=validate_pypi_mirror,
+    help="Specify a PyPI mirror.",
+)
 @argument('shell_args', nargs=-1)
 def shell(
-    three=None, python=False, fancy=False, shell_args=None, anyway=False
+    three=None, python=False, fancy=False, shell_args=None, anyway=False, pypi_mirror=None
 ):
     from .core import load_dot_env, do_shell
     # Prevent user from activating nested environments.
@@ -635,7 +650,7 @@ def shell(
     if os.name == 'nt':
         fancy = True
     do_shell(
-        three=three, python=python, fancy=fancy, shell_args=shell_args
+        three=three, python=python, fancy=fancy, shell_args=shell_args, pypi_mirror=pypi_mirror
     )
 
 
@@ -663,9 +678,16 @@ def shell(
     callback=validate_python_path,
     help="Specify which version of Python virtualenv should use.",
 )
-def run(command, args, three=None, python=False):
+@option(
+    '--pypi-mirror',
+    default=environments.PIPENV_PYPI_MIRROR,
+    nargs=1,
+    callback=validate_pypi_mirror,
+    help="Specify a PyPI mirror.",
+)
+def run(command, args, three=None, python=False, pypi_mirror=None):
     from .core import do_run
-    do_run(command=command, args=args, three=three, python=python)
+    do_run(command=command, args=args, three=three, python=python, pypi_mirror=pypi_mirror)
 
 
 @command(
@@ -700,6 +722,13 @@ def run(command, args, three=None, python=False):
     multiple=True,
     help="Ignore specified vulnerability during safety checks."
 )
+@option(
+    '--pypi-mirror',
+    default=environments.PIPENV_PYPI_MIRROR,
+    nargs=1,
+    callback=validate_pypi_mirror,
+    help="Specify a PyPI mirror.",
+)
 @argument('args', nargs=-1)
 def check(
     three=None,
@@ -709,6 +738,7 @@ def check(
     style=False,
     ignore=None,
     args=None,
+    pypi_mirror=None,
 ):
     from .core import do_check
     do_check(
@@ -717,7 +747,8 @@ def check(
         system=system,
         unused=unused,
         ignore=ignore,
-        args=args
+        args=args,
+        pypi_mirror=pypi_mirror
     )
 
 
@@ -819,7 +850,7 @@ def update(
         project,
     )
 
-    ensure_project(three=three, python=python, warn=True)
+    ensure_project(three=three, python=python, warn=True, pypi_mirror=pypi_mirror)
     if not outdated:
         outdated = bool(dry_run)
     if outdated:
@@ -894,12 +925,19 @@ def graph(bare=False, json=False, json_tree=False, reverse=False):
     callback=validate_python_path,
     help="Specify which version of Python virtualenv should use.",
 )
+@option(
+    '--pypi-mirror',
+    default=environments.PIPENV_PYPI_MIRROR,
+    nargs=1,
+    callback=validate_pypi_mirror,
+    help="Specify a PyPI mirror.",
+)
 @argument('module', nargs=1)
-def run_open(module, three=None, python=None):
+def run_open(module, three=None, python=None, pypi_mirror=None):
     from .core import which, ensure_project
 
     # Ensure that virtualenv is available.
-    ensure_project(three=three, python=python, validate=False)
+    ensure_project(three=three, python=python, validate=False, pypi_mirror=pypi_mirror)
     c = delegator.run(
         '{0} -c "import {1}; print({1}.__file__);"'.format(
             which('python'), module
diff --git a/pipenv/core.py b/pipenv/core.py
index a7ed3575..494a1efc 100644
--- a/pipenv/core.py
+++ b/pipenv/core.py
@@ -548,7 +548,7 @@ def ensure_python(three=None, python=None):
     return path_to_python
 
 
-def ensure_virtualenv(three=None, python=None, site_packages=False):
+def ensure_virtualenv(three=None, python=None, site_packages=False, pypi_mirror=None):
     """Creates a virtualenv, if one doesn't exist."""
 
     def abort():
@@ -571,7 +571,7 @@ def ensure_virtualenv(three=None, python=None, site_packages=False):
                     )
                 )
                 sys.exit(1)
-            do_create_virtualenv(python=python, site_packages=site_packages)
+            do_create_virtualenv(python=python, site_packages=site_packages, pypi_mirror=pypi_mirror)
         except KeyboardInterrupt:
             # If interrupted, cleanup the virtualenv.
             cleanup_virtualenv(bare=False)
@@ -599,7 +599,7 @@ def ensure_virtualenv(three=None, python=None, site_packages=False):
         cleanup_virtualenv(bare=True)
         # Call this function again.
         ensure_virtualenv(
-            three=three, python=python, site_packages=site_packages
+            three=three, python=python, site_packages=site_packages, pypi_mirror=pypi_mirror
         )
 
 
@@ -612,6 +612,7 @@ def ensure_project(
     site_packages=False,
     deploy=False,
     skip_requirements=False,
+    pypi_mirror=None,
 ):
     """Ensures both Pipfile and virtualenv exist for the project."""
     # Automatically use an activated virtualenv.
@@ -622,7 +623,7 @@ def ensure_project(
     # Skip virtualenv creation when --system was used.
     if not system:
         ensure_virtualenv(
-            three=three, python=python, site_packages=site_packages
+            three=three, python=python, site_packages=site_packages, pypi_mirror=pypi_mirror
         )
         if warn:
             # Warn users if they are using the wrong version of Python.
@@ -885,7 +886,7 @@ def convert_three_to_python(three, python):
         return python
 
 
-def do_create_virtualenv(python=None, site_packages=False):
+def do_create_virtualenv(python=None, site_packages=False, pypi_mirror=None):
     """Creates a virtualenv."""
     click.echo(
         crayons.normal(u'Creating a virtualenv for this project...', bold=True),
@@ -933,7 +934,8 @@ def do_create_virtualenv(python=None, site_packages=False):
     # Actually create the virtualenv.
     with spinner():
         try:
-            c = delegator.run(cmd, block=False, timeout=PIPENV_TIMEOUT)
+            pip_config = {'PIP_INDEX_URL': fs_str(pypi_mirror)} if pypi_mirror else {}
+            c = delegator.run(cmd, block=False, timeout=PIPENV_TIMEOUT, env=pip_config)
         except OSError:
             click.echo(
                 '{0}: it looks like {1} is not in your {2}. '
@@ -1264,7 +1266,7 @@ def do_init(
     if not system:
         if not project.virtualenv_exists:
             try:
-                do_create_virtualenv()
+                do_create_virtualenv(pypi_mirror=pypi_mirror)
             except KeyboardInterrupt:
                 cleanup_virtualenv(bare=False)
                 sys.exit(1)
@@ -1789,6 +1791,7 @@ def do_install(
         warn=True,
         deploy=deploy,
         skip_requirements=skip_requirements,
+        pypi_mirror=pypi_mirror,
     )
     # Load the --pre settings from the Pipfile.
     if not pre:
@@ -2097,7 +2100,7 @@ def do_uninstall(
     if PIPENV_USE_SYSTEM:
         system = True
     # Ensure that virtualenv is available.
-    ensure_project(three=three, python=python)
+    ensure_project(three=three, python=python, pypi_mirror=pypi_mirror)
     package_names = (package_name,) + more_packages
     pipfile_remove = True
     # Un-install all dependencies, if --all was provided.
@@ -2166,11 +2169,11 @@ def do_uninstall(
         do_lock(system=system, keep_outdated=keep_outdated, pypi_mirror=pypi_mirror)
 
 
-def do_shell(three=None, python=False, fancy=False, shell_args=None):
+def do_shell(three=None, python=False, fancy=False, shell_args=None, pypi_mirror=None):
     from .patched.pew import pew
 
     # Ensure that virtualenv is available.
-    ensure_project(three=three, python=python, validate=False)
+    ensure_project(three=three, python=python, validate=False, pypi_mirror=pypi_mirror)
     # Set an environment variable, so we know we're in the environment.
     os.environ['PIPENV_ACTIVE'] = '1'
     compat = (not fancy)
@@ -2320,13 +2323,13 @@ def do_run_posix(script, command):
     os.execl(command_path, command_path, *script.args)
 
 
-def do_run(command, args, three=None, python=False):
+def do_run(command, args, three=None, python=False, pypi_mirror=None):
     """Attempt to run command either pulling from project or interpreting as executable.
 
     Args are appended to the command in [scripts] section of project if found.
     """
     # Ensure that virtualenv is available.
-    ensure_project(three=three, python=python, validate=False)
+    ensure_project(three=three, python=python, validate=False, pypi_mirror=pypi_mirror)
     load_dot_env()
     # Activate virtualenv under the current interpreter's environment
     inline_activate_virtualenv()
@@ -2340,10 +2343,10 @@ def do_run(command, args, three=None, python=False):
         do_run_posix(script, command=command)
 
 
-def do_check(three=None, python=False, system=False, unused=False, ignore=None, args=None):
+def do_check(three=None, python=False, system=False, unused=False, ignore=None, args=None, pypi_mirror=None):
     if not system:
         # Ensure that virtualenv is available.
-        ensure_project(three=three, python=python, validate=False, warn=False)
+        ensure_project(three=three, python=python, validate=False, warn=False, pypi_mirror=pypi_mirror)
     if not args:
         args = []
     if unused:
@@ -2586,7 +2589,7 @@ def do_sync(
         sys.exit(1)
 
     # Ensure that virtualenv is available if not system.
-    ensure_project(three=three, python=python, validate=False, deploy=deploy)
+    ensure_project(three=three, python=python, validate=False, deploy=deploy, pypi_mirror=pypi_mirror)
 
     # Install everything.
     requirements_dir = TemporaryDirectory(
@@ -2610,7 +2613,7 @@ def do_clean(
     ctx, three=None, python=None, dry_run=False, bare=False, verbose=False, pypi_mirror=None
 ):
     # Ensure that virtualenv is available.
-    ensure_project(three=three, python=python, validate=False)
+    ensure_project(three=three, python=python, validate=False, pypi_mirror=pypi_mirror)
     ensure_lockfile(pypi_mirror=pypi_mirror)
 
     installed_package_names = []