Files
pipenv/pipenv/cli.py
T
2017-02-02 13:02:21 -05:00

995 lines
34 KiB
Python

# -*- coding: utf-8 -*-
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.
if PIPENV_NOSPIN:
import contextlib
@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:
# Ensure that Pipfile is using proper casing.
ensure_proper_casing()
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():
"""Ensures proper casing of Pipfile packages, writes changes to disk."""
p = project.parsed_pipfile
def proper_case_section(section):
# Casing for section
casing_changed = False
if section in p:
changed_values = []
# Replace each package with proper casing.
for dep in p[section].keys():
# Attempt to normalize name from PyPI.
# Use provided name if better one can't be found.
try:
# Get new casing for package name.
new_casing = proper_case(dep)
except IOError:
# Unable to normalize package name.
continue
if new_casing == dep:
continue
# Mark casing as changed, if it did.
casing_changed = True
changed_values.append((new_casing, dep))
for new, old in changed_values:
# Replace old value with new value.
old_value = p[section][old]
p[section][new] = old_value
del p[section][old]
return casing_changed
casing_changed = proper_case_section('packages')
casing_changed |= proper_case_section('dev-packages')
if casing_changed:
click.echo(crayons.yellow('Fixing package names in Pipfile...'), err=True)
# Write pipfile out to disk.
project.write(p)
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, 'r') 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, 'r') 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)))
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]
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(crayons.yellow('Using {0} to create virtualenv...'.format(python)))
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):
fname, fextension = os.path.splitext(fname)
if fextension == '.whl':
fname = '-'.join(fname.split('-')[:-3])
if fname.endswith('.tar'):
fname, _ = os.path.splitext(fname)
fname_components = fname.split('-')
for n, component in enumerate(fname_components):
if is_version(component):
# Return what's left when we find start of version.
return '-'.join(fname_components[n:])
return ''
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)
# 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.
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.
do_purge(downloads=True, bare=True)
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):
if r:
c = delegator.run('{0} install -r {1} --require-hashes -i {2}'.format(which_pip(allow_global=allow_global), r, project.source['url']))
else:
c = delegator.run('{0} install "{1}" -i {2}'.format(which_pip(allow_global=allow_global), package_name, project.source['url']))
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):
# Skip checking proper-case if it's already a good name.
if package_name in project.proper_names:
return 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('{0}/{1}'.format(project.source['url'], 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'].strip()
# Register the good name for future reference.
project.register_proper_name(good_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('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('--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, 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)
# --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:
# Proper-case incoming package name (check against API).
old_name = [k for k in convert_deps_from_pip(package_name).keys()][0]
try:
new_name = proper_case(old_name)
except IOError as e:
click.echo('{0} {1}'.format(crayons.red('Error: '), e.args[0], crayons.green(package_name)))
continue
package_name = package_name.replace(old_name, new_name)
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 package(s) 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('--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, 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.
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()