Close #19552: venv and pyvenv ensurepip integration
diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py
index dbbe157..6047f87 100644
--- a/Lib/test/test_venv.py
+++ b/Lib/test/test_venv.py
@@ -16,6 +16,10 @@
import unittest
import venv
+skipInVenv = unittest.skipIf(sys.prefix != sys.base_prefix,
+ 'Test not appropriate in a venv')
+
+
class BaseTest(unittest.TestCase):
"""Base class for venv tests."""
@@ -83,8 +87,7 @@
print(' %r' % os.listdir(bd))
self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
- @unittest.skipIf(sys.prefix != sys.base_prefix, 'Test not appropriate '
- 'in a venv')
+ @skipInVenv
def test_prefixes(self):
"""
Test that the prefix values are as expected.
@@ -217,8 +220,7 @@
# run the test, the pyvenv.cfg in the venv created in the test will
# point to the venv being used to run the test, and we lose the link
# to the source build - so Python can't initialise properly.
- @unittest.skipIf(sys.prefix != sys.base_prefix, 'Test not appropriate '
- 'in a venv')
+ @skipInVenv
def test_executable(self):
"""
Test that the sys.executable value is as expected.
@@ -247,8 +249,50 @@
out, err = p.communicate()
self.assertEqual(out.strip(), envpy.encode())
+
+@skipInVenv
+class EnsurePipTest(BaseTest):
+ """Test venv module installation of pip."""
+
+ def test_no_pip_by_default(self):
+ shutil.rmtree(self.env_dir)
+ self.run_with_capture(venv.create, self.env_dir)
+ envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe)
+ try_import = 'try:\n import pip\nexcept ImportError:\n print("OK")'
+ cmd = [envpy, '-c', try_import]
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = p.communicate()
+ self.assertEqual(err, b"")
+ self.assertEqual(out.strip(), b"OK")
+
+ def test_explicit_no_pip(self):
+ shutil.rmtree(self.env_dir)
+ self.run_with_capture(venv.create, self.env_dir, with_pip=False)
+ envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe)
+ try_import = 'try:\n import pip\nexcept ImportError:\n print("OK")'
+ cmd = [envpy, '-c', try_import]
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = p.communicate()
+ self.assertEqual(err, b"")
+ self.assertEqual(out.strip(), b"OK")
+
+ def test_with_pip(self):
+ shutil.rmtree(self.env_dir)
+ self.run_with_capture(venv.create, self.env_dir, with_pip=True)
+ envpy = os.path.join(os.path.realpath(self.env_dir), self.bindir, self.exe)
+ cmd = [envpy, '-m', 'pip', '--version']
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = p.communicate()
+ self.assertEqual(err, b"")
+ self.assertTrue(out.startswith(b"pip"))
+ self.assertIn(self.env_dir.encode(), out)
+
+
def test_main():
- run_unittest(BasicTest)
+ run_unittest(BasicTest, EnsurePipTest)
if __name__ == "__main__":
test_main()
diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py
index a57db0e..2991c66 100644
--- a/Lib/venv/__init__.py
+++ b/Lib/venv/__init__.py
@@ -24,10 +24,13 @@
raised.
--upgrade Upgrade the environment directory to use this version
of Python, assuming Python has been upgraded in-place.
+ --without-pip Skips installing or upgrading pip in the virtual
+ environment (pip is bootstrapped by default)
"""
import logging
import os
import shutil
+import subprocess
import sys
import sysconfig
import types
@@ -56,14 +59,17 @@
:param symlinks: If True, attempt to symlink rather than copy files into
virtual environment.
:param upgrade: If True, upgrade an existing virtual environment.
+ :param with_pip: If True, ensure pip is installed in the virtual
+ environment
"""
def __init__(self, system_site_packages=False, clear=False,
- symlinks=False, upgrade=False):
+ symlinks=False, upgrade=False, with_pip=False):
self.system_site_packages = system_site_packages
self.clear = clear
self.symlinks = symlinks
self.upgrade = upgrade
+ self.with_pip = with_pip
def create(self, env_dir):
"""
@@ -76,6 +82,8 @@
context = self.ensure_directories(env_dir)
self.create_configuration(context)
self.setup_python(context)
+ if self.with_pip:
+ self._setup_pip(context)
if not self.upgrade:
self.setup_scripts(context)
self.post_setup(context)
@@ -224,6 +232,12 @@
shutil.copyfile(src, dst)
break
+ def _setup_pip(self, context):
+ """Installs or upgrades pip in a virtual environment"""
+ cmd = [context.env_exe, '-m', 'ensurepip', '--upgrade',
+ '--default-pip']
+ subprocess.check_output(cmd)
+
def setup_scripts(self, context):
"""
Set up scripts into the created environment from a directory.
@@ -317,7 +331,8 @@
shutil.copymode(srcfile, dstfile)
-def create(env_dir, system_site_packages=False, clear=False, symlinks=False):
+def create(env_dir, system_site_packages=False, clear=False,
+ symlinks=False, with_pip=False):
"""
Create a virtual environment in a directory.
@@ -333,9 +348,11 @@
raised.
:param symlinks: If True, attempt to symlink rather than copy files into
virtual environment.
+ :param with_pip: If True, ensure pip is installed in the virtual
+ environment
"""
builder = EnvBuilder(system_site_packages=system_site_packages,
- clear=clear, symlinks=symlinks)
+ clear=clear, symlinks=symlinks, with_pip=with_pip)
builder.create(env_dir)
def main(args=None):
@@ -390,12 +407,19 @@
'directory to use this version '
'of Python, assuming Python '
'has been upgraded in-place.')
+ parser.add_argument('--without-pip', dest='with_pip',
+ default=True, action='store_false',
+ help='Skips installing or upgrading pip in the '
+ 'virtual environment (pip is bootstrapped '
+ 'by default)')
options = parser.parse_args(args)
if options.upgrade and options.clear:
raise ValueError('you cannot supply --upgrade and --clear together.')
builder = EnvBuilder(system_site_packages=options.system_site,
- clear=options.clear, symlinks=options.symlinks,
- upgrade=options.upgrade)
+ clear=options.clear,
+ symlinks=options.symlinks,
+ upgrade=options.upgrade,
+ with_pip=options.with_pip)
for d in options.dirs:
builder.create(d)