mirror of
https://github.com/kennethreitz/pipenv.git
synced 2026-06-05 22:50:18 +00:00
dc02663c66
hit OK, or neither did ok, then raise an exception. Also doing something similar to `cli.pip_install` so that it will try to pip install for all the sources and return the first cmd that worked or the last that didn't work.
1021 lines
35 KiB
Python
1021 lines
35 KiB
Python
# -*- coding: utf-8 -*-
|
|
import contextlib
|
|
import codecs
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import distutils.spawn
|
|
import shutil
|
|
import signal
|
|
|
|
import click
|
|
import click_completion
|
|
import crayons
|
|
import delegator
|
|
import parse
|
|
import pexpect
|
|
import requests
|
|
import pipfile
|
|
from blindspin import spinner
|
|
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
|
|
|
from .project import Project
|
|
from .utils import convert_deps_from_pip, convert_deps_to_pip, is_required_version
|
|
from .__version__ import __version__
|
|
from . import pep508checker
|
|
from .environments import PIPENV_COLORBLIND, PIPENV_NOSPIN, PIPENV_SHELL_COMPAT, PIPENV_VENV_IN_PROJECT
|
|
|
|
try:
|
|
from HTMLParser import HTMLParser
|
|
except ImportError:
|
|
from html.parser import HTMLParser
|
|
|
|
# Backport required for earlier versions of Python.
|
|
if sys.version_info < (3, 3):
|
|
from backports.shutil_get_terminal_size import get_terminal_size
|
|
else:
|
|
from shutil import get_terminal_size
|
|
|
|
# ___ _ ___
|
|
# | . \<_> ___ | __>._ _ _ _
|
|
# | _/| || . \| _> | ' || | |
|
|
# |_| |_|| _/|___>|_|_||__/
|
|
# |_|
|
|
|
|
# Enable shell completion.
|
|
click_completion.init()
|
|
|
|
# Disable colors, for the soulless.
|
|
if PIPENV_COLORBLIND:
|
|
crayons.disable()
|
|
|
|
# Disable spinner, for cleaner build logs (the unworthy).
|
|
if PIPENV_NOSPIN:
|
|
@contextlib.contextmanager
|
|
def spinner():
|
|
yield
|
|
|
|
# Disable warnings for Python 2.6.
|
|
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
|
|
|
project = Project()
|
|
|
|
|
|
def ensure_latest_pip():
|
|
"""Updates pip to the latest version."""
|
|
|
|
# Ensure that pip is installed.
|
|
c = delegator.run('{0} install pip'.format(which_pip()))
|
|
|
|
# Check if version is out of date.
|
|
if 'however' in c.err:
|
|
# If version is out of date, update.
|
|
click.echo(crayons.yellow('Pip is out of date... updating to latest.'))
|
|
c = delegator.run('{0} install pip --upgrade'.format(which_pip()), block=False)
|
|
click.echo(crayons.blue(c.out))
|
|
|
|
|
|
def ensure_pipfile(validate=True):
|
|
"""Creates a Pipfile for the project, if it doesn't exist."""
|
|
|
|
# Assert Pipfile exists.
|
|
if not project.pipfile_exists:
|
|
|
|
click.echo(crayons.yellow('Creating a Pipfile for this project...'), err=True)
|
|
|
|
# Create the pipfile if it doesn't exist.
|
|
project.create_pipfile()
|
|
|
|
# Validate the Pipfile's contents.
|
|
if validate and project.virtualenv_exists:
|
|
# Ensure that Pipfile is using proper casing.
|
|
p = project.parsed_pipfile
|
|
changed = ensure_proper_casing(pfile=p)
|
|
|
|
# Write changes out to disk.
|
|
if changed:
|
|
click.echo(crayons.yellow('Fixing package names in Pipfile...'), err=True)
|
|
project.write_toml(p)
|
|
|
|
|
|
def ensure_virtualenv(three=None, python=None):
|
|
"""Creates a virtualenv, if one doesn't exist."""
|
|
|
|
if not project.virtualenv_exists:
|
|
do_create_virtualenv(three=three, python=python)
|
|
|
|
# If --three, --two, or --python were passed...
|
|
elif (python) or (three is not None):
|
|
click.echo(crayons.red('Virtualenv already exists!'), err=True)
|
|
click.echo(crayons.yellow('Removing existing virtualenv...'), err=True)
|
|
|
|
# Remove the virtualenv.
|
|
shutil.rmtree(project.virtualenv_location)
|
|
|
|
# Call this function again.
|
|
ensure_virtualenv(three=three, python=python)
|
|
|
|
|
|
def ensure_project(three=None, python=None, validate=True):
|
|
"""Ensures both Pipfile and virtualenv exist for the project."""
|
|
ensure_pipfile(validate=validate)
|
|
ensure_virtualenv(three=three, python=python)
|
|
|
|
|
|
def ensure_proper_casing(pfile):
|
|
"""Ensures proper casing of Pipfile packages, writes changes to disk."""
|
|
|
|
casing_changed = proper_case_section(pfile.get('packages', {}))
|
|
casing_changed |= proper_case_section(pfile.get('dev-packages', {}))
|
|
|
|
return casing_changed
|
|
|
|
|
|
def proper_case_section(section):
|
|
"""Verify proper casing is retrieved, when available, for each
|
|
dependency in the section.
|
|
"""
|
|
# Casing for section
|
|
changed_values = False
|
|
unknown_names = [k for k in section.keys() if k not in set(project.proper_names)]
|
|
|
|
# Replace each package with proper casing.
|
|
for dep in unknown_names:
|
|
try:
|
|
# Get new casing for package name.
|
|
new_casing = proper_case(dep)
|
|
except IOError:
|
|
# Unable to normalize package name.
|
|
continue
|
|
|
|
if new_casing != dep:
|
|
changed_values = True
|
|
project.register_proper_name(new_casing)
|
|
|
|
# Replace old value with new value.
|
|
old_value = section[dep]
|
|
section[new_casing] = old_value
|
|
del section[dep]
|
|
|
|
# Return whether or not values have been changed.
|
|
return changed_values
|
|
|
|
|
|
def do_where(virtualenv=False, bare=True):
|
|
"""Executes the where functionality."""
|
|
|
|
if not virtualenv:
|
|
location = project.pipfile_location
|
|
|
|
if not location:
|
|
click.echo('No Pipfile present at project home. Consider running {0} first to automatically generate a Pipfile for you.'.format(crayons.green('`pipenv install`')), err=True)
|
|
elif not bare:
|
|
click.echo('Pipfile found at {0}. Considering this to be the project home.'.format(crayons.green(location)), err=True)
|
|
else:
|
|
click.echo(location)
|
|
|
|
else:
|
|
location = project.virtualenv_location
|
|
|
|
if not bare:
|
|
click.echo('Virtualenv location: {0}'.format(crayons.green(location)), err=True)
|
|
else:
|
|
click.echo(location)
|
|
|
|
|
|
def do_install_dependencies(dev=False, only=False, bare=False, requirements=False, allow_global=False):
|
|
""""Executes the install functionality."""
|
|
|
|
if requirements:
|
|
bare = True
|
|
|
|
# Load the Pipfile.
|
|
p = pipfile.load(project.pipfile_location)
|
|
|
|
# Load the lockfile if it exists, or if only is being used (e.g. lock is being used).
|
|
if only or not project.lockfile_exists:
|
|
if not bare:
|
|
click.echo(crayons.yellow('Installing dependencies from Pipfile...'))
|
|
lockfile = json.loads(p.lock())
|
|
else:
|
|
if not bare:
|
|
click.echo(crayons.yellow('Installing dependencies from Pipfile.lock...'))
|
|
with open(project.lockfile_location) as f:
|
|
lockfile = json.load(f)
|
|
|
|
# Install default dependencies, always.
|
|
deps = lockfile['default'] if not only else {}
|
|
|
|
# Add development deps if --dev was passed.
|
|
if dev:
|
|
deps.update(lockfile['develop'])
|
|
|
|
# Convert the deps to pip-compatible arguments.
|
|
deps_path = convert_deps_to_pip(deps)
|
|
|
|
# --requirements was passed.
|
|
if requirements:
|
|
with open(deps_path) as f:
|
|
click.echo(f.read())
|
|
sys.exit(0)
|
|
|
|
# pip install:
|
|
with spinner():
|
|
c = pip_install(r=deps_path, allow_global=allow_global)
|
|
|
|
if c.return_code != 0:
|
|
click.echo(crayons.red('An error occured while installing!'))
|
|
click.echo(crayons.blue(format_pip_error(c.err)))
|
|
sys.exit(c.return_code)
|
|
|
|
if not bare:
|
|
click.echo(crayons.blue(format_pip_output(c.out, r=deps_path)))
|
|
|
|
# Cleanup the temp requirements file.
|
|
if requirements:
|
|
os.remove(deps_path)
|
|
|
|
|
|
def do_download_dependencies(dev=False, only=False, bare=False):
|
|
""""Executes the download functionality."""
|
|
|
|
# Load the Pipfile.
|
|
p = pipfile.load(project.pipfile_location)
|
|
|
|
# Load the Pipfile.
|
|
if not bare:
|
|
click.echo(crayons.yellow('Downloading dependencies from Pipfile...'))
|
|
lockfile = json.loads(p.lock())
|
|
|
|
# Install default dependencies, always.
|
|
deps = lockfile['default'] if not only else {}
|
|
|
|
# Add development deps if --dev was passed.
|
|
if dev:
|
|
deps.update(lockfile['develop'])
|
|
|
|
# Convert the deps to pip-compatible arguments.
|
|
deps = convert_deps_to_pip(deps, r=False)
|
|
|
|
# Actually install each dependency into the virtualenv.
|
|
name_map = {}
|
|
for package_name in deps:
|
|
|
|
if not bare:
|
|
click.echo('Downloading {0}...'.format(crayons.green(package_name)))
|
|
|
|
# pip install:
|
|
c = pip_download(package_name)
|
|
|
|
if not bare:
|
|
click.echo(crayons.blue(c.out))
|
|
|
|
parsed_output = parse_install_output(c.out)
|
|
for filename, name in parsed_output:
|
|
name_map[filename] = name
|
|
|
|
return name_map
|
|
|
|
|
|
def parse_install_output(output):
|
|
"""Parse output from pip download to get name and file mappings
|
|
for all dependencies and their sub dependencies.
|
|
|
|
This is required for proper file hashing with --require-hashes.
|
|
"""
|
|
output_sections = output.split('Collecting ')
|
|
names = []
|
|
|
|
for section in output_sections:
|
|
lines = section.split('\n')
|
|
|
|
# Strip dependency parens from name line. e.g. package (from other_package)
|
|
name = lines[0].split('(')[0]
|
|
# Strip version specification. e.g. package; python-version=2.6
|
|
name = name.split(';')[0]
|
|
|
|
for line in lines:
|
|
r = parse.parse('Saved {file}', line.strip())
|
|
if r is None:
|
|
r = parse.parse('Using cached {file}', line.strip())
|
|
if r is None:
|
|
continue
|
|
|
|
fname = r['file'].split(os.sep)[-1]
|
|
# Unencode percent-encoded values like ``!`` in version number.
|
|
fname = requests.compat.unquote(fname)
|
|
|
|
names.append((fname, name.strip()))
|
|
break
|
|
|
|
return names
|
|
|
|
|
|
def do_create_virtualenv(three=None, python=None):
|
|
"""Creates a virtualenv."""
|
|
click.echo(crayons.yellow('Creating a virtualenv for this project...'), err=True)
|
|
|
|
# The user wants the virtualenv in the project.
|
|
if PIPENV_VENV_IN_PROJECT:
|
|
cmd = ['virtualenv', project.virtualenv_location, '--prompt=({0})'.format(project.name)]
|
|
else:
|
|
# Default: use pew.
|
|
cmd = ['pew', 'new', project.name, '-d']
|
|
|
|
# Pass a Python version to virtualenv, if needed.
|
|
if python:
|
|
click.echo('{0} {1} {2}'.format(crayons.yellow('Using'), crayons.red(python), crayons.yellow('to create virtualenv...')))
|
|
elif three is False:
|
|
python = 'python2'
|
|
elif three is True:
|
|
python = 'python3'
|
|
|
|
if python:
|
|
cmd = cmd + ['-p', python]
|
|
|
|
# Actually create the virtualenv.
|
|
with spinner():
|
|
c = delegator.run(cmd, block=False)
|
|
click.echo(crayons.blue(c.out), err=True)
|
|
|
|
# Say where the virtualenv is.
|
|
do_where(virtualenv=True, bare=False)
|
|
|
|
|
|
def is_version(text):
|
|
return re.match('^[\d]+\.[\d]+.*', text) or False
|
|
|
|
|
|
def parse_download_fname(fname, name):
|
|
fname, fextension = os.path.splitext(fname)
|
|
|
|
if fextension == '.whl':
|
|
fname = '-'.join(fname.split('-')[:-3])
|
|
|
|
if fname.endswith('.tar'):
|
|
fname, _ = os.path.splitext(fname)
|
|
|
|
# Substring out package name (plus dash) from file name to get version.
|
|
version = fname[len(name)+1:]
|
|
|
|
return version
|
|
|
|
|
|
def get_downloads_info(names_map, section):
|
|
info = []
|
|
|
|
p = project.parsed_pipfile
|
|
|
|
for fname in os.listdir(project.download_location):
|
|
# Get name from filename mapping.
|
|
name = list(convert_deps_from_pip(names_map[fname]))[0]
|
|
# Get the version info from the filenames.
|
|
version = parse_download_fname(fname, name)
|
|
|
|
# Get the hash of each file.
|
|
c = delegator.run('{0} hash {1}'.format(which_pip(), os.sep.join([project.download_location, fname])))
|
|
hash = c.out.split('--hash=')[1].strip()
|
|
|
|
# Verify we're adding the correct version from Pipfile
|
|
# and not one from a dependency.
|
|
specified_version = p[section].get(name, '')
|
|
if is_required_version(version, specified_version):
|
|
info.append(dict(name=name, version=version, hash=hash))
|
|
|
|
return info
|
|
|
|
|
|
def do_lock():
|
|
"""Executes the freeze functionality."""
|
|
|
|
# Purge the virtualenv download dir, for development dependencies.
|
|
do_purge(downloads=True, bare=True)
|
|
|
|
click.echo(crayons.yellow('Locking {0} dependencies...'.format(crayons.red('[dev-packages]'))))
|
|
|
|
with spinner():
|
|
# Install only development dependencies.
|
|
names_map = do_download_dependencies(dev=True, only=True, bare=True)
|
|
|
|
# Load the Pipfile and generate a lockfile.
|
|
p = pipfile.load(project.pipfile_location)
|
|
lockfile = json.loads(p.lock())
|
|
|
|
# Pip freeze development dependencies.
|
|
with spinner():
|
|
results = get_downloads_info(names_map, 'dev-packages')
|
|
|
|
# Clear generated lockfile before updating.
|
|
lockfile['develop'] = {}
|
|
|
|
# Add Development dependencies to lockfile.
|
|
for dep in results:
|
|
if dep:
|
|
lockfile['develop'].update({dep['name']: {'hash': dep['hash'], 'version': '=={0}'.format(dep['version'])}})
|
|
|
|
with spinner():
|
|
# Purge the virtualenv download dir, for default dependencies.
|
|
do_purge(downloads=True, bare=True)
|
|
|
|
click.echo(crayons.yellow('Locking {0} dependencies...'.format(crayons.red('[packages]'))))
|
|
|
|
with spinner():
|
|
# Install only development dependencies.
|
|
names_map = do_download_dependencies(bare=True)
|
|
|
|
# Pip freeze default dependencies.
|
|
results = get_downloads_info(names_map, 'packages')
|
|
|
|
# Clear generated lockfile before updating.
|
|
lockfile['default'] = {}
|
|
|
|
# Add default dependencies to lockfile.
|
|
for dep in results:
|
|
if dep:
|
|
lockfile['default'].update({dep['name']: {'hash': dep['hash'], 'version': '=={0}'.format(dep['version'])}})
|
|
|
|
# Write out lockfile.
|
|
with open(project.lockfile_location, 'w') as f:
|
|
f.write(json.dumps(lockfile, indent=4, separators=(',', ': ')))
|
|
|
|
# Purge the virtualenv download dir, for next time.
|
|
with spinner():
|
|
do_purge(downloads=True, bare=True)
|
|
|
|
click.echo('{0} Pipfile.lock{1}'.format(crayons.yellow('Updated'), crayons.yellow('!')))
|
|
|
|
|
|
def activate_virtualenv(source=True):
|
|
"""Returns the string to activate a virtualenv."""
|
|
|
|
# Suffix for other shells.
|
|
suffix = ''
|
|
|
|
# Support for fish shell.
|
|
if 'fish' in os.environ['SHELL']:
|
|
suffix = '.fish'
|
|
|
|
# Support for csh shell.
|
|
if 'csh' in os.environ['SHELL']:
|
|
suffix = '.csh'
|
|
|
|
# Escape any spaces located within the virtualenv path to allow
|
|
# for proper activation.
|
|
venv_location = project.virtualenv_location.replace(' ', r'\ ')
|
|
|
|
if source:
|
|
return 'source {0}/bin/activate{1}'.format(venv_location, suffix)
|
|
else:
|
|
return '{0}/bin/activate'.format(venv_location)
|
|
|
|
|
|
def do_activate_virtualenv(bare=False):
|
|
"""Executes the activate virtualenv functionality."""
|
|
# Check for environment marker, and skip if it's set.
|
|
if 'PIPENV_ACTIVE' not in os.environ:
|
|
if not bare:
|
|
click.echo('To activate this project\'s virtualenv, run the following:\n $ {0}'.format(
|
|
crayons.red('pipenv shell'))
|
|
)
|
|
else:
|
|
click.echo(activate_virtualenv())
|
|
|
|
|
|
def do_purge(bare=False, downloads=False, allow_global=False):
|
|
"""Executes the purge functionality."""
|
|
|
|
if downloads:
|
|
if not bare:
|
|
click.echo(crayons.yellow('Clearing out downloads directory...'))
|
|
shutil.rmtree(project.download_location)
|
|
return
|
|
|
|
freeze = delegator.run('{0} freeze'.format(which_pip(allow_global=allow_global))).out
|
|
installed = freeze.split()
|
|
|
|
# Remove setuptools and friends from installed, if present.
|
|
for package_name in ['setuptools', 'pip', 'wheel', 'six', 'packaging', 'pyparsing', 'appdirs']:
|
|
for i, package in enumerate(installed):
|
|
if package.startswith(package_name):
|
|
del installed[i]
|
|
|
|
if not bare:
|
|
click.echo('Found {0} installed package(s), purging...'.format(len(installed)))
|
|
command = '{0} uninstall {1} -y'.format(which_pip(allow_global=allow_global), ' '.join(installed))
|
|
c = delegator.run(command)
|
|
|
|
if not bare:
|
|
click.echo(crayons.blue(c.out))
|
|
|
|
click.echo(crayons.yellow('Environment now purged and fresh!'))
|
|
|
|
|
|
def do_init(dev=False, requirements=False, skip_virtualenv=False, allow_global=False):
|
|
"""Executes the init functionality."""
|
|
|
|
ensure_pipfile()
|
|
|
|
# Display where the Project is established.
|
|
if not requirements:
|
|
do_where(bare=False)
|
|
|
|
if not project.virtualenv_exists:
|
|
do_create_virtualenv()
|
|
|
|
# Write out the lockfile if it doesn't exist.
|
|
if project.lockfile_exists:
|
|
|
|
# Open the lockfile.
|
|
with codecs.open(project.lockfile_location, 'r') as f:
|
|
lockfile = json.load(f)
|
|
|
|
# Update the lockfile if it is out-of-date.
|
|
p = pipfile.load(project.pipfile_location)
|
|
|
|
# Check that the hash of the Lockfile matches the lockfile's hash.
|
|
if not lockfile['_meta'].get('hash', {}).get('sha256') == p.hash:
|
|
click.echo(crayons.red('Pipfile.lock out of date, updating...'), err=True)
|
|
|
|
do_lock()
|
|
|
|
# Write out the lockfile if it doesn't exist.
|
|
if not project.lockfile_exists:
|
|
click.echo(crayons.yellow('Pipfile.lock not found, creating...'), err=True)
|
|
do_lock()
|
|
|
|
do_install_dependencies(dev=dev, requirements=requirements, allow_global=allow_global)
|
|
|
|
# Activate virtualenv instructions.
|
|
do_activate_virtualenv()
|
|
|
|
|
|
def pip_install(package_name=None, r=None, allow_global=False):
|
|
# try installing for each source in project.sources
|
|
for source in project.sources:
|
|
if r:
|
|
c = delegator.run('{0} install -r {1} --require-hashes -i {2}'.format(which_pip(allow_global=allow_global), r, source['url']))
|
|
else:
|
|
c = delegator.run('{0} install "{1}" -i {2}'.format(which_pip(allow_global=allow_global), package_name, source['url']))
|
|
|
|
if c.return_code == 0:
|
|
break
|
|
# return the result of the first one that runs ok or the last one that didn't work
|
|
return c
|
|
|
|
|
|
def pip_download(package_name):
|
|
c = delegator.run('{0} download "{1}" -d {2}'.format(which_pip(), package_name, project.download_location))
|
|
return c
|
|
|
|
|
|
def which(command):
|
|
return os.sep.join([project.virtualenv_location] + ['bin/{0}'.format(command)])
|
|
|
|
|
|
def which_pip(allow_global=False):
|
|
"""Returns the location of virtualenv-installed pip."""
|
|
if allow_global:
|
|
return distutils.spawn.find_executable('pip')
|
|
|
|
return which('pip')
|
|
|
|
|
|
def proper_case(package_name):
|
|
|
|
# Capture tag contents here.
|
|
collected = []
|
|
|
|
class SimpleHTMLParser(HTMLParser):
|
|
def handle_data(self, data):
|
|
# Remove extra blank data from https://pypi.org/simple
|
|
data = data.strip()
|
|
if len(data) > 2:
|
|
collected.append(data)
|
|
|
|
# Hit the simple API.
|
|
r = requests.get('https://pypi.org/simple/{0}'.format(package_name))
|
|
if not r.ok:
|
|
raise IOError('Unable to find package {0} in PyPI repository.'.format(crayons.green(package_name)))
|
|
|
|
# Parse the HTML.
|
|
parser = SimpleHTMLParser()
|
|
parser.feed(r.text)
|
|
|
|
r = parse.parse('Links for {name}', collected[1])
|
|
good_name = r['name']
|
|
|
|
return good_name
|
|
|
|
|
|
def format_help(help):
|
|
"""Formats the help string."""
|
|
help = help.replace(' check', str(crayons.green(' check')))
|
|
help = help.replace(' uninstall', str(crayons.yellow(' uninstall', bold=True)))
|
|
help = help.replace(' install', str(crayons.yellow(' install', bold=True)))
|
|
help = help.replace(' lock', str(crayons.red(' lock', bold=True)))
|
|
help = help.replace(' run', str(crayons.blue(' run')))
|
|
help = help.replace(' shell', str(crayons.blue(' shell', bold=True)))
|
|
help = help.replace(' update', str(crayons.yellow(' update')))
|
|
|
|
additional_help = """
|
|
Usage Examples:
|
|
Create a new project using Python 3:
|
|
$ {0}
|
|
|
|
Install all dependencies for a project (including dev):
|
|
$ {1}
|
|
|
|
Create a lockfile:
|
|
$ {2}
|
|
|
|
Commands:""".format(
|
|
crayons.red('pipenv --three'),
|
|
crayons.red('pipenv install --dev'),
|
|
crayons.red('pipenv lock')
|
|
)
|
|
|
|
help = help.replace('Commands:', additional_help)
|
|
|
|
return help
|
|
|
|
|
|
def format_pip_error(error):
|
|
error = error.replace('Expected', str(crayons.green('Expected', bold=True)))
|
|
error = error.replace('Got', str(crayons.red('Got', bold=True)))
|
|
error = error.replace('THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE', str(crayons.red('THESE PACKAGES DO NOT MATCH THE HASHES FROM Pipfile.lock!', bold=True)))
|
|
error = error.replace('someone may have tampered with them', str(crayons.red('someone may have tampered with them')))
|
|
return error
|
|
|
|
|
|
def format_pip_output(out, r=None):
|
|
def gen(out):
|
|
for line in out.split('\n'):
|
|
# Remove requirements file information from pip output.
|
|
if '(from -r' in line:
|
|
yield line[:line.index('(from -r')]
|
|
else:
|
|
yield line
|
|
|
|
out = '\n'.join([l for l in gen(out)])
|
|
return out
|
|
|
|
|
|
# |\/| /\ |) [- ]3 `/
|
|
# . . .-. . . . . .-. .-. . . .-. .-. .-. .-. .-.
|
|
# |< |- |\| |\| |- | |-| |( |- | | /
|
|
# ' ` `-' ' ` ' ` `-' ' ' ` ' ' `-' `-' ' `-'
|
|
|
|
def easter_egg(package_name):
|
|
if package_name in ['requests', 'maya', 'crayons', 'delegator.py' 'records', 'tablib']:
|
|
click.echo(u'P.S. You have excellent taste! ✨ 🍰 ✨')
|
|
|
|
|
|
@click.group(invoke_without_command=True)
|
|
@click.option('--where', is_flag=True, default=False, help="Output project home information.")
|
|
@click.option('--venv', is_flag=True, default=False, help="Output virtualenv information.")
|
|
@click.option('--rm', is_flag=True, default=False, help="Remove the virtualenv.")
|
|
@click.option('--bare', is_flag=True, default=False, help="Minimal output.")
|
|
@click.option('--three/--two', is_flag=True, default=None, help="Use Python 3/2 when creating virtualenv.")
|
|
@click.option('--python', default=False, nargs=1, help="Specify which version of Python virtualenv should use.")
|
|
@click.option('--help', '-h', is_flag=True, default=None, help="Show this message then exit.")
|
|
@click.version_option(prog_name=crayons.yellow('pipenv'), version=__version__)
|
|
@click.pass_context
|
|
def cli(ctx, where=False, venv=False, rm=False, bare=False, three=False, python=False, help=False):
|
|
|
|
if ctx.invoked_subcommand is None:
|
|
# --where was passed...
|
|
if where:
|
|
do_where(bare=bare)
|
|
sys.exit(0)
|
|
|
|
# --venv was passed...
|
|
elif venv:
|
|
|
|
with spinner():
|
|
loc = project.virtualenv_location
|
|
|
|
# There is no virtualenv yet.
|
|
if not project.virtualenv_exists:
|
|
click.echo(crayons.red('No virtualenv has been created for this project yet!'), err=True)
|
|
sys.exit(1)
|
|
else:
|
|
click.echo(project.virtualenv_location)
|
|
sys.exit(0)
|
|
|
|
# --rm was passed...
|
|
elif rm:
|
|
|
|
with spinner():
|
|
loc = project.virtualenv_location
|
|
|
|
if project.virtualenv_exists:
|
|
click.echo(crayons.yellow('{0} ({1})...'.format(crayons.yellow('Removing virtualenv'), crayons.green(loc))))
|
|
with spinner():
|
|
# Remove the virtualenv.
|
|
shutil.rmtree(project.virtualenv_location)
|
|
sys.exit(0)
|
|
else:
|
|
click.echo(crayons.red('No virtualenv has been created for this project yet!'), err=True)
|
|
sys.exit(1)
|
|
|
|
# --two / --three was passed...
|
|
if python or three is not None:
|
|
ensure_project(three=three, python=python)
|
|
|
|
else:
|
|
# Display help to user, if no commands were passed.
|
|
click.echo(format_help(ctx.get_help()))
|
|
|
|
|
|
@click.command(help="Installs provided packages and adds them to Pipfile, or (if none is given), installs all packages.")
|
|
@click.argument('package_name', default=False)
|
|
@click.argument('more_packages', nargs=-1)
|
|
@click.option('--dev', '-d', is_flag=True, default=False, help="Install package(s) in [dev-packages].")
|
|
@click.option('--three/--two', is_flag=True, default=None, help="Use Python 3/2 when creating virtualenv.")
|
|
@click.option('--python', default=False, nargs=1, help="Specify which version of Python virtualenv should use.")
|
|
@click.option('--system', is_flag=True, default=False, help="System pip management.")
|
|
@click.option('--lock', is_flag=True, default=False, help="Lock afterwards.")
|
|
def install(package_name=False, more_packages=False, dev=False, three=False, python=False, system=False, lock=False):
|
|
|
|
# Ensure that virtualenv is available.
|
|
ensure_project(three=three, python=python)
|
|
|
|
# Allow more than one package to be provided.
|
|
package_names = (package_name,) + more_packages
|
|
|
|
# Install all dependencies, if none was provided.
|
|
if package_name is False:
|
|
click.echo(crayons.yellow('No package provided, installing all dependencies.'), err=True)
|
|
do_init(dev=dev, allow_global=system)
|
|
sys.exit(0)
|
|
|
|
for package_name in package_names:
|
|
click.echo('Installing {0}...'.format(crayons.green(package_name)))
|
|
|
|
# pip install:
|
|
with spinner():
|
|
c = pip_install(package_name, allow_global=system)
|
|
|
|
click.echo(crayons.blue(format_pip_output(c.out)))
|
|
|
|
# TODO: This
|
|
# Ensure that package was successfully installed.
|
|
try:
|
|
assert c.return_code == 0
|
|
except AssertionError:
|
|
click.echo('{0} An error occurred while installing {1}!'.format(crayons.red('Error: '), crayons.green(package_name)))
|
|
click.echo(crayons.blue(format_pip_error(c.err)))
|
|
sys.exit(1)
|
|
|
|
if dev:
|
|
click.echo('Adding {0} to Pipfile\'s {1}...'.format(crayons.green(package_name), crayons.red('[dev-packages]')))
|
|
else:
|
|
click.echo('Adding {0} to Pipfile\'s {1}...'.format(crayons.green(package_name), crayons.red('[packages]')))
|
|
|
|
# Add the package to the Pipfile.
|
|
project.add_package_to_pipfile(package_name, dev)
|
|
|
|
# Ego boost.
|
|
easter_egg(package_name)
|
|
|
|
if lock:
|
|
do_lock()
|
|
|
|
|
|
@click.command(help="Un-installs a provided package and removes it from Pipfile, or (if none is given), un-installs all packages.")
|
|
@click.argument('package_name', default=False)
|
|
@click.argument('more_packages', nargs=-1)
|
|
@click.option('--three/--two', is_flag=True, default=None, help="Use Python 3/2 when creating virtualenv.")
|
|
@click.option('--python', default=False, nargs=1, help="Specify which version of Python virtualenv should use.")
|
|
@click.option('--system', is_flag=True, default=False, help="System pip management.")
|
|
@click.option('--lock', is_flag=True, default=False, help="Lock afterwards.")
|
|
@click.option('--dev', '-d', is_flag=True, default=False, help="Un-install all package from [dev-packages].")
|
|
@click.option('--all', is_flag=True, default=False, help="Purge all package(s) from virtualenv. Does not edit Pipfile.")
|
|
def uninstall(package_name=False, more_packages=False, three=None, python=False, system=False, lock=False, dev=False, all=False):
|
|
# Ensure that virtualenv is available.
|
|
ensure_project(three=three, python=python)
|
|
|
|
package_names = (package_name,) + more_packages
|
|
pipfile_remove = True
|
|
|
|
# Un-install all dependencies, if --all was provided.
|
|
if all is True:
|
|
if not dev:
|
|
click.echo(crayons.yellow('Un-installing all packages from virtualenv...'))
|
|
do_purge(allow_global=system)
|
|
sys.exit(0)
|
|
|
|
# Uninstall [dev-packages], if --dev was provided.
|
|
if dev:
|
|
if 'dev-packages' in project.parsed_pipfile:
|
|
click.echo(crayons.yellow('Un-installing {0}...'.format(crayons.red('[dev-packages]'))))
|
|
package_names = project.parsed_pipfile['dev-packages']
|
|
pipfile_remove = False
|
|
else:
|
|
click.echo(crayons.yellow('No {0} to uninstall.'.format(crayons.red('[dev-packages]'))))
|
|
sys.exit(0)
|
|
|
|
if package_name is False and not dev:
|
|
click.echo(crayons.red('No package provided!'))
|
|
sys.exit(1)
|
|
|
|
for package_name in package_names:
|
|
|
|
click.echo('Un-installing {0}...'.format(crayons.green(package_name)))
|
|
|
|
c = delegator.run('{0} uninstall {1} -y'.format(which_pip(allow_global=system), package_name))
|
|
click.echo(crayons.blue(c.out))
|
|
|
|
if pipfile_remove:
|
|
if dev:
|
|
click.echo('Removing {0} from Pipfile\'s {1}...'.format(crayons.green(package_name), crayons.red('[dev-packages]')))
|
|
else:
|
|
click.echo('Removing {0} from Pipfile\'s {1}...'.format(crayons.green(package_name), crayons.red('[packages]')))
|
|
|
|
project.remove_package_from_pipfile(package_name, dev)
|
|
|
|
if lock:
|
|
do_lock()
|
|
|
|
|
|
@click.command(help="Generates Pipfile.lock.")
|
|
@click.option('--three/--two', is_flag=True, default=None, help="Use Python 3/2 when creating virtualenv.")
|
|
@click.option('--python', default=False, nargs=1, help="Specify which version of Python virtualenv should use.")
|
|
@click.option('--requirements', '-r', is_flag=True, default=False, help="Just generate a requirements.txt. Only works with bare install command.")
|
|
def lock(three=None, python=False, requirements=False):
|
|
# Ensure that virtualenv is available.
|
|
ensure_project(three=three, python=python)
|
|
|
|
if requirements:
|
|
do_init(dev=True, requirements=requirements)
|
|
do_lock()
|
|
|
|
|
|
@click.command(help="Spawns a shell within the virtualenv.", context_settings=dict(
|
|
ignore_unknown_options=True,
|
|
allow_extra_args=True
|
|
))
|
|
@click.option('--three/--two', is_flag=True, default=None, help="Use Python 3/2 when creating virtualenv.")
|
|
@click.option('--python', default=False, nargs=1, help="Specify which version of Python virtualenv should use.")
|
|
@click.option('--compat', '-c', is_flag=True, default=False, help="Run in shell compatibility mode (for misconfigured shells).")
|
|
@click.argument('shell_args', nargs=-1)
|
|
def shell(three=None, python=False, compat=False, shell_args=None):
|
|
# Ensure that virtualenv is available.
|
|
ensure_project(three=three, python=python, validate=False)
|
|
|
|
# Set an environment variable, so we know we're in the environment.
|
|
os.environ['PIPENV_ACTIVE'] = '1'
|
|
|
|
# Support shell compatibility mode.
|
|
if PIPENV_SHELL_COMPAT:
|
|
compat = True
|
|
|
|
# Compatibility mode:
|
|
if compat:
|
|
try:
|
|
shell = os.environ['SHELL']
|
|
except KeyError:
|
|
click.echo(crayons.red('Windows is not currently supported.'))
|
|
sys.exit(1)
|
|
|
|
click.echo(crayons.yellow('Spawning environment shell ({0}).'.format(crayons.red(shell))))
|
|
|
|
cmd = "{0} -i'".format(shell)
|
|
args = []
|
|
|
|
# Standard (properly configured shell) mode:
|
|
else:
|
|
cmd = 'pew'
|
|
args = ["workon", project.name]
|
|
|
|
# Grab current terminal dimensions to replace the hardcoded default
|
|
# dimensions of pexpect
|
|
terminal_dimensions = get_terminal_size()
|
|
|
|
c = pexpect.spawn(
|
|
cmd,
|
|
args,
|
|
dimensions=(
|
|
terminal_dimensions.lines,
|
|
terminal_dimensions.columns
|
|
)
|
|
)
|
|
|
|
# Activate the virtualenv if in compatibility mode.
|
|
if compat:
|
|
c.sendline(activate_virtualenv())
|
|
|
|
# Send additional arguments to the subshell.
|
|
if shell_args:
|
|
c.sendline(' '.join(shell_args))
|
|
|
|
# Handler for terminal resizing events
|
|
# Must be defined here to have the shell process in its context, since we
|
|
# can't pass it as an argument
|
|
def sigwinch_passthrough(sig, data):
|
|
terminal_dimensions = get_terminal_size()
|
|
c.setwinsize(terminal_dimensions.lines, terminal_dimensions.columns)
|
|
signal.signal(signal.SIGWINCH, sigwinch_passthrough)
|
|
|
|
# Interact with the new shell.
|
|
c.interact()
|
|
c.close()
|
|
sys.exit(c.exitstatus)
|
|
|
|
|
|
@click.command(help="Spawns a command installed into the virtualenv.", context_settings=dict(
|
|
ignore_unknown_options=True,
|
|
allow_extra_args=True
|
|
))
|
|
@click.argument('command')
|
|
@click.argument('args', nargs=-1)
|
|
@click.option('--no-interactive', is_flag=True, default=False, help="Run the command in non-interactive mode.")
|
|
@click.option('--three/--two', is_flag=True, default=None, help="Use Python 3/2 when creating virtualenv.")
|
|
@click.option('--python', default=False, nargs=1, help="Specify which version of Python virtualenv should use.")
|
|
def run(command, args, no_interactive=False, three=None, python=False):
|
|
# Ensure that virtualenv is available.
|
|
ensure_project(three=three, python=python, validate=False)
|
|
|
|
# Spawn the new process, and interact with it.
|
|
try:
|
|
c = pexpect.spawn(which(command), list(args))
|
|
except pexpect.exceptions.ExceptionPexpect:
|
|
click.echo(crayons.red('The command ({0}) was not found within the virtualenv!'.format(which(command))))
|
|
sys.exit(1)
|
|
|
|
# Interact with the new shell.
|
|
if no_interactive:
|
|
c.block()
|
|
else:
|
|
c.interact()
|
|
c.close()
|
|
sys.exit(c.exitstatus)
|
|
|
|
|
|
@click.command(help="Checks PEP 508 markers provided in Pipfile.")
|
|
@click.option('--three/--two', is_flag=True, default=None, help="Use Python 3/2 when creating virtualenv.")
|
|
@click.option('--python', default=False, nargs=1, help="Specify which version of Python virtualenv should use.")
|
|
def check(three=None, python=False):
|
|
# Ensure that virtualenv is available.
|
|
ensure_project(three=three, python=python, validate=False)
|
|
|
|
click.echo(crayons.yellow('Checking PEP 508 requirements...'))
|
|
|
|
# Run the PEP 508 checker in the virtualenv.
|
|
c = delegator.run('{0} {1}'.format(which('python'), pep508checker.__file__.rstrip('cdo')))
|
|
results = json.loads(c.out)
|
|
|
|
# Load the pipfile.
|
|
p = pipfile.Pipfile.load(project.pipfile_location)
|
|
|
|
failed = False
|
|
# Assert each specified requirement.
|
|
for marker, specifier in p.data['_meta']['requires'].items():
|
|
|
|
if marker in results:
|
|
try:
|
|
assert results[marker] == specifier
|
|
except AssertionError:
|
|
failed = True
|
|
click.echo('Specifier {0} does not match {1} ({2}).'.format(crayons.green(marker), crayons.blue(specifier), crayons.red(results[marker])))
|
|
if failed:
|
|
click.echo(crayons.red('Failed!'))
|
|
sys.exit(1)
|
|
else:
|
|
click.echo(crayons.green('Passed!'))
|
|
|
|
|
|
@click.command(help="Updates pip to latest version, uninstalls all packages, and re-installs them to latest compatible versions.")
|
|
@click.option('--dev', '-d', is_flag=True, default=False, help="Install package(s) in [dev-packages].")
|
|
@click.option('--three/--two', is_flag=True, default=None, help="Use Python 3/2 when creating virtualenv.")
|
|
@click.option('--python', default=False, nargs=1, help="Specify which version of Python virtualenv should use.")
|
|
def update(dev=False, three=None, python=None):
|
|
|
|
# Ensure that virtualenv is available.
|
|
ensure_project(three=three, python=python, validate=False)
|
|
|
|
# Update pip to latest version.
|
|
ensure_latest_pip()
|
|
|
|
click.echo(crayons.yellow('Updating all dependencies from Pipfile...'))
|
|
|
|
do_purge()
|
|
do_init(dev=dev)
|
|
|
|
click.echo(crayons.yellow('All dependencies are now up-to-date!'))
|
|
|
|
|
|
# Install click commands.
|
|
cli.add_command(install)
|
|
cli.add_command(uninstall)
|
|
cli.add_command(update)
|
|
cli.add_command(lock)
|
|
cli.add_command(check)
|
|
cli.add_command(shell)
|
|
cli.add_command(run)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
cli()
|