-
-
Save vsajip/4673395 to your computer and use it in GitHub Desktop.
# | |
# 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) |
I found that (on Linux) pip installs to the "local/bin" folder of the venv. But since running "activate" only prepends the "bin" folder to $PATH, a symlink needs to be created so that pip can be run in the venv without specifying a path. I forked your code and added a few lines to do this. If you want to pull the changes, it's here:
https://gist.github.com/abbottc/7382709
And thanks for the script!
Quick shortcut for using:
$ python3 -c "$(curl https://gist.github.com/vsajip/4673395/raw/3420d9150ce1e9797dc8522fce7386d8643b02a1/pyvenvex.py)" env-dir
As mentioned, I needed to install bzip2 $ pip3 install bz2file
.
I installed without symlinks, but core modules are missing, like os, encodings...
$ ./pyvenvex-env/bin/python3.4
Python 3.4.0 (default, Apr 11 2014, 13:05:11)
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os
<module 'os' from '/usr/lib/python3.4/os.py'>
@vsajip I forked and made some cosmetic changes to this script that I would like to include in an update to the Python docs:
https://gist.github.com/stevepiercy/5039c54d65dfad7315411637f335479e/revisions
I don't know how to submit PRs to gists, so hopefully you can just copy-pasta.
Python docs:
https://docs.python.org/3/library/venv.html#an-example-of-extending-envbuilder
Issue tracker:
https://bugs.python.org/issue27285
These changes help to clarify that a "virtual environment" (and not an "env" or "environment") is being built. Thank you for your consideration.
Note... Line 136 tries to download git-pip from 'https://raw.github.com/pypa/pip/master/contrib/get-pip.py'
That returns a 404
I recommend the following URL instead... https://github.com/pypa/get-pip
Oh I did that lol
https://gist.github.com/vsajip/4673395#file-pyvenvex-py-L120
Looks like Bitbucket repo is not available anymore.
I made it work again by replacing it with https://raw.githubusercontent.com/ActiveState/ez_setup/master/ez_setup.py
Or perhaps the canonical location - https://bootstrap.pypa.io/ez_setup.py - might be better. I've updated the Gist to get those resources from https://bootstrap.pypa.io/ - hopefully that solves the linkrot problem!
Had to replace the url for pip, output during recent install on Windows 10 machine:
Installing pip ...
ERROR: This script does not work on Python 3.3 The minimum supported Python version is 3.7. Please use https://bootstrap.pypa.io/pip/3.3/get-pip.py instead.
done.
New link https://bootstrap.pypa.io/pip/3.3/get-pip.py worked great. Was able to install packages with
python -m pip install [packagename] --ignore-requires-python
Hi, and thanks!
I'm fiddling with a Python-from-scratch script...and found that your venv wrapper doesn't generate an error whenever pip install fails due to a missing bz2 library.
Bit pip is not installed!
If I add --verbose, it reveals the error. And of course the workaround is to build Python with bzip2 support. But still no exit(!0) status.