Skip to content

Instantly share code, notes, and snippets.

@vsajip
Created April 4, 2011 07:58
Show Gist options
  • Save vsajip/901266 to your computer and use it in GitHub Desktop.
Save vsajip/901266 to your computer and use it in GitHub Desktop.
# Copyright (C) 2011 Vinay Sajip.
#
# Poor man's virtualize.py.
#
# Use with a Python executable built from the Python fork at
#
# https://bitbucket.org/vinay.sajip/pythonv/ as follows:
#
# python -m virtualize env_dir
#
# You'll need an Internet connection (needed to download distribute_setup.py).
#
# The script will change to the environment's binary directory and run
#
# ./python distribute_setup.py
#
# after which you can change to the environment's binary directory and do some
# easy_installs, e.g.
#
# ./easy_install setuptools-git
# ./easy_install ply
# ./easy_install Pygments
# ./easy_install Jinja2
# ./easy_install SQLAlchemy
# ./easy_install coverage
#
# Note that on Windows, distributions which include C extensions (e.g. coverage)
# may fail due to lack of a suitable C compiler.
#
import os
import os.path
import shutil
import sys
class Context:
"""
Holds information about a current virtualisation request.
"""
pass
class Virtualizer:
"""
This class exists to allow virtualisation to be customised. The
constructor parameters determine the virtualizer's behaviour when
called upon to create a virtual environment.
By default, the virtualizer installs Distribute so that you can pip or
easy_install other packages into the created environment.
By default, the virtualizer makes the system (global) site-packages dir
available to the created environment.
:param nosite: If True, the system (global) site-packages dir is
not available to the environment.
:param nodist: If True, Distribute is not installed into the created
environment.
:param clear: If True and the target directory exists, it is deleted.
Otherwise, if the target directory exists, an error is
raised.
:param progress: If Distribute is 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 Distribute.
If a callable is not specified, default progress
information is output to sys.stderr.
"""
def __init__(self, nosite=False, clear=False, nodist=False, progress=None):
self.nosite = nosite
self.clear = clear
self.nodist = nodist
self.progress = progress
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:
sys.stderr.write('.')
sys.stderr.flush()
def virtualize(self, env_dir):
"""
Create a virtualized environment in a directory.
:param env_dir: The target directory to create an environment in.
"""
context = self.create_directories(env_dir)
self.create_configuration(context)
self.setup_python(context)
if not self.nodist:
self.install_distribute(context)
def create_directories(self, env_dir):
"""
Create the directories for the environment.
Returns a context object which holds paths in the environment,
for use by subsequent logic.
"""
if os.path.exists(env_dir) and not self.clear:
raise ValueError('Directory exists: %s' % env_dir)
if os.path.exists(env_dir) and self.clear:
shutil.rmtree(env_dir)
context = Context()
context.env_dir = env_dir
os.makedirs(env_dir)
dirname, exename = os.path.split(os.path.abspath(sys.executable))
context.python_dir = dirname
context.python_exe = exename
if sys.platform == 'win32':
binname = 'Scripts'
incpath = 'Include'
libpath = os.path.join(env_dir, 'Lib', 'site-packages')
else:
binname = 'bin'
incpath = 'include'
libpath = os.path.join(env_dir, 'lib', 'python%d.%d' % sys.version_info[:2], 'site-packages')
context.inc_path = path = os.path.join(env_dir, incpath)
os.mkdir(path)
os.makedirs(libpath)
context.bin_path = binpath = os.path.join(env_dir, binname)
os.mkdir(binpath)
return context
def create_configuration(self, context):
"""
Create a configuration file indicating where the environment's Python
was copied from, and whether the system site-packages should be made
available in the environment.
:param context: The information for the virtualisation request being
processed.
"""
context.cfg_path = path = os.path.join(context.env_dir, 'env.cfg')
with open(path, 'w', encoding='utf-8') as f:
f.write('home = %s\n' % context.python_dir)
if self.nosite:
f.write('include-system-site = false\n')
def setup_python(self, context):
"""
Set up a Python executable in the environment.
:param context: The information for the virtualisation request being
processed.
"""
binpath = context.bin_path
exename = context.python_exe
path = os.path.join(binpath, exename)
shutil.copyfile(sys.executable, path)
dirname = context.python_dir
if sys.platform == 'win32':
files = [f for f in os.listdir(dirname) if f.endswith(('.pyd', '.dll'))]
for f in files:
src = os.path.join(dirname, f)
dst = os.path.join(binpath, f)
shutil.copyfile(src, dst)
else:
os.chmod(path, 0o755)
dst = os.path.join(binpath, 'python')
os.symlink(exename, dst)
def install_distribute(self, context):
"""
Install Distribute in the environment.
:param context: The information for the virtualisation request being
processed.
"""
from subprocess import Popen, PIPE
from threading import Thread
from urllib.request import urlretrieve
url = 'http://python-distribute.org/distribute_setup.py'
binpath = context.bin_path
exename = context.python_exe
distpath = os.path.join(binpath, 'distribute_setup.py')
# Download Distribute in the env
urlretrieve(url, distpath)
progress = self.progress
if progress is not None:
progress('Installing distribute', 'main')
else:
sys.stderr.write('Installing distribute ')
sys.stderr.flush()
# Install Distribute in the env
os.chdir(binpath)
if sys.platform == 'win32':
args = [exename, 'distribute_setup.py']
else:
args = [os.path.join('.', exename), 'distribute_setup.py']
p = Popen(args, stdout=PIPE, stderr=PIPE)
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)
# To customise, replace this factory with your own
virtualizer_factory = Virtualizer
def virtualize(env_dir, nosite=False, clear=False, nodist=False,
progress=None):
"""
Create a virtualized environment in a directory.
By default, installs Distribute so that you can pip or easy_install
other packages into the created environment.
By default, makes the system (global) site-packages dir available to
the created environment.
:param env_dir: The target directory to create an environment in.
:param nosite: If True, the system (global) site-packages dir is
not available to the environment.
:param nodist: If True, Distribute is not installed into the created
environment.
:param clear: If True and the target directory exists, it is deleted.
Otherwise, if the target directory exists, an error is
raised.
:param progress: If Distribute is 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 Distribute.
If a callable is not specified, default progress
information is output to sys.stderr.
"""
virtualizer = virtualizer_factory(nosite=nosite,
clear=clear,
nodist=nodist)
virtualizer.virtualize(env_dir)
def main():
compatible = True
if sys.version_info < (3, 3):
compatible = False
elif not hasattr(sys, 'virtual_prefix'):
compatible = False
if not compatible:
raise ValueError('This script is only for use with '
'Python 3.3 (pythonv variant)')
else:
import argparse
parser = argparse.ArgumentParser(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-distribute', default=False,
action='store_true', dest='nodist',
help="Don't install Distribute in the virtual "
"environment.")
parser.add_argument('--no-site-packages', default=False,
action='store_true', dest='nosite',
help="Don't give access to the global "
"site-packages dir to the virtual "
"environment.")
parser.add_argument('--clear', default=False, action='store_true',
dest='clear', help='Delete the environment '
'directory if it already '
'exists. If not specified and '
'the directory exists, an error'
' is raised.')
options = parser.parse_args()
virtualizer = virtualizer_factory(nosite=options.nosite,
clear=options.clear,
nodist=options.nodist)
for d in options.dirs:
virtualizer.virtualize(d)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment