-
-
Save BenjaminHoegh/336d6133b1ef5ecb787e63d083b1cb9e to your computer and use it in GitHub Desktop.
A script which demonstrates how to extend Python 3.3's EnvBuilder, by installing setuptools and pip in created venvs. This functionality is not provided as an integral part of Python 3.3 because, while setuptools and pip are very popular, they are third-party packages.The script needs Python 3.3 or later; invoke it using"python pyvenvex.py -h"fo…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# Copyright (C) 2013-2020 Vinay Sajip. New BSD License. | |
# | |
import os | |
import os.path | |
from subprocess import Popen, PIPE | |
import sys | |
from threading import Thread | |
from urllib.parse import urlparse | |
from urllib.request import urlretrieve | |
import venv | |
class ExtendedEnvBuilder(venv.EnvBuilder): | |
""" | |
This builder installs setuptools and pip so that you can pip or | |
easy_install other packages into the created environment. | |
:param nodist: If True, setuptools and pip are not installed into the | |
created environment. | |
:param nopip: If True, pip is not installed into the created | |
environment. | |
:param progress: If setuptools or pip are installed, the progress of the | |
installation can be monitored by passing a progress | |
callable. If specified, it is called with two | |
arguments: a string indicating some progress, and a | |
context indicating where the string is coming from. | |
The context argument can have one of three values: | |
'main', indicating that it is called from virtualize() | |
itself, and 'stdout' and 'stderr', which are obtained | |
by reading lines from the output streams of a subprocess | |
which is used to install the app. | |
If a callable is not specified, default progress | |
information is output to sys.stderr. | |
""" | |
def __init__(self, *args, **kwargs): | |
self.nodist = kwargs.pop('nodist', False) | |
self.nopip = kwargs.pop('nopip', False) | |
self.progress = kwargs.pop('progress', None) | |
self.verbose = kwargs.pop('verbose', False) | |
super().__init__(*args, **kwargs) | |
def post_setup(self, context): | |
""" | |
Set up any packages which need to be pre-installed into the | |
environment being created. | |
:param context: The information for the environment creation request | |
being processed. | |
""" | |
os.environ['VIRTUAL_ENV'] = context.env_dir | |
if not self.nodist: | |
self.install_setuptools(context) | |
# Can't install pip without setuptools | |
if not self.nopip and not self.nodist: | |
self.install_pip(context) | |
def reader(self, stream, context): | |
""" | |
Read lines from a subprocess' output stream and either pass to a progress | |
callable (if specified) or write progress information to sys.stderr. | |
""" | |
progress = self.progress | |
while True: | |
s = stream.readline() | |
if not s: | |
break | |
if progress is not None: | |
progress(s, context) | |
else: | |
if not self.verbose: | |
sys.stderr.write('.') | |
else: | |
sys.stderr.write(s.decode('utf-8')) | |
sys.stderr.flush() | |
stream.close() | |
def install_script(self, context, name, url): | |
_, _, path, _, _, _ = urlparse(url) | |
fn = os.path.split(path)[-1] | |
binpath = context.bin_path | |
distpath = os.path.join(binpath, fn) | |
# Download script into the env's binaries folder | |
urlretrieve(url, distpath) | |
progress = self.progress | |
if self.verbose: | |
term = '\n' | |
else: | |
term = '' | |
if progress is not None: | |
progress('Installing %s ...%s' % (name, term), 'main') | |
else: | |
sys.stderr.write('Installing %s ...%s' % (name, term)) | |
sys.stderr.flush() | |
# Install in the env | |
args = [context.env_exe, fn] | |
p = Popen(args, stdout=PIPE, stderr=PIPE, cwd=binpath) | |
t1 = Thread(target=self.reader, args=(p.stdout, 'stdout')) | |
t1.start() | |
t2 = Thread(target=self.reader, args=(p.stderr, 'stderr')) | |
t2.start() | |
p.wait() | |
t1.join() | |
t2.join() | |
if progress is not None: | |
progress('done.', 'main') | |
else: | |
sys.stderr.write('done.\n') | |
# Clean up - no longer needed | |
os.unlink(distpath) | |
def install_setuptools(self, context): | |
""" | |
Install setuptools in the environment. | |
:param context: The information for the environment creation request | |
being processed. | |
""" | |
url = 'https://bootstrap.pypa.io/ez_setup.py' | |
self.install_script(context, 'setuptools', url) | |
# clear up the setuptools archive which gets downloaded | |
pred = lambda o: o.startswith('setuptools-') and o.endswith('.tar.gz') | |
files = filter(pred, os.listdir(context.bin_path)) | |
for f in files: | |
f = os.path.join(context.bin_path, f) | |
os.unlink(f) | |
def install_pip(self, context): | |
""" | |
Install pip in the environment. | |
:param context: The information for the environment creation request | |
being processed. | |
""" | |
url = 'https://bootstrap.pypa.io/get-pip.py' | |
self.install_script(context, 'pip', url) | |
def main(args=None): | |
compatible = True | |
if sys.version_info < (3, 3): | |
compatible = False | |
elif not hasattr(sys, 'base_prefix'): | |
compatible = False | |
if not compatible: | |
raise ValueError('This script is only for use with ' | |
'Python 3.3 or later') | |
else: | |
import argparse | |
parser = argparse.ArgumentParser(prog=__name__, | |
description='Creates virtual Python ' | |
'environments in one or ' | |
'more target ' | |
'directories.') | |
parser.add_argument('dirs', metavar='ENV_DIR', nargs='+', | |
help='A directory to create the environment in.') | |
parser.add_argument('--no-setuptools', default=False, | |
action='store_true', dest='nodist', | |
help="Don't install setuptools or pip in the " | |
"virtual environment.") | |
parser.add_argument('--no-pip', default=False, | |
action='store_true', dest='nopip', | |
help="Don't install pip in the virtual " | |
"environment.") | |
parser.add_argument('--system-site-packages', default=False, | |
action='store_true', dest='system_site', | |
help='Give the virtual environment access to the ' | |
'system site-packages dir.') | |
if os.name == 'nt': | |
use_symlinks = False | |
else: | |
use_symlinks = True | |
parser.add_argument('--symlinks', default=use_symlinks, | |
action='store_true', dest='symlinks', | |
help='Try to use symlinks rather than copies, ' | |
'when symlinks are not the default for ' | |
'the platform.') | |
parser.add_argument('--clear', default=False, action='store_true', | |
dest='clear', help='Delete the contents of the ' | |
'environment directory if it ' | |
'already exists, before ' | |
'environment creation.') | |
parser.add_argument('--upgrade', default=False, action='store_true', | |
dest='upgrade', help='Upgrade the environment ' | |
'directory to use this version ' | |
'of Python, assuming Python ' | |
'has been upgraded in-place.') | |
parser.add_argument('--verbose', default=False, action='store_true', | |
dest='verbose', help='Display the output ' | |
'from the scripts which ' | |
'install setuptools and pip.') | |
options = parser.parse_args(args) | |
if options.upgrade and options.clear: | |
raise ValueError('you cannot supply --upgrade and --clear together.') | |
builder = ExtendedEnvBuilder(system_site_packages=options.system_site, | |
clear=options.clear, | |
symlinks=options.symlinks, | |
upgrade=options.upgrade, | |
nodist=options.nodist, | |
nopip=options.nopip, | |
verbose=options.verbose) | |
for d in options.dirs: | |
builder.create(d) | |
if __name__ == '__main__': | |
rc = 1 | |
try: | |
main() | |
rc = 0 | |
except Exception as e: | |
print('Error: %s' % e, file=sys.stderr) | |
sys.exit(rc) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment