Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit 00f1e2df58ab7f1cbbd922933bcfc5cd93a8538a Ben Weaver committed Sep 9, 2009
Showing with 554 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +25 −0 LICENSE
  3. +14 −0 README
  4. +31 −0 bin/ve
  5. +42 −0 bin/ve-clone
  6. +56 −0 bin/ve-extend
  7. +19 −0 bin/ve-init
  8. +32 −0 setup.py
  9. +10 −0 vecmd/__init__.py
  10. +167 −0 vecmd/commands.py
  11. +156 −0 vecmd/script.py
2 .gitignore
@@ -0,0 +1,2 @@
+*.pyc
+vecmd.egg-info
25 LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2009, Coptix, Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 README
@@ -0,0 +1,14 @@
+Virtualenv Commands
+-------------------
+
+The python virtualenv package is a tool used to create isolated Python
+environments. This package provides commands for inspecting and
+manipulating these environments.
+
+These utilities are provided:
+
+ ve -- a wrapper command used to execute ve-* commands
+ ve-init -- create and environment by invoking virtualenv
+ ve-extend -- chain virtualenvs together
+ ve-clone -- create a new virtualenv by copying an existing one
+
31 bin/ve
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+#### virtualenv-commands
+#### Command Wrapper
+
+### Copyright (c) 2009, Coptix, Inc. All rights reserved.
+### See the LICENSE file for license terms and warranty disclaimer.
+
+"""Run a virtualenv command.
+
+Commands may be any executable in the current path with a `ve-'
+prefix. Common commands are: init, extend, clone.
+"""
+
+from __future__ import absolute_import
+import sys, os, vecmd
+
+def main(command, *args):
+ command = 've-%s' % command
+ try:
+ return os.execvp(command, (command, ) + args)
+ except OSError as exc:
+ print exc
+ print 'Is "%s" executable in the current path?' % command
+
+if __name__ == '__main__':
+ vecmd.begin(
+ main,
+ usage="usage: %prog command [options] [arguments]",
+ description=__doc__
+ )
42 bin/ve-clone
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+#### virtualenv-commands
+#### Clone an Environment
+
+### Copyright (c) 2009, Coptix, Inc. All rights reserved.
+### See the LICENSE file for license terms and warranty disclaimer.
+
+"""Clone a virtualenv into a distinct virtualenv.
+
+All packages in the source environment are preserved. Cloning
+destroys an existing destination (you will be asked before this
+happens).
+"""
+
+from __future__ import absolute_import
+import sys, os, vecmd
+from vecmd import script
+
+def main(source, dest, **options):
+
+ if not options.get('force') and not confirm_existing(dest):
+ print 'Nothing modified.'
+ return 0
+
+ vecmd.clone(source, dest)
+ print 'Done.'
+
+def confirm_existing(ve):
+ if os.path.exists(ve):
+ print '"%s" already exists.' % ve
+ return script.confirm('Overwrite?')
+ return True
+
+if __name__ == '__main__':
+ vecmd.begin(
+ main,
+ usage="usage: %prog source destination",
+ description=__doc__,
+ options=(
+ vecmd.option('-f', '--force', action='store_true', help="don't prompt to overwrite destination"),
+ ))
56 bin/ve-extend
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+
+#### virtualenv-commands
+#### Extend an Environment
+
+### Copyright (c) 2009, Coptix, Inc. All rights reserved.
+### See the LICENSE file for license terms and warranty disclaimer.
+
+"""Extend a destination virtualenv with the source virtualenvs.
+
+Extending a virtual environment means "chaining" it to some other
+source environments. Anything in the destination virtualenv overrides
+source virtualenvs. The effect of extending one virtualenv with
+another is to append the source virtualenv site-packages to the
+sys.path of the destination virtualenv.
+"""
+
+from __future__ import absolute_import
+import sys, os, vecmd
+from vecmd import script
+
+def main(*args, **options):
+ source = args[0:-1]; dest = args[-1]
+
+ if not os.path.exists(dest) and options.get('create'):
+ vecmd.create(dest)
+
+ if not options.get('force') and not confirm_existing(dest):
+ print 'Nothing modified.'
+ return 0
+
+ vecmd.write_extensions(dest, source)
+ print 'Done modifying "%s".' % dest
+ print 'sys.path = ', vecmd.sys_path(dest)
+
+def confirm_existing(ve):
+ if vecmd.has_extensions(ve):
+ show_extensions(ve)
+ return script.confirm('Overwrite?')
+ return True
+
+def show_extensions(ve):
+ print '"%s" is already extends:' % ve
+ for ext in vecmd.extends(ve):
+ print ' ', ext
+
+if __name__ == '__main__':
+ vecmd.begin(
+ main,
+ require=2,
+ usage="usage: %prog source ... destination",
+ description=__doc__,
+ options=(
+ vecmd.option('-c', '--create', action='store_true', help='create the destination virtualenv if it does not exist'),
+ vecmd.option('-f', '--force', action='store_true', help="don't prompt to overwrite existing extensions")
+ ))
19 bin/ve-init
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+#### virtualenv-commands
+#### Create and Environment
+
+### Copyright (c) 2009, Coptix, Inc. All rights reserved.
+### See the LICENSE file for license terms and warranty disclaimer.
+
+"""Create a new environment by invoking virtualenv."""
+
+from __future__ import absolute_import
+import sys, os, vecmd
+from vecmd import script
+
+def main(*args):
+ return vecmd.virtualenv(*args)
+
+if __name__ == '__main__':
+ main(*sys.argv[1:])
32 setup.py
@@ -0,0 +1,32 @@
+#### virtualenv-commands
+#### Setup Script
+
+### Copyright (c) 2009, Coptix, Inc. All rights reserved.
+### See the LICENSE file for license terms and warranty disclaimer.
+
+from __future__ import absolute_import
+from setuptools import setup, find_packages
+
+setup(
+ name = 'vecmd',
+ version = '0.1',
+ description = 'Additional commands for virtualenv.',
+ author = 'Medium',
+ author_email = 'labs@thisismedium.com',
+ url = 'http://thisismedium.com/labs/virtualenv-commands/',
+ license = 'BSD',
+ keywords = 'utilities commands virtualenv',
+
+ packages = list(find_packages(exclude=('tests', 'docs', 'docs.*'))),
+ install_requires = 'virtualenv',
+ scripts = ['bin/ve', 'bin/ve-extend', 'bin/ve-clone', 'bin/ve-init'],
+
+ classifiers = [
+ 'Development Status :: 3 - Alpha',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: Unix',
+ 'Programming Language :: Python',
+ 'Topic :: Utilities'
+ ]
+)
10 vecmd/__init__.py
@@ -0,0 +1,10 @@
+#### virtualenv-commands
+#### Package Initialization
+
+### Copyright (c) 2009, Coptix, Inc. All rights reserved.
+### See the LICENSE file for license terms and warranty disclaimer.
+
+from __future__ import absolute_import
+from . import script
+from .script import begin, option
+from .commands import *
167 vecmd/commands.py
@@ -0,0 +1,167 @@
+#### virtualenv-commands
+#### Extended Environment API
+
+### Copyright (c) 2009, Coptix, Inc. All rights reserved.
+### See the LICENSE file for license terms and warranty disclaimer.
+
+"""ve -- virtualenv utilities"""
+
+from __future__ import absolute_import
+import os, re, contextlib
+from . import script
+
+__all__ = (
+ 'is_virtualenv', 'interpreter',
+ 'version', 'site_packages', 'sys_path',
+ 'create',
+ 'virtualenv', 'python',
+
+ 'has_extensions', 'extends_path', 'extends', 'write_extensions',
+ 'clone'
+)
+
+
+### Inspection
+
+def is_virtualenv(ve):
+ """Return True if the path ve exists and looks like a
+ virtualenv."""
+
+ return (
+ os.path.exists(ve)
+ and os.path.isfile(interpreter(ve))
+ )
+
+def interpreter(ve):
+ """Return a path to the python interpreter for the virtualenv ve."""
+
+ return os.path.join(ve, 'bin/python')
+
+def version(ve):
+ """Return the major.minor version of the python interpreter in
+ virtualenv ve."""
+
+ return python(ve, 'import sys; print "%d.%d" % sys.version_info[0:2]')
+
+def site_packages(ve):
+ """Return a path to the site-packages directory in virtualenv
+ ve."""
+ return os.path.join(ve, 'lib', 'python%s' % version(ve), 'site-packages')
+
+def without_site_packages(path):
+ """The inverse operation of site_packages()."""
+ return path.rsplit('/lib/', 2)[0]
+
+def sys_path(ve):
+ return python(ve, 'import sys; print sys.path')
+
+
+### Command execution
+
+def create(ve):
+ return virtualenv('--no-site-packages', '-q', ve)
+
+def virtualenv(*args):
+ """Run `virtualenv' with arguments args."""
+ return script.python(script.which('virtualenv'), '-q', *args)
+
+def python(ve, command):
+ """Execute a simple python statement in the virtualenv
+ interpreter. Return the captured output."""
+
+ assert is_virtualenv(ve)
+ return script.capture(interpreter(ve), '-c', command).strip()
+
+
+### Extensions
+
+## Extending a virtual environment means "chaining" it to some other
+## source environments. Anything in the destination virtualenv
+## overrides source virtualenvs. This effect of extending one
+## virtualenv with another is to append the source virtualenv
+## site-packages to the sys.path of the destination virtualenv.
+
+## For more information about sitecustomize.py, see the `site' module
+## documentation. Use sitecustomize.py instead of a `.pth' file since
+## site.addsitedir() recursively expands the directory (processes any
+## `.pth' files, runs its sitecustomize.py, etc).
+
+EXTENDS = 'sitecustomize.py'
+
+def has_extensions(ve):
+ return is_virtualenv(ve) and os.path.exists(extends_path(ve))
+
+def extends_path(ve):
+ return os.path.join(site_packages(ve), EXTENDS)
+
+def extends(ve):
+ if has_extensions(ve):
+ with extensions_file(ve) as port:
+ return filter(None, (_extension_to_ve(l) for l in port.readlines()))
+ return []
+
+def write_extensions(ve, extends):
+ with extensions_file(ve, 'w') as port:
+ print >> port, 'import site'
+ for source_ve in extends:
+ print >> port, _ve_to_extension(source_ve)
+
+def _ve_to_extension(ve):
+ return 'site.addsitedir("%s")' % os.path.abspath(site_packages(ve))
+
+_EXTENSION = re.compile('site\.addsitedir\("([^"]+)"\)')
+def _extension_to_ve(ext):
+ match = _EXTENSION.match(ext)
+ return match and without_site_packages(match.group(1))
+
+def extensions_file(ve, *args):
+ assert is_virtualenv(ve)
+ return contextlib.closing(open(extends_path(ve), *args))
+
+
+### Clone
+
+## Cloning a virtualenv means copying most of the source into the
+## destination. A little trickery happens in order to create all of
+## the right paths in the destination correctly. The basic strategy
+## is to create the new virtualenv, move it out of the way, copy the
+## source virtualenv, then overlay the newly-created virtualenv's bin
+## directory onto the copy of the source.
+
+CLONE_OVERWRITE = ('bin', )
+
+def clone(source, dest):
+
+ with script.tempdir(prefix='ve-clone-') as temp:
+ new_ve = os.path.join(temp, 'new')
+ old_ve = os.path.join(temp, 'old')
+
+ ## Do this transactionally. If something goes wrong, leave
+ ## the filesystem as it was.
+ if os.path.exists(dest):
+ script.move(dest, old_ve)
+
+ try:
+ ## 1. Create the new virtualenv in the correct location.
+ create(dest)
+
+ ## 2. Move it out of the way.
+ script.move(dest, new_ve)
+
+ ## 3. Copy the source virtualenv to the correct
+ ## destination path.
+ script.copy(source, dest)
+
+ ## 4. Copy the necessary parts of the new virtualenv over the
+ ## copied source.
+ script.overwrite(
+ (os.path.join(new_ve, rel) for rel in CLONE_OVERWRITE),
+ dest
+ )
+ except Exception:
+ ## Something went wrong. Put the old virtualenv back.
+ if os.path.exists(dest):
+ script.remove(dest)
+ if os.path.exists(old_ve):
+ script.move(old_ve, dest)
+ raise
156 vecmd/script.py
@@ -0,0 +1,156 @@
+#### virtualenv-commands
+#### Scripting Utilities
+
+### Copyright (c) 2009, Coptix, Inc. All rights reserved.
+### See the LICENSE file for license terms and warranty disclaimer.
+
+"""script -- Utilities for writing scripts."""
+
+from __future__ import absolute_import
+import os, sys, subprocess, tempfile, contextlib, optparse, inspect
+
+
+### Options and script execution
+
+def begin(proc, require=None, usage=None, description=None, options=None):
+ """Begin executing a command-line python script.
+
+ Example:
+
+ def main(foo, bar):
+ # main program
+
+ if __name__ = '__main__':
+ begin(main, usage="usage: %prog foo bar")
+ """
+ parser = optparse.OptionParser(usage, description=description)
+ parser.disable_interspersed_args()
+ if options:
+ for (args, kwargs) in options:
+ parser.add_option(*args, **kwargs)
+
+ ## The number of required arguments is the same as the number of
+ ## normal arguments to proc. If proc accepts varargs, this
+ ## command is nary.
+ spec = inspect.getargspec(proc)
+ nary = spec[1] is not None
+ if require is None:
+ require = len(spec[0]) - (len(spec[3]) if spec[3] else 0)
+
+ (options, args) = parser.parse_args()
+
+ arity = len(args)
+ if arity < require:
+ parser.error(
+ 'not enough arguments: %d expected, got %d' % (require, arity)
+ )
+ elif not nary and arity > require:
+ parser.error(
+ 'too many arguments: %d expected, got %d' % (require, arity)
+ )
+
+ sys.exit(proc(*args, **options.__dict__))
+
+def option(*args, **kwargs):
+ """Encapsulate the arguments to OptionParser.add_option() for
+ later use."""
+ return (args, kwargs)
+
+
+### Utilities
+
+def log(*args):
+ """Print a log message to STDERR."""
+
+ print >> sys.stderr, ' '.join(str(a) for a in args)
+
+def choose(prompt, choices='yn', default='n'):
+ """Present the user with a yes/no prompt. Return True if the user
+ chooses "yes"."""
+
+ compact = ''.join(c.upper() if c == default else c for c in choices)
+ prompt = '%s [%s]: ' % (prompt, compact)
+
+ value = ''
+ while True:
+ value = raw_input(prompt).strip().lower() or default
+ if value in choices:
+ break
+ print 'Invalid choice.'
+
+ return value
+
+def confirm(prompt, default='n'):
+ return choose(prompt, default=default) == 'y'
+
+@contextlib.contextmanager
+def tempdir(*args, **kwargs):
+ """Produce a context in which a temporary directory exists.
+ Destroy it when the context is exited."""
+
+ path = tempfile.mkdtemp(*args, **kwargs)
+ try:
+ yield path
+ finally:
+ remove(path)
+
+
+### Subprocess
+
+def must(*args):
+ ## FIXME: For some reason check_call() doesn't execute args as a
+ ## tuple.
+ cmd = join_args(*args)
+ # log('+', cmd)
+ return subprocess.check_call(cmd, shell=True)
+
+def capture(*args):
+ return subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0]
+
+def capture_stderr(*args):
+ return subprocess.Popen(args, stderr=subprocess.PIPE).communicate()[1]
+
+def join_args(*args):
+ from pipes import quote
+ return ' '.join(quote(a) for a in args)
+
+## Commands
+
+def overwrite(source, dest):
+ """Write source files over dest. The destination is not deleted
+ first, so the result is to merge source files into dest."""
+
+ args = [source] if isinstance(source, basestring) else list(source)
+ args.append(dest)
+
+ return must('rsync', '-aq', *args)
+
+def which(*args):
+ return capture('which', *args).strip()
+
+def move(*args):
+ return must('mv', *args)
+
+def copy(*args):
+ return must('cp', '-rp', *args)
+
+def remove(*paths):
+ return must('rm', '-rf', *paths)
+
+def python(*args):
+ return must(current_python_interpreter(), *args)
+
+def current_python_interpreter():
+ ## Note: This is kludge; what's a better way to find the "current"
+ ## python interpreter?
+
+ ## If this script is being executed directly, use which() to find
+ ## the python interpreter. Otherwise, assume the script is being
+ ## run through a python interpreter directly; look for the
+ ## interpreter in the environment.
+
+ path = os.environ['_']
+ if os.path.basename(path).startswith('python'):
+ return path
+ else:
+ return which('python')

0 comments on commit 00f1e2d

Please sign in to comment.
Something went wrong with that request. Please try again.