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. 


Discussion:
I've started working on the fix for this and hope to have it available for review by tomorrow. I intend to set the  PIP_INDEX_URL environment variable to the value of --pypi-mirror when calling virtualenv in do_create_virtualenv.

This was not detected during prior testing, as pip/virtualenv will silently fail if connections to pypi fail. However, pip will hang (which will cause pipenv to time out) if connections to pypi hang.
Thanks for digging into this.  Do you want to tackle it?  Is it a good idea to handle the exception(s)? -- are there any?

If not, should we temporarily set the default pip index while we create the environment?


(I see after typing this that you like the second option, which sounds fine to me)
Yeah, I'll tackle it. I see two options:
1. If --pypi-mirror is set, pass --no-download to virtualenv.
2. If it's set, temporarily set the default pip index via environment variable before calling virtualenv. There's no need to refine this behavior further, as all of the packages pip will try to download will necessarily be PyPI packages. 

I think 2 is probably more consistent with what the user would expect.
I'm marking our (immediate) bugfix release as 7.0, but really we are gonna get it released whenever we can.

If this is done whenever that is done, we will include it in the bugfix release.  If not, no pressure, we can get this in on the next cycle

Resolving patch:
diff --git a/pipenv/cli.py b/pipenv/cli.py
--- 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
--- a/pipenv/core.py
+++ b/pipenv/core.py
@@ -548,7 +548,7 @@ def activate_pyenv():
     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 abort():
                     )
                 )
                 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 abort():
         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 = []


Test patch: