Skip to content

Commit e10c4ff

Browse files
committed
gh-110548: modernize venv EnvBuilder example, remove deprecated ez_setup.py
Replace the deprecated ez_setup.py-based setuptools installation example with a modern approach using pip (which is already bootstrapped via ensurepip since Python 3.4). The old example relied on downloading ez_setup.py from bootstrap.pypa.io to install setuptools, which has been deprecated since 2014. The new example demonstrates a cleaner pattern for extending EnvBuilder to pre-install packages using pip subprocess calls.
1 parent 072cd7c commit e10c4ff

File tree

1 file changed

+39
-127
lines changed

1 file changed

+39
-127
lines changed

Doc/library/venv.rst

Lines changed: 39 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -543,142 +543,65 @@ An example of extending ``EnvBuilder``
543543
--------------------------------------
544544

545545
The following script shows how to extend :class:`EnvBuilder` by implementing a
546-
subclass which installs setuptools and pip into a created virtual environment::
546+
subclass which pre-installs packages into a created virtual environment.
547+
548+
Since :mod:`!venv` bootstraps ``pip`` by default (via :mod:`ensurepip`), the
549+
example uses ``pip`` to install additional packages after the environment is
550+
created::
547551

548552
import os
549-
import os.path
550-
from subprocess import Popen, PIPE
553+
import subprocess
551554
import sys
552-
from threading import Thread
553-
from urllib.parse import urlsplit
554-
from urllib.request import urlretrieve
555555
import venv
556556

557+
557558
class ExtendedEnvBuilder(venv.EnvBuilder):
558559
"""
559-
This builder installs setuptools and pip so that you can pip or
560-
easy_install other packages into the created virtual environment.
561-
562-
:param nodist: If true, setuptools and pip are not installed into the
563-
created virtual environment.
564-
:param nopip: If true, pip is not installed into the created
565-
virtual environment.
566-
:param progress: If setuptools or pip are installed, the progress of the
567-
installation can be monitored by passing a progress
568-
callable. If specified, it is called with two
569-
arguments: a string indicating some progress, and a
570-
context indicating where the string is coming from.
571-
The context argument can have one of three values:
572-
'main', indicating that it is called from virtualize()
573-
itself, and 'stdout' and 'stderr', which are obtained
574-
by reading lines from the output streams of a subprocess
575-
which is used to install the app.
576-
577-
If a callable is not specified, default progress
578-
information is output to sys.stderr.
560+
This builder installs additional packages into the created
561+
virtual environment using pip.
562+
563+
:param packages: A list of packages to install after creating
564+
the virtual environment.
565+
:param verbose: If true, display the output from pip.
579566
"""
580567

581568
def __init__(self, *args, **kwargs):
582-
self.nodist = kwargs.pop('nodist', False)
583-
self.nopip = kwargs.pop('nopip', False)
584-
self.progress = kwargs.pop('progress', None)
569+
self.packages = kwargs.pop('packages', [])
585570
self.verbose = kwargs.pop('verbose', False)
586571
super().__init__(*args, **kwargs)
587572

