pax_global_header00006660000000000000000000000064125627324440014523gustar00rootroot0000000000000052 comment=6f78a94269f8a0f3cca9363672e4fb327711fc14 pipsi-0.9/000077500000000000000000000000001256273244400125175ustar00rootroot00000000000000pipsi-0.9/.gitignore000066400000000000000000000001061256273244400145040ustar00rootroot00000000000000.DS_Store *.pyc *.pyo dist build *.egg *.egg-info .tox/ .env/ .cache/ pipsi-0.9/.travis.yml000066400000000000000000000001741256273244400146320ustar00rootroot00000000000000language: python python: - "2.6" - "2.7" - "pypy" - "3.3" - "3.4" install: pip install tox script: tox pipsi-0.9/LICENSE000066400000000000000000000027761256273244400135400ustar00rootroot00000000000000Copyright (c) 2014 by Armin Ronacher. Some rights reserved. Redistribution and use in source and binary forms of the software as well as documentation, 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. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE AND DOCUMENTATION 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 OWNER 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 AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pipsi-0.9/MANIFEST.in000066400000000000000000000001271256273244400142550ustar00rootroot00000000000000include README.rst LICENSE tox.ini include get-pipsi.py recursive-include testing *.py pipsi-0.9/README.rst000066400000000000000000000020021256273244400142000ustar00rootroot00000000000000pipsi ===== pipsi = pip script installer What does it do? pipsi is a wrapper around virtualenv and pip which installs scripts provided by python packages into separate virtualenvs to shield them from your system and each other. In other words: you can use pipsi to install things like pygmentize without making your system painful. How do I get it? :: curl https://raw.githubusercontent.com/mitsuhiko/pipsi/master/get-pipsi.py | python How does it work? pipsi installs each package into ~/.local/venvs/PKGNAME and then symlinks all new scripts into ~/.local/bin. Installing scripts from a package:: $ pipsi install Pygments Uninstalling packages and their scripts:: $ pipsi uninstall Pygments Upgrading a package:: $ pipsi upgrade Pygments Showing what's installed:: $ pipsi list How do I get rid of pipsi? :: $ pipsi uninstall pipsi How do I upgrade pipsi? With 0.5 and later just do this:: $ pipsi upgrade pipsi On older versions just uninstall and reinstall. pipsi-0.9/appveyor.yml000066400000000000000000000007561256273244400151170ustar00rootroot00000000000000build: false # Not a C# project, build stuff at the test step instead. environment: matrix: - PYTHON: "C:/Python27" - PYTHON: "C:/Python33" - PYTHON: "C:/Python34" init: - "ECHO %PYTHON%" - ps: "ls C:/Python*" install: - ps: (new-object net.webclient).DownloadFile('https://raw.github.com/pypa/pip/master/contrib/get-pip.py', 'C:/get-pip.py') - "%PYTHON%/python.exe C:/get-pip.py" - "%PYTHON%/Scripts/pip.exe install tox" test_script: - "%PYTHON%/Scripts/tox.exe" pipsi-0.9/get-pipsi.py000066400000000000000000000053651256273244400150030ustar00rootroot00000000000000#!/usr/bin/env python import os import sys from subprocess import call import shutil try: WindowsError except NameError: IS_WIN = False PIP = '/bin/pip' PIPSI = '/bin/pipsi' else: IS_WIN = True PIP = '/Scripts/pip.exe' PIPSI = '/Scripts/pipsi.exe' DEFAULT_PIPSI_HOME = os.path.expanduser('~/.local/venvs') DEFAULT_PIPSI_BIN_DIR = os.path.expanduser('~/.local/bin') def echo(msg=''): sys.stdout.write(msg + '\n') sys.stdout.flush() def fail(msg): sys.stderr.write(msg + '\n') sys.stderr.flush() sys.exit(1) def succeed(msg): echo(msg) sys.exit(0) def command_exists(cmd): with open(os.devnull, 'w') as null: try: return call( [cmd, '--version'], stdout=null, stderr=null) == 0 except OSError: return False def publish_script(venv, bin_dir): if IS_WIN: for name in os.listdir(venv + '/Scripts'): if 'pipsi' in name.lower(): shutil.copy(venv + '/Scripts/' + name, bin_dir) else: os.symlink(venv + '/bin/pipsi', bin_dir + '/pipsi') echo('Installed pipsi binary in ' + bin_dir) def install_files(venv, bin_dir, install): try: os.makedirs(bin_dir) except OSError: pass def _cleanup(): try: shutil.rmtree(venv) except (OSError, IOError): pass if call(['virtualenv', venv]) != 0: _cleanup() fail('Could not create virtualenv for pipsi :(') if call([venv + PIP, 'install', install]) != 0: _cleanup() fail('Could not install pipsi :(') publish_script(venv, bin_dir) def main(): if command_exists('pipsi'): succeed('You already have pipsi installed') else: echo('Installing pipsi') if not command_exists('virtualenv'): fail('You need to have virtualenv installed to bootstrap pipsi.') bin_dir = os.environ.get('PIPSI_BIN_DIR', DEFAULT_PIPSI_BIN_DIR) venv = os.path.join(os.environ.get('PIPSI_HOME', DEFAULT_PIPSI_HOME), 'pipsi') install_files(venv, bin_dir, 'pipsi') if not command_exists('pipsi') != 0: echo() echo('=' * 60) echo() echo('Warning:') echo(' It looks like {0} is not on your PATH so pipsi will'.format(bin_dir)) echo(' not work out of the box. To fix this problem make sure to') echo(' add this to your .bashrc / .profile file:') echo() echo(' export PATH={0}:$PATH'.format(bin_dir)) echo() echo('=' * 60) echo() succeed('pipsi is now installed.') if __name__ == '__main__': if len(sys.argv) > 1: # we are being tested install_files(*sys.argv[1:]) else: main() pipsi-0.9/pipsi.py000066400000000000000000000310571256273244400142230ustar00rootroot00000000000000import os import sys import shutil import glob from os.path import join, realpath, dirname, normpath, normcase from operator import methodcaller try: from urlparse import urlparse except ImportError: from urllib.parse import urlparse import click from pkg_resources import Requirement try: WindowsError except NameError: IS_WIN = False BIN_DIR = 'bin' else: IS_WIN = True BIN_DIR = 'Scripts' FIND_SCRIPTS_SCRIPT = r'''if 1: import os import sys import pkg_resources pkg = sys.argv[1] prefix = sys.argv[2] dist = pkg_resources.get_distribution(pkg) if dist.has_metadata('RECORD'): for line in dist.get_metadata_lines('RECORD'): print(os.path.join(dist.location, line.split(',')[0])) elif dist.has_metadata('installed-files.txt'): for line in dist.get_metadata_lines('installed-files.txt'): print(os.path.join(dist.egg_info, line.split(',')[0])) elif dist.has_metadata('entry_points.txt'): try: from ConfigParser import SafeConfigParser from StringIO import StringIO except ImportError: from configparser import SafeConfigParser from io import StringIO parser = SafeConfigParser() parser.readfp(StringIO( '\n'.join(dist.get_metadata_lines('entry_points.txt')))) if parser.has_section('console_scripts'): for name, _ in parser.items('console_scripts'): print(os.path.join(prefix, name)) ''' # The `click` custom context settings CONTEXT_SETTINGS = dict( help_option_names=['-h', '--help'], ) def normalize_package(value): # Strips the version and normalizes name requirement = Requirement.parse(value) return requirement.project_name.lower() def normalize(path): return normcase(normpath(realpath(path))) def real_readlink(filename): try: target = os.readlink(filename) except (OSError, IOError, AttributeError): return None return normpath(realpath(join(dirname(filename), target))) def statusoutput(argv, **kw): from subprocess import Popen, PIPE p = Popen( argv, stdout=PIPE, stderr=PIPE, **kw) output = p.communicate()[0].strip() if not isinstance(output, str): output = output.decode('utf-8', 'replace') return p.returncode, output def publish_script(src, dst): if IS_WIN: # always copy new exe on windows shutil.copy(src, dst) click.echo(' Copied Executable ' + dst) return True else: old_target = real_readlink(dst) if old_target == src: return True try: os.remove(dst) except OSError: pass try: os.symlink(src, dst) except OSError: pass else: click.echo(' Linked script ' + dst) return True def find_scripts(virtualenv, package): prefix = normalize(join(virtualenv, BIN_DIR, '')) files = statusoutput([ join(prefix, 'python'), '-c', FIND_SCRIPTS_SCRIPT, package, prefix ])[1].splitlines() files = map(normalize, files) files = filter( methodcaller('startswith', prefix), files, ) def valid(filename): return os.path.isfile(filename) and \ IS_WIN or os.access(filename, os.X_OK) result = list(filter(valid, files)) if IS_WIN: for filename in files: globed = glob.glob(filename + '*') result.extend(filter(valid, globed)) return result class UninstallInfo(object): def __init__(self, package, paths=None, installed=True): self.package = package self.paths = paths or [] self.installed = installed def perform(self): for path in self.paths: try: os.remove(path) except OSError: shutil.rmtree(path) class Repo(object): def __init__(self, home, bin_dir): self.home = home self.bin_dir = bin_dir def resolve_package(self, spec, python=None): url = urlparse(spec) if url.netloc == 'file': location = url.path elif url.netloc != '': if not url.fragment.startswith('egg='): raise click.UsageError('When installing from URLs you need ' 'to add an egg at the end. For ' 'instance git+https://.../#egg=Foo') return url.fragment[4:], [spec] elif os.path.isdir(spec): location = spec else: return spec, [spec] error, name = statusoutput( [python or sys.executable, 'setup.py', '--name'], cwd=location) if error: raise click.UsageError('%s does not appear to be a local ' 'Python package.' % spec) return name, [location] def get_package_path(self, package): return join(self.home, normalize_package(package)) def find_installed_executables(self, path): prefix = join(realpath(normpath(path)), '') try: for filename in os.listdir(self.bin_dir): exe = os.path.join(self.bin_dir, filename) target = real_readlink(exe) if target is None: continue if target.startswith(prefix): yield exe except OSError: pass def link_scripts(self, scripts): rv = [] for script in scripts: script_dst = os.path.join( self.bin_dir, os.path.basename(script)) if publish_script(script, script_dst): rv.append((script, script_dst)) return rv def install(self, package, python=None, editable=False): package, install_args = self.resolve_package(package, python) venv_path = self.get_package_path(package) if os.path.isdir(venv_path): click.echo('%s is already installed' % package) return if not os.path.exists(self.bin_dir): os.makedirs(self.bin_dir) from subprocess import Popen def _cleanup(): try: shutil.rmtree(venv_path) except (OSError, IOError): pass return False # Install virtualenv, use the pipsi used python version by default args = ['virtualenv', '-p', python or sys.executable, venv_path] try: if Popen(args).wait() != 0: click.echo('Failed to create virtualenv. Aborting.') return _cleanup() args = [os.path.join(venv_path, BIN_DIR, 'pip'), 'install'] if editable: args.append('--editable') if Popen(args + install_args).wait() != 0: click.echo('Failed to pip install. Aborting.') return _cleanup() except Exception: _cleanup() raise # Find all the scripts scripts = find_scripts(venv_path, package) # And link them linked_scripts = self.link_scripts(scripts) # We did not link any, rollback. if not linked_scripts: click.echo('Did not find any scripts. Uninstalling.') return _cleanup() return True def uninstall(self, package): path = self.get_package_path(package) if not os.path.isdir(path): return UninstallInfo(package, installed=False) paths = [path] paths.extend(self.find_installed_executables(path)) return UninstallInfo(package, paths) def upgrade(self, package, editable=False): package, install_args = self.resolve_package(package) venv_path = self.get_package_path(package) if not os.path.isdir(venv_path): click.echo('%s is not installed' % package) return from subprocess import Popen old_scripts = set(find_scripts(venv_path, package)) args = [os.path.join(venv_path, BIN_DIR, 'pip'), 'install', '--upgrade'] if editable: args.append('--editable') if Popen(args + install_args).wait() != 0: click.echo('Failed to upgrade through pip. Aborting.') return scripts = find_scripts(venv_path, package) linked_scripts = self.link_scripts(scripts) to_delete = old_scripts - set(x[0] for x in linked_scripts) for script_src, script_link in linked_scripts: if script_src in to_delete: try: click.echo(' Removing old script %s' % script_src) os.remove(script_link) except (IOError, OSError): pass def list_everything(self): venvs = {} python = '/Scripts/python.exe' if IS_WIN else '/bin/python' for venv in os.listdir(self.home): venv_path = os.path.join(self.home, venv) if os.path.isdir(venv_path) and \ os.path.isfile(venv_path + python): venvs[venv] = [] def _find_venv(target): for venv in venvs: if target.startswith(join(self.home, venv, '')): return venv for script in os.listdir(self.bin_dir): exe = os.path.join(self.bin_dir, script) target = real_readlink(exe) if target is None: continue venv = _find_venv(target) if venv is not None: venvs[venv].append(script) return sorted(venvs.items()) @click.group(context_settings=CONTEXT_SETTINGS) @click.option( '--home', type=click.Path(),envvar='PIPSI_HOME', default=os.path.expanduser('~/.local/venvs'), help='The folder that contains the virtualenvs.') @click.option( '--bin-dir', type=click.Path(), envvar='PIPSI_BIN_DIR', default=os.path.expanduser('~/.local/bin'), help='The path where the scripts are symlinked to.') @click.version_option( message='%(prog)s, version %(version)s, python ' + str(sys.executable)) @click.pass_context def cli(ctx, home, bin_dir): """pipsi is a tool that uses virtualenv and pip to install shell tools that are separated from each other. """ ctx.obj = Repo(home, bin_dir) @cli.command() @click.argument('package') @click.option('--python', default=None, help='The python interpreter to use.') @click.option('--editable', '-e', is_flag=True, help='Enable editable installation. This only works for ' 'locally installed packages.') @click.pass_obj def install(repo, package, python, editable): """Installs scripts from a Python package. Given a package this will install all the scripts and their dependencies of the given Python package into a new virtualenv and symlinks the discovered scripts into BIN_DIR (defaults to ~/.local/bin). """ if repo.install(package, python, editable): click.echo('Done.') else: sys.exit(1) @cli.command() @click.argument('package') @click.option('--editable', '-e', is_flag=True, help='Enable editable installation. This only works for ' 'locally installed packages.') @click.pass_obj def upgrade(repo, package, editable): """Upgrades an already installed package.""" if repo.upgrade(package, editable): click.echo('Done.') else: sys.exit(1) @cli.command(short_help='Uninstalls scripts of a package.') @click.argument('package') @click.option('--yes', is_flag=True, help='Skips all prompts.') @click.pass_obj def uninstall(repo, package, yes): """Uninstalls all scripts of a Python package and cleans up the virtualenv. """ uinfo = repo.uninstall(package) if not uinfo.installed: click.echo('%s is not installed' % package) else: click.echo('The following paths will be removed:') for path in uinfo.paths: click.echo(' %s' % click.format_filename(path)) click.echo() if yes or click.confirm('Do you want to uninstall %s?' % package): uinfo.perform() click.echo('Done!') else: click.echo('Aborted!') sys.exit(1) @cli.command('list') @click.pass_obj def list_cmd(repo): """Lists all scripts installed through pipsi.""" click.echo('Packages and scripts installed through pipsi:') for venv, scripts in repo.list_everything(): if not scripts: continue click.echo(' Package "%s":' % venv) for script in scripts: click.echo(' ' + script) if __name__ == '__main__': cli() pipsi-0.9/setup.py000066400000000000000000000010471256273244400142330ustar00rootroot00000000000000from setuptools import setup with open('README.rst', 'rb') as f: readme = f.read().decode('utf-8') setup( name='pipsi', version='0.9', description='Wraps pip and virtualenv to install scripts', long_description=readme, license='BSD', author='Armin Ronacher', author_email='armin.ronacher@active-4.com', url='http://github.com/mitsuhiko/pipsi/', py_modules=['pipsi'], install_requires=[ 'Click', 'virtualenv', ], entry_points=''' [console_scripts] pipsi=pipsi:cli ''' ) pipsi-0.9/testing/000077500000000000000000000000001256273244400141745ustar00rootroot00000000000000pipsi-0.9/testing/test_install_script.py000066400000000000000000000007051256273244400206410ustar00rootroot00000000000000import sys import subprocess from pipsi import IS_WIN def test_create_env(tmpdir): subprocess.check_call([ sys.executable, 'get-pipsi.py', str(tmpdir.join('venv')), str(tmpdir.join('test_bin')), '.' ]) if IS_WIN: subprocess.check_call([ str(tmpdir.join('test_bin/pipsi.exe')) ]) else: subprocess.check_call([ str(tmpdir.join('test_bin/pipsi')) ]) pipsi-0.9/testing/test_repo.py000066400000000000000000000023151256273244400165530ustar00rootroot00000000000000import os import sys import pytest from pipsi import Repo, find_scripts @pytest.fixture(params=['normal', 'MixedCase']) def mix(request): return request.param @pytest.fixture def bin(tmpdir, mix): return tmpdir.ensure(mix, 'bin', dir=1) @pytest.fixture def home(tmpdir, mix): return tmpdir.ensure(mix, 'venvs', dir=1) @pytest.fixture def repo(home, bin): return Repo(str(home), str(bin)) @pytest.mark.parametrize('package, glob', [ ('grin', 'grin*'), ('pipsi', 'pipsi*'), ]) def test_simple_install(repo, home, bin, package, glob): assert not home.listdir() assert not bin.listdir() repo.install(package) assert home.join(package).check() assert bin.listdir(glob) @pytest.mark.xfail(sys.version_info[0] != 3, reason='attic is python3 only') def test_simple_install_attic(repo, home, bin): test_simple_install(repo, home, bin, 'attic', 'attic*') def test_find_scripts(): print('executable ' + sys.executable) env = os.path.dirname( os.path.dirname(sys.executable)) print('env %r' % env) print('listdir %r' % os.listdir(env)) scripts = list(find_scripts(env, 'pipsi')) print('scripts %r' % scripts) assert scripts pipsi-0.9/tox.ini000066400000000000000000000000651256273244400140330ustar00rootroot00000000000000 [testenv] deps= pytest commands= py.test []