588573
def post_setup(self, context):
589574
"""
590-
Set up any packages which need to be pre-installed into the
591-
virtual environment being created.
575+
Install additional packages into the virtual environment.
592576

593577
:param context: The information for the virtual environment
594578
creation request being processed.
595579
"""
596-
os.environ['VIRTUAL_ENV'] = context.env_dir
597-
if not self.nodist:
598-
self.install_setuptools(context)
599-
# Can't install pip without setuptools
600-
if not self.nopip and not self.nodist:
601-
self.install_pip(context)
602-
603-
def reader(self, stream, context):
604-
"""
605-
Read lines from a subprocess' output stream and either pass to a progress
606-
callable (if specified) or write progress information to sys.stderr.
607-
"""
608-
progress = self.progress
609-
while True:
610-
s = stream.readline()
611-
if not s:
612-
break
613-
if progress is not None:
614-
progress(s, context)
615-
else:
616-
if not self.verbose:
617-
sys.stderr.write('.')
618-
else:
619-
sys.stderr.write(s.decode('utf-8'))
620-
sys.stderr.flush()
621-
stream.close()
622-
623-
def install_script(self, context, name, url):
624-
_, _, path, _, _ = urlsplit(url)
625-
fn = os.path.split(path)[-1]
626-
binpath = context.bin_path
627-
distpath = os.path.join(binpath, fn)
628-
# Download script into the virtual environment's binaries folder
629-
urlretrieve(url, distpath)
630-
progress = self.progress
631-
if self.verbose:
632-
term = '\n'
633-
else:
634-
term = ''
635-
if progress is not None:
636-
progress('Installing %s ...%s' % (name, term), 'main')
637-
else:
638-
sys.stderr.write('Installing %s ...%s' % (name, term))
639-
sys.stderr.flush()
640-
# Install in the virtual environment
641-
args = [context.env_exe, fn]
642-
p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=binpath)
643-
t1 = Thread(target=self.reader, args=(p.stdout, 'stdout'))
644-
t1.start()
645-
t2 = Thread(target=self.reader, args=(p.stderr, 'stderr'))
646-
t2.start()
647-
p.wait()
648-
t1.join()
649-
t2.join()
650-
if progress is not None:
651-
progress('done.', 'main')
652-
else:
653-
sys.stderr.write('done.\n')
654-
# Clean up - no longer needed
655-
os.unlink(distpath)
656-
657-
def install_setuptools(self, context):
658-
"""
659-
Install setuptools in the virtual environment.
580+
if self.packages:
581+
self.install_packages(context)
660582

661-
:param context: The information for the virtual environment
662-
creation request being processed.
663-
"""
664-
url = "https://bootstrap.pypa.io/ez_setup.py"
665-
self.install_script(context, 'setuptools', url)
666-
# clear up the setuptools archive which gets downloaded
667-
pred = lambda o: o.startswith('setuptools-') and o.endswith('.tar.gz')
668-
files = filter(pred, os.listdir(context.bin_path))
669-
for f in files:
670-
f = os.path.join(context.bin_path, f)
671-
os.unlink(f)
672-
673-
def install_pip(self, context):
583+
def install_packages(self, context):
674584
"""
675-
Install pip in the virtual environment.
585+
Install the specified packages using pip.
676586

677587
:param context: The information for the virtual environment
678588
creation request being processed.
679589
"""
680-
url = 'https://bootstrap.pypa.io/get-pip.py'
681-
self.install_script(context, 'pip', url)
590+
# Use the pip installed in the virtual environment
591+
args = [context.env_exec_cmd, '-m', 'pip', 'install']
592+
if not self.verbose:
593+
args.append('-q')
594+
args.extend(self.packages)
595+
sys.stderr.write('Installing packages: %s ...\n'
596+
% ', '.join(self.packages))
597+
result = subprocess.run(args, capture_output=not self.verbose)
598+
if result.returncode != 0:
599+
sys.stderr.write('Package installation failed '
600+
'(exit code %d).\n' % result.returncode)
601+
if result.stderr:
602+
sys.stderr.write(result.stderr.decode('utf-8'))
603+
else:
604+
sys.stderr.write('done.\n')
682605

683606

684607
def main(args=None):
@@ -692,14 +615,9 @@ subclass which installs setuptools and pip into a created virtual environment::
692615
parser.add_argument('dirs', metavar='ENV_DIR', nargs='+',
693616
help='A directory in which to create the '
694617
'virtual environment.')
695-
parser.add_argument('--no-setuptools', default=False,
696-
action='store_true', dest='nodist',
697-
help="Don't install setuptools or pip in the "
698-
"virtual environment.")
699-
parser.add_argument('--no-pip', default=False,
700-
action='store_true', dest='nopip',
701-
help="Don't install pip in the virtual "
702-
"environment.")
618+
parser.add_argument('--packages', nargs='*', default=[],
619+
help='Packages to install after environment '
620+
'creation.')
703621
parser.add_argument('--system-site-packages', default=False,
704622
action='store_true', dest='system_site',
705623
help='Give the virtual environment access to the '
@@ -728,17 +646,15 @@ subclass which installs setuptools and pip into a created virtual environment::
728646
'in-place.')
729647
parser.add_argument('--verbose', default=False, action='store_true',
730648
dest='verbose', help='Display the output '
731-
'from the scripts which '
732-
'install setuptools and pip.')
649+
'from pip install.')
733650
options = parser.parse_args(args)
734651
if options.upgrade and options.clear:
735652
raise ValueError('you cannot supply --upgrade and --clear together.')
736653
builder = ExtendedEnvBuilder(system_site_packages=options.system_site,
737654
clear=options.clear,
738655
symlinks=options.symlinks,
739656
upgrade=options.upgrade,
740-
nodist=options.nodist,
741-
nopip=options.nopip,
657+
packages=options.packages,
742658
verbose=options.verbose)
743659
for d in options.dirs:
744660
builder.create(d)
@@ -751,7 +667,3 @@ subclass which installs setuptools and pip into a created virtual environment::
751667
except Exception as e:
752668
print('Error: %s' % e, file=sys.stderr)
753669
sys.exit(rc)
754-
755-
756-
This script is also available for download `online
757-
<https://gist.github.com/vsajip/4673395>`_.

0 commit comments

Comments
 (0)