diff --git a/pipenv/cli.py b/pipenv/cli.py index 47366402..08f48a43 100644 --- a/pipenv/cli.py +++ b/pipenv/cli.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- import os import sys -from click import ( + +from pipenv.patched import crayons +from pipenv.vendor import click_completion +from pipenv.vendor.click import ( argument, command, echo, - edit, group, Group, option, @@ -14,29 +16,24 @@ from click import ( version_option, BadParameter, ) -from click_didyoumean import DYMCommandCollection - -import click_completion -import crayons -import delegator - -from .__version__ import __version__ +from pipenv.vendor.click_didyoumean import DYMCommandCollection from . import environments +from .__version__ import __version__ from .utils import is_valid_url # Enable shell completion. click_completion.init() + CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) class PipenvGroup(Group): - """Custom Group class provides formatted main help""" - + """Custom Group class provides formatted main help. + """ def get_help_option(self, ctx): - from .core import format_help - - """Override for showing formatted main help via --help and -h options""" + """Show formatted main help via --help and -h options. + """ help_options = self.get_help_option_names(ctx) if not help_options or not self.add_help_option: return @@ -45,6 +42,7 @@ class PipenvGroup(Group): if value and not ctx.resilient_parsing: if not ctx.invoked_subcommand: # legit main help + from .operations.help import format_help echo(format_help(ctx.get_help())) else: # legit sub-command help @@ -76,7 +74,9 @@ def validate_python_path(ctx, param, value): # we'll report absolute paths which do not exist: if isinstance(value, (str, bytes)): if os.path.isabs(value) and not os.path.isfile(value): - raise BadParameter('Expected Python at path %s does not exist' % value) + raise BadParameter( + 'Expected Python at path {} does not exist'.format(value), + ) return value @@ -165,120 +165,52 @@ def cli( completion=False, ): if completion: # Handle this ASAP to make shell startup fast. - from . import shells - try: - shell = shells.detect_info()[0] - except shells.ShellDetectionFailure: - echo( - 'Fail to detect shell. Please provide the {0} environment ' - 'variable.'.format(crayons.normal('PIPENV_SHELL', bold=True)), - err=True, - ) - sys.exit(1) - print(click_completion.get_code(shell=shell, prog_name='pipenv')) - sys.exit(0) - - from .core import ( - do_py, - warn_in_virtualenv, - project, - format_help - ) + from .operations.options import do_completion + do_completion() + return if man: - from .utils import system_which - if system_which('man'): - path = os.sep.join([os.path.dirname(__file__), 'pipenv.1']) - os.execle(system_which('man'), 'man', path, os.environ) - else: - echo( - 'man does not appear to be available on your system.', err=True - ) + from .operations.options import do_man + do_man() + return if envs: - echo( - 'The following environment variables can be set, to do various things:\n' - ) - for key in environments.__dict__: - if key.startswith('PIPENV'): - echo(' - {0}'.format(crayons.normal(key, bold=True))) - echo( - '\nYou can learn more at:\n {0}'.format( - crayons.green( - 'http://docs.pipenv.org/advanced/#configuration-with-environment-variables' - ) - ) - ) - sys.exit(0) + from .operations.options import do_envs + do_envs() + return + + from .operations.options import warn_in_virtualenv warn_in_virtualenv() - if ctx.invoked_subcommand is None: - # --where was passed... - if where: - from .operations.where import do_where - do_where(bare=True) - sys.exit(0) - elif py: - do_py() - sys.exit() - # --venv was passed... - elif venv: - # There is no virtualenv yet. - if not project.virtualenv_exists: - echo( - crayons.red( - 'No virtualenv has been created for this project yet!' - ), - err=True, - ) - sys.exit(1) - else: - echo(project.virtualenv_location) - sys.exit(0) - # --rm was passed... - elif rm: - # Abort if --system (or running in a virtualenv). - if environments.PIPENV_USE_SYSTEM: - echo( - crayons.red( - 'You are attempting to remove a virtualenv that ' - 'Pipenv did not create. Aborting.' - ) - ) - sys.exit(1) - if project.virtualenv_exists: - loc = project.virtualenv_location - echo( - crayons.normal( - u'{0} ({1})…'.format( - crayons.normal('Removing virtualenv', bold=True), - crayons.green(loc), - ) - ) - ) - # Remove the virtualenv. - # TODO: Where can I better put this import? pipenv.ui? - from .operations._utils import spinner - with spinner(): - from .operations.virtualenv import cleanup_virtualenv - cleanup_virtualenv(bare=True) - sys.exit(0) - else: - echo( - crayons.red( - 'No virtualenv has been created for this project yet!', - bold=True, - ), - err=True, - ) - sys.exit(1) - # --two / --three was passed... - if (python or three is not None) or site_packages: - from .operations.ensure import ensure_project - ensure_project( - three=three, python=python, warn=True, site_packages=site_packages - ) - # Check this again before exiting for empty ``pipenv`` command. - elif ctx.invoked_subcommand is None: - # Display help to user, if no commands were passed. - echo(format_help(ctx.get_help())) + + # Pre-hook for subcommands. + if ctx.invoked_subcommand is not None: + # --two / --three was passed... + if (python or three is not None) or site_packages: + from .operations.ensure import ensure_project + ensure_project( + three=three, python=python, warn=True, + site_packages=site_packages, + ) + return + + if where: + from .operations.where import do_where + do_where(bare=True) + return + if py: + from .operations.options import do_py + do_py() + return + if venv: + from .operations.options import do_venv + do_venv() + return + if rm: + from .operations.options import do_rm + do_rm() + return + + # Display help to user if nothing were passed. + from .operations.help import format_help + echo(format_help(ctx.get_help())) @command( @@ -486,7 +418,7 @@ def uninstall( keep_outdated=False, pypi_mirror=None, ): - from .core import do_uninstall + from .operations.uninstall import do_uninstall do_uninstall( package_name=package_name, @@ -569,12 +501,15 @@ def lock( pre=False, keep_outdated=False, ): - from .core import ensure_project, do_init, do_lock - # Ensure that virtualenv is available. + from .operations.ensure import ensure_project ensure_project(three=three, python=python) + if requirements: + from .operations.init import do_init do_init(dev=dev, requirements=requirements, pypi_mirror=pypi_mirror) + + from .operations.lock import do_lock do_lock( verbose=verbose, clear=clear, pre=pre, keep_outdated=keep_outdated, pypi_mirror=pypi_mirror ) @@ -613,7 +548,6 @@ def lock( def shell( three=None, python=False, fancy=False, shell_args=None, anyway=False ): - from .core import load_dot_env, do_shell # Prevent user from activating nested environments. if 'PIPENV_ACTIVE' in os.environ: # If PIPENV_ACTIVE is set, VIRTUAL_ENV should always be set too. @@ -630,11 +564,8 @@ def shell( err=True, ) sys.exit(1) - # Load .env file. - load_dot_env() - # Use fancy mode for Windows. - if os.name == 'nt': - fancy = True + + from .operations.shell import do_shell do_shell( three=three, python=python, fancy=fancy, shell_args=shell_args ) @@ -711,7 +642,7 @@ def check( ignore=None, args=None, ): - from .core import do_check + from .operations.check import do_check do_check( three=three, python=python, @@ -791,9 +722,7 @@ def check( help=u"List out-of-date dependencies.", ) @argument('package', default=False) -@pass_context def update( - ctx, three=None, python=False, pypi_mirror=None, @@ -810,59 +739,13 @@ def update( outdated=False, more_packages=None, ): - from .core import ( - ensure_project, - do_outdated, - do_lock, - do_sync, - ensure_lockfile, - do_install, - project, - ) - - ensure_project(three=three, python=python, warn=True) - if not outdated: - outdated = bool(dry_run) - if outdated: - do_outdated(pypi_mirror=pypi_mirror) - if not package: - echo( - '{0} {1} {2} {3}{4}'.format( - crayons.white('Running', bold=True), - crayons.red('$ pipenv lock', bold=True), - crayons.white('then', bold=True), - crayons.red('$ pipenv sync', bold=True), - crayons.white('.', bold=True), - ) - ) - else: - for package in ([package] + list(more_packages) or []): - if package not in project.all_packages: - echo( - '{0}: {1} was not found in your Pipfile! Aborting.' - ''.format( - crayons.red('Warning', bold=True), - crayons.green(package, bold=True), - ), - err=True, - ) - sys.exit(1) - do_lock( - verbose=verbose, clear=clear, pre=pre, keep_outdated=keep_outdated, pypi_mirror=pypi_mirror - ) - do_sync( - ctx=ctx, - dev=dev, - three=three, - python=python, - bare=bare, - dont_upgrade=False, - user=False, - verbose=verbose, - clear=clear, - unused=False, - sequential=sequential, - pypi_mirror=pypi_mirror, + from .operations.update import do_update + do_update( + package, list(more_packages) if more_packages else [], + three=three, python=python, + pypi_mirror=pypi_mirror, verbose=verbose, clear=clear, + keep_outdated=keep_outdated, pre=pre, dev=dev, bare=bare, + sequential=sequential, dry_run=dry_run, outdated=outdated, ) @@ -876,9 +759,8 @@ def update( '--reverse', is_flag=True, default=False, help="Reversed dependency graph." ) def graph(bare=False, json=False, json_tree=False, reverse=False): - from .core import do_graph - - do_graph(bare=bare, json=json, json_tree=json_tree, reverse=reverse) + from .operations.graph import do_graph + do_graph(bare=bare, json_=json, json_tree=json_tree, reverse=reverse) @command(short_help="View a given module in your editor.", name="open") @@ -897,29 +779,8 @@ def graph(bare=False, json=False, json_tree=False, reverse=False): ) @argument('module', nargs=1) def run_open(module, three=None, python=None): - from .core import which, ensure_project - - # Ensure that virtualenv is available. - ensure_project(three=three, python=python, validate=False) - c = delegator.run( - '{0} -c "import {1}; print({1}.__file__);"'.format( - which('python'), module - ) - ) - try: - assert c.return_code == 0 - except AssertionError: - echo(crayons.red('Module not found!')) - sys.exit(1) - if '__init__.py' in c.out: - p = os.path.dirname(c.out.strip().rstrip('cdo')) - else: - p = c.out.strip().rstrip('cdo') - echo( - crayons.normal('Opening {0!r} in your EDITOR.'.format(p), bold=True) - ) - edit(filename=p) - sys.exit(0) + from .operations.open import do_open + do_open(module, three=three, python=python) @command(short_help="Installs all packages specified in Pipfile.lock.") @@ -968,9 +829,7 @@ def run_open(module, three=None, python=None): default=False, help="Install dependencies one-at-a-time, instead of concurrently.", ) -@pass_context def sync( - ctx, dev=False, three=None, python=None, @@ -984,10 +843,8 @@ def sync( sequential=False, pypi_mirror=None, ): - from .core import do_sync - + from .operations.sync import do_sync do_sync( - ctx=ctx, dev=dev, three=three, python=python, @@ -1032,9 +889,7 @@ def sync( default=False, help="Just output unneeded packages.", ) -@pass_context def clean( - ctx, three=None, python=None, dry_run=False, @@ -1042,10 +897,9 @@ def clean( user=False, verbose=False, ): - from .core import do_clean - + from .operations.clean import do_clean do_clean( - ctx=ctx, three=three, python=python, dry_run=dry_run, verbose=verbose + three=three, python=python, dry_run=dry_run, verbose=verbose ) @@ -1061,6 +915,7 @@ cli.add_command(shell) cli.add_command(run) cli.add_command(update) cli.add_command(run_open) + # Only invoke the "did you mean" when an argument wasn't passed (it breaks those). if '-' not in ''.join(sys.argv) and len(sys.argv) > 1: cli = DYMCommandCollection(sources=[cli]) diff --git a/pipenv/core.py b/pipenv/core.py index d29b450d..1967c4fd 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -1,48 +1,8 @@ -# -*- coding=utf-8 -*- import os import sys -import shutil -import signal -import json as simplejson - -import click -import crayons -import delegator -from .vendor import pexpect -import pipfile from .project import Project -from .vendor.requirementslib import Requirement -from .utils import ( - is_required_version, - pep423_name, - escape_grouped_arguments, - find_windows_executable, - temp_environ, - fs_str, -) -from ._compat import ( - Path -) -from . import pep508checker -from .environments import ( - PIPENV_SHELL_FANCY, - PIPENV_USE_SYSTEM, - PIPENV_SHELL, - PIPENV_CACHE_DIR, -) - -# Backport required for earlier versions of Python. -if sys.version_info < (3, 3): - from .vendor.backports.shutil_get_terminal_size import get_terminal_size -else: - from shutil import get_terminal_size - - -# ###################3 I PLAN TO KEEP THESE HERE. ######################### - - -from .utils import system_which +from .utils import find_windows_executable, system_which # Packages that should be ignored later. @@ -94,736 +54,3 @@ def which_pip(allow_global=False): project = Project(which=which) - - -# ########################################################################### - - -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:] - # Ignore implicit post releases in version number. - if '-' in version and version.split('-')[1].isdigit(): - version = version.split('-')[0] - 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 = Requirement.from_line(names_map[fname]).name - # Get the version info from the filenames. - version = parse_download_fname(fname, name) - # Get the hash of each file. - cmd = '{0} hash "{1}"'.format( - escape_grouped_arguments(which_pip()), - os.sep.join([project.download_location, fname]), - ) - c = delegator.run(cmd) - 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 activate_virtualenv(source=True): - """Returns the string to activate a virtualenv.""" - # Suffix and source command for other shells. - suffix = '' - command = ' .' if source else '' - # Support for fish shell. - if PIPENV_SHELL and 'fish' in PIPENV_SHELL: - suffix = '.fish' - command = 'source' - # Support for csh shell. - if PIPENV_SHELL and 'csh' in PIPENV_SHELL: - suffix = '.csh' - command = 'source' - # Escape any spaces located within the virtualenv path to allow - # for proper activation. - venv_location = project.virtualenv_location.replace(' ', r'\ ') - if source: - return '{2} {0}/bin/activate{1}'.format(venv_location, suffix, command) - - else: - return '{0}/bin/activate'.format(venv_location) - - -def do_purge(bare=False, downloads=False, allow_global=False, verbose=False): - """Executes the purge functionality.""" - if downloads: - if not bare: - click.echo( - crayons.normal(u'Clearing out downloads directory...', bold=True) - ) - shutil.rmtree(project.download_location) - return - - freeze = delegator.run( - '{0} freeze'.format( - escape_grouped_arguments(which_pip(allow_global=allow_global)) - ) - ).out - # Remove comments from the output, if any. - installed = [ - line - for line in freeze.splitlines() - if not line.lstrip().startswith('#') - ] - # Remove setuptools and friends from installed, if present. - for package_name in BAD_PACKAGES: - for i, package in enumerate(installed): - if package.startswith(package_name): - del installed[i] - actually_installed = [] - for package in installed: - try: - dep = Requirement.from_line(package) - except AssertionError: - dep = None - if dep and not dep.is_vcs and not dep.editable: - dep = dep.name - actually_installed.append(dep) - if not bare: - click.echo( - u'Found {0} installed package(s), purging...'.format( - len(actually_installed) - ) - ) - command = '{0} uninstall {1} -y'.format( - escape_grouped_arguments(which_pip(allow_global=allow_global)), - ' '.join(actually_installed), - ) - if verbose: - click.echo('$ {0}'.format(command)) - c = delegator.run(command) - if not bare: - click.echo(crayons.blue(c.out)) - click.echo(crayons.green('Environment now purged and fresh!')) - - -def pip_download(package_name): - cache_dir = Path(PIPENV_CACHE_DIR) - pip_config = { - 'PIP_CACHE_DIR': fs_str(cache_dir.as_posix()), - 'PIP_WHEEL_DIR': fs_str(cache_dir.joinpath('wheels').as_posix()), - 'PIP_DESTINATION_DIR': fs_str(cache_dir.joinpath('pkgs').as_posix()), - } - for source in project.sources: - cmd = '{0} download "{1}" -i {2} -d {3}'.format( - escape_grouped_arguments(which_pip()), - package_name, - source['url'], - project.download_location, - ) - c = delegator.run(cmd, env=pip_config) - if c.return_code == 0: - break - - return c - - -def format_help(help): - """Formats the help string.""" - help = help.replace('Options:', str(crayons.normal('Options:', bold=True))) - help = help.replace( - 'Usage: pipenv', - str('Usage: {0}'.format(crayons.normal('pipenv', bold=True))), - ) - help = help.replace(' check', str(crayons.red(' check', bold=True))) - help = help.replace(' clean', str(crayons.red(' clean', bold=True))) - help = help.replace(' graph', str(crayons.red(' graph', bold=True))) - help = help.replace( - ' install', str(crayons.magenta(' install', bold=True)) - ) - help = help.replace(' lock', str(crayons.green(' lock', bold=True))) - help = help.replace(' open', str(crayons.red(' open', bold=True))) - help = help.replace(' run', str(crayons.yellow(' run', bold=True))) - help = help.replace(' shell', str(crayons.yellow(' shell', bold=True))) - help = help.replace(' sync', str(crayons.green(' sync', bold=True))) - help = help.replace( - ' uninstall', str(crayons.magenta(' uninstall', bold=True)) - ) - help = help.replace(' update', str(crayons.green(' update', bold=True))) - additional_help = """ -Usage Examples: - Create a new project using Python 3.6, specifically: - $ {1} - - Install all dependencies for a project (including dev): - $ {2} - - Create a lockfile containing pre-releases: - $ {6} - - Show a graph of your installed dependencies: - $ {4} - - Check your installed dependencies for security vulnerabilities: - $ {7} - - Install a local setup.py into your virtual environment/Pipfile: - $ {5} - - Use a lower-level pip command: - $ {8} - -Commands:""".format( - crayons.red('pipenv --three'), - crayons.red('pipenv --python 3.6'), - crayons.red('pipenv install --dev'), - crayons.red('pipenv lock'), - crayons.red('pipenv graph'), - crayons.red('pipenv install -e .'), - crayons.red('pipenv lock --pre'), - crayons.red('pipenv check'), - crayons.red('pipenv run pip freeze'), - ) - help = help.replace('Commands:', additional_help) - return help - - -def warn_in_virtualenv(): - if PIPENV_USE_SYSTEM: - # Only warn if pipenv isn't already active. - if 'PIPENV_ACTIVE' not in os.environ: - click.echo( - '{0}: Pipenv found itself running within a virtual environment, ' - 'so it will automatically use that environment, instead of ' - 'creating its own for any project. You can set ' - '{1} to force pipenv to ignore that environment and create ' - 'its own instead.'.format( - crayons.green('Courtesy Notice'), - crayons.normal('PIPENV_IGNORE_VIRTUALENVS=1', bold=True), - ), - err=True, - ) - - -def ensure_lockfile(keep_outdated=False, pypi_mirror=None): - """Ensures that the lockfile is up-to-date.""" - if not keep_outdated: - keep_outdated = project.settings.get('keep_outdated') - # Write out the lockfile if it doesn't exist, but not if the Pipfile is being ignored - if project.lockfile_exists: - old_hash = project.get_lockfile_hash() - new_hash = project.calculate_pipfile_hash() - if new_hash != old_hash: - click.echo( - crayons.red( - u'Pipfile.lock ({0}) out of date, updating to ({1})...'.format( - old_hash[-6:], new_hash[-6:] - ), - bold=True, - ), - err=True, - ) - do_lock(keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) - else: - do_lock(keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) - - -def do_py(system=False): - try: - click.echo(which('python', allow_global=system)) - except AttributeError: - click.echo(crayons.red('No project found!')) - - -def do_outdated(pypi_mirror=None): - packages = {} - results = delegator.run('{0} freeze'.format(which('pip'))).out.strip( - ).split( - '\n' - ) - results = filter(bool, results) - for result in results: - dep = Requirement.from_line(result) - packages.update(dep.as_pipfile()) - updated_packages = {} - lockfile = do_lock(write=False, pypi_mirror=pypi_mirror) - for section in ('develop', 'default'): - for package in lockfile[section]: - try: - updated_packages[package] = lockfile[section][package][ - 'version' - ] - except KeyError: - pass - outdated = [] - for package in packages: - norm_name = pep423_name(package) - if norm_name in updated_packages: - if updated_packages[norm_name] != packages[package]: - outdated.append( - (package, updated_packages[norm_name], packages[package]) - ) - for package, new_version, old_version in outdated: - click.echo( - 'Package {0!r} out-of-date: {1!r} installed, {2!r} available.'.format( - package, old_version, new_version - ) - ) - sys.exit(bool(outdated)) - - -def do_uninstall( - package_name=False, - more_packages=False, - three=None, - python=False, - system=False, - lock=False, - all_dev=False, - all=False, - verbose=False, - keep_outdated=False, - pypi_mirror=None, -): - # Automatically use an activated virtualenv. - if PIPENV_USE_SYSTEM: - system = True - # 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: - click.echo( - crayons.normal( - u'Un-installing all packages from virtualenv...', bold=True - ) - ) - do_purge(allow_global=system, verbose=verbose) - sys.exit(0) - # Uninstall [dev-packages], if --dev was provided. - if all_dev: - if 'dev-packages' not in project.parsed_pipfile: - click.echo( - crayons.normal( - 'No {0} to uninstall.'.format( - crayons.red('[dev-packages]') - ), - bold=True, - ) - ) - sys.exit(0) - click.echo( - crayons.normal( - u'Un-installing {0}...'.format(crayons.red('[dev-packages]')), - bold=True, - ) - ) - package_names = project.dev_packages.keys() - if package_name is False and not all_dev: - click.echo(crayons.red('No package provided!'), err=True) - sys.exit(1) - for package_name in package_names: - click.echo(u'Un-installing {0}...'.format(crayons.green(package_name))) - cmd = '{0} uninstall {1} -y'.format( - escape_grouped_arguments(which_pip(allow_global=system)), - package_name, - ) - if verbose: - click.echo('$ {0}'.format(cmd)) - c = delegator.run(cmd) - click.echo(crayons.blue(c.out)) - if pipfile_remove: - in_packages = project.get_package_name_in_pipfile( - package_name, dev=False) - in_dev_packages = project.get_package_name_in_pipfile( - package_name, dev=True) - if not in_dev_packages and not in_packages: - click.echo( - 'No package {0} to remove from Pipfile.'.format( - crayons.green(package_name) - ) - ) - continue - - click.echo( - u'Removing {0} from Pipfile...'.format( - crayons.green(package_name) - ) - ) - # Remove package from both packages and dev-packages. - project.remove_package_from_pipfile(package_name, dev=True) - project.remove_package_from_pipfile(package_name, dev=False) - if lock: - do_lock(system=system, keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) - - -def do_shell(three=None, python=False, fancy=False, shell_args=None): - from .patched.pew import pew - - # 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' - compat = (not fancy) - # Support shell compatibility mode. - if PIPENV_SHELL_FANCY: - compat = False - # Compatibility mode: - if compat: - if PIPENV_SHELL: - shell = os.path.abspath(PIPENV_SHELL) - else: - click.echo( - crayons.red( - 'Please ensure that the {0} environment variable ' - 'is set before activating shell.'.format( - crayons.normal('SHELL', bold=True) - ) - ), - err=True, - ) - sys.exit(1) - click.echo( - crayons.normal( - 'Spawning environment shell ({0}). Use {1} to leave.'.format( - crayons.red(shell), crayons.normal("'exit'", bold=True) - ), - bold=True, - ), - err=True, - ) - cmd = "{0} -i'".format(shell) - args = [] - # Standard (properly configured shell) mode: - else: - if project.is_venv_in_project(): - # use .venv as the target virtualenv name - workon_name = '.venv' - else: - workon_name = project.virtualenv_name - cmd = sys.executable - args = ['-m', 'pipenv.pew', 'workon', workon_name] - # Grab current terminal dimensions to replace the hardcoded default - # dimensions of pexpect - terminal_dimensions = get_terminal_size() - try: - with temp_environ(): - if project.is_venv_in_project(): - os.environ['WORKON_HOME'] = project.project_directory - c = pexpect.spawn( - cmd, - args, - dimensions=( - terminal_dimensions.lines, terminal_dimensions.columns - ), - ) - # Windows! - except AttributeError: - # import subprocess - # Tell pew to use the project directory as its workon_home - with temp_environ(): - if project.is_venv_in_project(): - os.environ['WORKON_HOME'] = project.project_directory - pew.workon_cmd([workon_name]) - sys.exit(0) - # 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(escape_character=None) - c.close() - sys.exit(c.exitstatus) - - -def do_check(three=None, python=False, system=False, unused=False, ignore=None, args=None): - if not system: - # Ensure that virtualenv is available. - ensure_project(three=three, python=python, validate=False, warn=False) - if not args: - args = [] - if unused: - deps_required = [k for k in project.packages.keys()] - deps_needed = import_from_code(unused) - for dep in deps_needed: - try: - deps_required.remove(dep) - except ValueError: - pass - if deps_required: - click.echo( - crayons.normal( - 'The following dependencies appear unused, and may be safe for removal:' - ) - ) - for dep in deps_required: - click.echo(' - {0}'.format(crayons.green(dep))) - sys.exit(1) - else: - sys.exit(0) - click.echo(crayons.normal(u'Checking PEP 508 requirements...', bold=True)) - if system: - python = system_which('python') - else: - python = which('python') - # Run the PEP 508 checker in the virtualenv. - c = delegator.run( - '"{0}" {1}'.format( - python, - escape_grouped_arguments(pep508checker.__file__.rstrip('cdo')), - ) - ) - results = simplejson.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]), - ), - err=True, - ) - if failed: - click.echo(crayons.red('Failed!'), err=True) - sys.exit(1) - else: - click.echo(crayons.green('Passed!')) - click.echo( - crayons.normal(u'Checking installed package safety...', bold=True) - ) - path = pep508checker.__file__.rstrip('cdo') - path = os.sep.join(__file__.split(os.sep)[:-1] + ['patched', 'safety.zip']) - if not system: - python = which('python') - else: - python = system_which('python') - if ignore: - ignored = '--ignore {0}'.format('--ignore '.join(ignore)) - click.echo(crayons.normal('Notice: Ignoring CVE(s) {0}'.format(crayons.yellow(', '.join(ignore)))), err=True) - else: - ignored = '' - c = delegator.run( - '"{0}" {1} check --json --key=1ab8d58f-5122e025-83674263-bc1e79e0 {2}'.format( - python, escape_grouped_arguments(path), ignored - ) - ) - try: - results = simplejson.loads(c.out) - except ValueError: - click.echo('An error occurred:', err=True) - click.echo(c.err, err=True) - sys.exit(1) - for (package, resolved, installed, description, vuln) in results: - click.echo( - '{0}: {1} {2} resolved ({3} installed)!'.format( - crayons.normal(vuln, bold=True), - crayons.green(package), - crayons.red(resolved, bold=False), - crayons.red(installed, bold=True), - ) - ) - click.echo('{0}'.format(description)) - click.echo() - if not results: - click.echo(crayons.green('All good!')) - else: - sys.exit(1) - - -def do_graph(bare=False, json=False, json_tree=False, reverse=False): - import pipdeptree - try: - python_path = which('python') - except AttributeError: - click.echo( - u'{0}: {1}'.format( - crayons.red('Warning', bold=True), - u'Unable to display currently-installed dependency graph information here. ' - u'Please run within a Pipenv project.', - ), - err=True, - ) - sys.exit(1) - if reverse and json: - click.echo( - u'{0}: {1}'.format( - crayons.red('Warning', bold=True), - u'Using both --reverse and --json together is not supported. ' - u'Please select one of the two options.', - ), - err=True, - ) - sys.exit(1) - if reverse and json_tree: - click.echo( - u'{0}: {1}'.format( - crayons.red('Warning', bold=True), - u'Using both --reverse and --json-tree together is not supported. ' - u'Please select one of the two options.', - ), - err=True, - ) - sys.exit(1) - if json and json_tree: - click.echo( - u'{0}: {1}'.format( - crayons.red('Warning', bold=True), - u'Using both --json and --json-tree together is not supported. ' - u'Please select one of the two options.', - ), - err=True, - ) - sys.exit(1) - flag = '' - if json: - flag = '--json' - if json_tree: - flag = '--json-tree' - if reverse: - flag = '--reverse' - if not project.virtualenv_exists: - click.echo( - u'{0}: No virtualenv has been created for this project yet! Consider ' - u'running {1} first to automatically generate one for you or see' - u'{2} for further instructions.'.format( - crayons.red('Warning', bold=True), - crayons.green('`pipenv install`'), - crayons.green('`pipenv install --help`'), - ), - err=True, - ) - sys.exit(1) - cmd = '"{0}" {1} {2}'.format( - python_path, - escape_grouped_arguments(pipdeptree.__file__.rstrip('cdo')), - flag, - ) - # Run dep-tree. - c = delegator.run(cmd) - if not bare: - if json: - data = [] - for d in simplejson.loads(c.out): - if d['package']['key'] not in BAD_PACKAGES: - data.append(d) - click.echo(simplejson.dumps(data, indent=4)) - sys.exit(0) - elif json_tree: - def traverse(obj): - if isinstance(obj, list): - return [traverse(package) for package in obj if package['key'] not in BAD_PACKAGES] - else: - obj['dependencies'] = traverse(obj['dependencies']) - return obj - data = traverse(simplejson.loads(c.out)) - click.echo(simplejson.dumps(data, indent=4)) - sys.exit(0) - else: - for line in c.out.split('\n'): - # Ignore bad packages as top level. - if line.split('==')[0] in BAD_PACKAGES and not reverse: - continue - - # Bold top-level packages. - if not line.startswith(' '): - click.echo(crayons.normal(line, bold=True)) - # Echo the rest. - else: - click.echo(crayons.normal(line, bold=False)) - else: - click.echo(c.out) - if c.return_code != 0: - click.echo( - '{0} {1}'.format( - crayons.red('ERROR: ', bold=True), - crayons.white('{0}'.format(c.err, bold=True)), - ), - err=True - ) - # Return its return code. - sys.exit(c.return_code) - - -def do_clean( - ctx, three=None, python=None, dry_run=False, bare=False, verbose=False, pypi_mirror=None -): - # Ensure that virtualenv is available. - ensure_project(three=three, python=python, validate=False) - ensure_lockfile(pypi_mirror=pypi_mirror) - - installed_package_names = [] - pip_freeze_command = delegator.run('{0} freeze'.format(which_pip())) - for line in pip_freeze_command.out.split('\n'): - installed = line.strip() - if not installed or installed.startswith('#'): # Comment or empty. - continue - r = Requirement.from_line(installed).requirement - # Ignore editable installations. - if not r.editable: - installed_package_names.append(r.name.lower()) - else: - if verbose: - click.echo('Ignoring {0}.'.format(repr(r.name)), err=True) - # Remove known "bad packages" from the list. - for bad_package in BAD_PACKAGES: - if bad_package in installed_package_names: - if verbose: - click.echo('Ignoring {0}.'.format(repr(bad_package)), err=True) - del installed_package_names[ - installed_package_names.index(bad_package) - ] - # Intelligently detect if --dev should be used or not. - develop = [k.lower() for k in project.lockfile_content['develop'].keys()] - default = [k.lower() for k in project.lockfile_content['default'].keys()] - for used_package in set(develop + default): - if used_package in installed_package_names: - del installed_package_names[ - installed_package_names.index(used_package) - ] - failure = False - for apparent_bad_package in installed_package_names: - if dry_run: - click.echo(apparent_bad_package) - else: - click.echo( - crayons.white( - 'Uninstalling {0}...'.format(repr(apparent_bad_package)), - bold=True, - ) - ) - # Uninstall the package. - c = delegator.run( - '{0} uninstall {1} -y'.format( - which_pip(), apparent_bad_package - ) - ) - if c.return_code != 0: - failure = True - sys.exit(int(failure)) diff --git a/pipenv/operations/_utils.py b/pipenv/operations/_utils.py index 32a56b15..2879684a 100644 --- a/pipenv/operations/_utils.py +++ b/pipenv/operations/_utils.py @@ -1,12 +1,17 @@ import contextlib +import os from pipenv.patched import crayons -from pipenv.vendor import blindspin +from pipenv.vendor import blindspin, click +from pipenv.core import BAD_PACKAGES, project from pipenv.environments import ( PIPENV_COLORBLIND, + PIPENV_DONT_LOAD_ENV, + PIPENV_DOTENV_LOCATION, PIPENV_NOSPIN, ) +from pipenv.utils import proper_case # Disable colors, for the color blind and others who do not prefer colors. @@ -44,3 +49,36 @@ def convert_deps_to_pip(deps, project=None, r=True, include_index=False): f.write('\n'.join(dependencies).encode('utf-8')) f.close() return f.name + + +def load_dot_env(): + """Loads .env file into sys.environ. + """ + if not PIPENV_DONT_LOAD_ENV: + # If the project doesn't exist yet, check current directory for a .env file + from pipenv.vendor import dotenv + project_directory = project.project_directory or '.' + denv = dotenv.find_dotenv( + PIPENV_DOTENV_LOCATION or os.sep.join([project_directory, '.env']) + ) + if os.path.isfile(denv): + click.echo( + crayons.normal( + 'Loading .env environment variables...', bold=True + ), + err=True, + ) + dotenv.load_dotenv(denv, override=True) + + +def import_from_code(path='.'): + from pipreqs import pipreqs + rs = [] + try: + for r in pipreqs.get_all_imports(path): + if r not in BAD_PACKAGES: + rs.append(r) + pkg_names = pipreqs.get_pkg_names(rs) + return [proper_case(r) for r in pkg_names] + except Exception: + return [] diff --git a/pipenv/operations/check.py b/pipenv/operations/check.py new file mode 100644 index 00000000..306b70c2 --- /dev/null +++ b/pipenv/operations/check.py @@ -0,0 +1,117 @@ +import json +import os +import sys + +from pipenv.patched import crayons, pipfile +from pipenv.vendor import click, delegator + +from pipenv import pep508checker +from pipenv.core import project, which +from pipenv.utils import escape_grouped_arguments, system_which + +from ._utils import import_from_code +from .ensure import ensure_project + + +def do_check(three=None, python=False, system=False, unused=False, ignore=None, args=None): + if not system: + # Ensure that virtualenv is available. + ensure_project(three=three, python=python, validate=False, warn=False) + if not args: + args = [] + if unused: + deps_required = [k for k in project.packages.keys()] + deps_needed = import_from_code(unused) + for dep in deps_needed: + try: + deps_required.remove(dep) + except ValueError: + pass + if deps_required: + click.echo( + crayons.normal( + 'The following dependencies appear unused, and may be safe for removal:' + ) + ) + for dep in deps_required: + click.echo(' - {0}'.format(crayons.green(dep))) + sys.exit(1) + else: + sys.exit(0) + click.echo(crayons.normal(u'Checking PEP 508 requirements...', bold=True)) + if system: + python = system_which('python') + else: + python = which('python') + # Run the PEP 508 checker in the virtualenv. + c = delegator.run( + '"{0}" {1}'.format( + python, + escape_grouped_arguments(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]), + ), + err=True, + ) + if failed: + click.echo(crayons.red('Failed!'), err=True) + sys.exit(1) + else: + click.echo(crayons.green('Passed!')) + click.echo( + crayons.normal(u'Checking installed package safety...', bold=True) + ) + path = pep508checker.__file__.rstrip('cdo') + path = os.sep.join(__file__.split(os.sep)[:-1] + ['patched', 'safety.zip']) + if not system: + python = which('python') + else: + python = system_which('python') + if ignore: + ignored = '--ignore {0}'.format('--ignore '.join(ignore)) + click.echo(crayons.normal('Notice: Ignoring CVE(s) {0}'.format(crayons.yellow(', '.join(ignore)))), err=True) + else: + ignored = '' + c = delegator.run( + '"{0}" {1} check --json --key=1ab8d58f-5122e025-83674263-bc1e79e0 {2}'.format( + python, escape_grouped_arguments(path), ignored + ) + ) + try: + results = json.loads(c.out) + except ValueError: + click.echo('An error occurred:', err=True) + click.echo(c.err, err=True) + sys.exit(1) + for (package, resolved, installed, description, vuln) in results: + click.echo( + '{0}: {1} {2} resolved ({3} installed)!'.format( + crayons.normal(vuln, bold=True), + crayons.green(package), + crayons.red(resolved, bold=False), + crayons.red(installed, bold=True), + ) + ) + click.echo('{0}'.format(description)) + click.echo() + if not results: + click.echo(crayons.green('All good!')) + else: + sys.exit(1) diff --git a/pipenv/operations/clean.py b/pipenv/operations/clean.py new file mode 100644 index 00000000..9ad6bd59 --- /dev/null +++ b/pipenv/operations/clean.py @@ -0,0 +1,66 @@ +import sys + +from pipenv.patched import crayons +from pipenv.vendor import click, delegator, requirementslib + +from pipenv.core import BAD_PACKAGES, project, which_pip + +from .ensure import ensure_project, ensure_lockfile + + +def do_clean( + three=None, python=None, dry_run=False, bare=False, verbose=False, +): + # Ensure that virtualenv is available. + ensure_project(three=three, python=python, validate=False) + ensure_lockfile() + + installed_package_names = [] + pip_freeze_command = delegator.run('{0} freeze'.format(which_pip())) + for line in pip_freeze_command.out.split('\n'): + installed = line.strip() + if not installed or installed.startswith('#'): # Comment or empty. + continue + r = requirementslib.Requirement.from_line(installed).requirement + # Ignore editable installations. + if not r.editable: + installed_package_names.append(r.name.lower()) + else: + if verbose: + click.echo('Ignoring {0}.'.format(repr(r.name)), err=True) + # Remove known "bad packages" from the list. + for bad_package in BAD_PACKAGES: + if bad_package in installed_package_names: + if verbose: + click.echo('Ignoring {0}.'.format(repr(bad_package)), err=True) + del installed_package_names[ + installed_package_names.index(bad_package) + ] + # Intelligently detect if --dev should be used or not. + develop = [k.lower() for k in project.lockfile_content['develop'].keys()] + default = [k.lower() for k in project.lockfile_content['default'].keys()] + for used_package in set(develop + default): + if used_package in installed_package_names: + del installed_package_names[ + installed_package_names.index(used_package) + ] + failure = False + for apparent_bad_package in installed_package_names: + if dry_run: + click.echo(apparent_bad_package) + else: + click.echo( + crayons.white( + 'Uninstalling {0}...'.format(repr(apparent_bad_package)), + bold=True, + ) + ) + # Uninstall the package. + c = delegator.run( + '{0} uninstall {1} -y'.format( + which_pip(), apparent_bad_package + ) + ) + if c.return_code != 0: + failure = True + sys.exit(int(failure)) diff --git a/pipenv/operations/ensure.py b/pipenv/operations/ensure.py index 5e88f99f..f2d33629 100644 --- a/pipenv/operations/ensure.py +++ b/pipenv/operations/ensure.py @@ -14,6 +14,7 @@ from pipenv.core import ( from pipenv.environments import ( PIPENV_SKIP_VALIDATION, PIPENV_USE_SYSTEM, + PIPENV_VIRTUALENV, PIPENV_YES, ) from pipenv.utils import get_python_executable_version @@ -242,3 +243,26 @@ def ensure_project( sys.exit(1) # Ensure the Pipfile exists. ensure_pipfile(validate=validate, skip_requirements=skip_requirements, system=system) + + +def ensure_lockfile(keep_outdated=False, pypi_mirror=None): + """Ensures that the lockfile is up-to-date.""" + if not keep_outdated: + keep_outdated = project.settings.get('keep_outdated') + # Write out the lockfile if it doesn't exist, but not if the Pipfile is being ignored + if project.lockfile_exists: + old_hash = project.get_lockfile_hash() + new_hash = project.calculate_pipfile_hash() + if new_hash == old_hash: + return + click.echo( + crayons.red( + u'Pipfile.lock ({0}) out of date, updating to ({1})...'.format( + old_hash[-6:], new_hash[-6:] + ), + bold=True, + ), + err=True, + ) + from .lock import do_lock + do_lock(keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) diff --git a/pipenv/operations/graph.py b/pipenv/operations/graph.py new file mode 100644 index 00000000..1ef99cda --- /dev/null +++ b/pipenv/operations/graph.py @@ -0,0 +1,122 @@ +import json +import sys + +from pipenv.patched import crayons +from pipenv.vendor import click, delegator + +from pipenv.core import BAD_PACKAGES, project, which +from pipenv.utils import escape_grouped_arguments + + +def do_graph(bare=False, json_=False, json_tree=False, reverse=False): + import pipdeptree + try: + python_path = which('python') + except AttributeError: + click.echo( + u'{0}: {1}'.format( + crayons.red('Warning', bold=True), + u'Unable to display currently-installed dependency graph information here. ' + u'Please run within a Pipenv project.', + ), + err=True, + ) + sys.exit(1) + if reverse and json_: + click.echo( + u'{0}: {1}'.format( + crayons.red('Warning', bold=True), + u'Using both --reverse and --json together is not supported. ' + u'Please select one of the two options.', + ), + err=True, + ) + sys.exit(1) + if reverse and json_tree: + click.echo( + u'{0}: {1}'.format( + crayons.red('Warning', bold=True), + u'Using both --reverse and --json-tree together is not supported. ' + u'Please select one of the two options.', + ), + err=True, + ) + sys.exit(1) + if json_ and json_tree: + click.echo( + u'{0}: {1}'.format( + crayons.red('Warning', bold=True), + u'Using both --json and --json-tree together is not supported. ' + u'Please select one of the two options.', + ), + err=True, + ) + sys.exit(1) + flag = '' + if json_: + flag = '--json' + if json_tree: + flag = '--json-tree' + if reverse: + flag = '--reverse' + if not project.virtualenv_exists: + click.echo( + u'{0}: No virtualenv has been created for this project yet! Consider ' + u'running {1} first to automatically generate one for you or see' + u'{2} for further instructions.'.format( + crayons.red('Warning', bold=True), + crayons.green('`pipenv install`'), + crayons.green('`pipenv install --help`'), + ), + err=True, + ) + sys.exit(1) + cmd = '"{0}" {1} {2}'.format( + python_path, + escape_grouped_arguments(pipdeptree.__file__.rstrip('cdo')), + flag, + ) + # Run dep-tree. + c = delegator.run(cmd) + if not bare: + if json_: + data = [] + for d in json.loads(c.out): + if d['package']['key'] not in BAD_PACKAGES: + data.append(d) + click.echo(json.dumps(data, indent=4)) + sys.exit(0) + elif json_tree: + def traverse(obj): + if isinstance(obj, list): + return [traverse(package) for package in obj if package['key'] not in BAD_PACKAGES] + else: + obj['dependencies'] = traverse(obj['dependencies']) + return obj + data = traverse(json.loads(c.out)) + click.echo(json.dumps(data, indent=4)) + sys.exit(0) + else: + for line in c.out.split('\n'): + # Ignore bad packages as top level. + if line.split('==')[0] in BAD_PACKAGES and not reverse: + continue + + # Bold top-level packages. + if not line.startswith(' '): + click.echo(crayons.normal(line, bold=True)) + # Echo the rest. + else: + click.echo(crayons.normal(line, bold=False)) + else: + click.echo(c.out) + if c.return_code != 0: + click.echo( + '{0} {1}'.format( + crayons.red('ERROR: ', bold=True), + crayons.white('{0}'.format(c.err, bold=True)), + ), + err=True + ) + # Return its return code. + sys.exit(c.return_code) diff --git a/pipenv/operations/help.py b/pipenv/operations/help.py new file mode 100644 index 00000000..759f8209 --- /dev/null +++ b/pipenv/operations/help.py @@ -0,0 +1,61 @@ +from pipenv.patched import crayons + + +def format_help(help): + """Formats the help string.""" + help = help.replace('Options:', str(crayons.normal('Options:', bold=True))) + help = help.replace( + 'Usage: pipenv', + str('Usage: {0}'.format(crayons.normal('pipenv', bold=True))), + ) + help = help.replace(' check', str(crayons.red(' check', bold=True))) + help = help.replace(' clean', str(crayons.red(' clean', bold=True))) + help = help.replace(' graph', str(crayons.red(' graph', bold=True))) + help = help.replace( + ' install', str(crayons.magenta(' install', bold=True)) + ) + help = help.replace(' lock', str(crayons.green(' lock', bold=True))) + help = help.replace(' open', str(crayons.red(' open', bold=True))) + help = help.replace(' run', str(crayons.yellow(' run', bold=True))) + help = help.replace(' shell', str(crayons.yellow(' shell', bold=True))) + help = help.replace(' sync', str(crayons.green(' sync', bold=True))) + help = help.replace( + ' uninstall', str(crayons.magenta(' uninstall', bold=True)) + ) + help = help.replace(' update', str(crayons.green(' update', bold=True))) + additional_help = """ +Usage Examples: + Create a new project using Python 3.6, specifically: + $ {1} + + Install all dependencies for a project (including dev): + $ {2} + + Create a lockfile containing pre-releases: + $ {6} + + Show a graph of your installed dependencies: + $ {4} + + Check your installed dependencies for security vulnerabilities: + $ {7} + + Install a local setup.py into your virtual environment/Pipfile: + $ {5} + + Use a lower-level pip command: + $ {8} + +Commands:""".format( + crayons.red('pipenv --three'), + crayons.red('pipenv --python 3.6'), + crayons.red('pipenv install --dev'), + crayons.red('pipenv lock'), + crayons.red('pipenv graph'), + crayons.red('pipenv install -e .'), + crayons.red('pipenv lock --pre'), + crayons.red('pipenv check'), + crayons.red('pipenv run pip freeze'), + ) + help = help.replace('Commands:', additional_help) + return help diff --git a/pipenv/operations/install.py b/pipenv/operations/install.py index b822c61d..2692e0d3 100644 --- a/pipenv/operations/install.py +++ b/pipenv/operations/install.py @@ -6,13 +6,12 @@ from pipenv.patched import crayons from pipenv.vendor import click, requirementslib from pipenv._compat import TemporaryDirectory -from pipenv.core import BAD_PACKAGES, project +from pipenv.core import project from pipenv.environments import PIPENV_USE_SYSTEM from pipenv.utils import ( download_file, is_star, is_valid_url, - proper_case, ) from ._install import ( @@ -21,25 +20,11 @@ from ._install import ( pip_install, split_argument, ) -from ._utils import convert_deps_to_pip, spinner +from ._utils import convert_deps_to_pip, import_from_code, spinner from .ensure import ensure_project, import_requirements from .init import do_init -def _import_from_code(path='.'): - from pipreqs import pipreqs - rs = [] - try: - for r in pipreqs.get_all_imports(path): - if r not in BAD_PACKAGES: - rs.append(r) - pkg_names = pipreqs.get_pkg_names(rs) - return [proper_case(r) for r in pkg_names] - - except Exception: - return [] - - def do_install( package_name=False, more_packages=False, @@ -179,7 +164,7 @@ def do_install( u'Discovering imports from local codebase...', bold=True ) ) - for req in _import_from_code(code): + for req in import_from_code(code): click.echo(' Found {0}!'.format(crayons.green(req))) project.add_package_to_pipfile(req) # Capture -e argument and assign it to following package_name. diff --git a/pipenv/operations/lock.py b/pipenv/operations/lock.py index 2a790c81..932cbf78 100644 --- a/pipenv/operations/lock.py +++ b/pipenv/operations/lock.py @@ -1,3 +1,4 @@ +import json import os import sys @@ -7,8 +8,9 @@ except ImportError: from collections import Mapping from pipenv.patched import crayons -from pipenv.vendor import click, delegator, six +from pipenv.vendor import click, delegator, requirementslib, six +from pipenv._compat import Path, TemporaryDirectory from pipenv.core import project, which, which_pip from pipenv.utils import ( escape_grouped_arguments, @@ -25,13 +27,68 @@ def _is_pinned(val): return isinstance(val, six.string_types) and val.startswith('==') +def _obtain_vcs_req(vcs_obj, src_dir, name, rev=None): + target_dir = os.path.join(src_dir, name) + target_rev = vcs_obj.make_rev_options(rev) + if not os.path.exists(target_dir): + vcs_obj.obtain(target_dir) + if not vcs_obj.is_commit_id_equal(target_dir, rev) and not vcs_obj.is_commit_id_equal(target_dir, target_rev): + vcs_obj.update(target_dir, target_rev) + return vcs_obj.get_revision(target_dir) + + +def _get_vcs_deps( + project, + pip_freeze=None, + which=None, + verbose=False, + clear=False, + pre=False, + allow_global=False, + dev=False, + pypi_mirror=None, +): + from pipenv.patched.notpip._internal.vcs import VcsSupport + + section = "vcs_dev_packages" if dev else "vcs_packages" + reqs = [] + lockfile = {} + try: + packages = getattr(project, section) + except AttributeError: + return [], [] + if not os.environ.get("PIP_SRC") and not project.virtualenv_location: + _src_dir = TemporaryDirectory(prefix='pipenv-', suffix='-src') + src_dir = Path(_src_dir.name) + else: + src_dir = Path( + os.environ.get("PIP_SRC", os.path.join(project.virtualenv_location, "src")) + ) + src_dir.mkdir(mode=0o775, exist_ok=True) + vcs_registry = VcsSupport + for pkg_name, pkg_pipfile in packages.items(): + requirement = requirementslib.Requirement.from_pipfile( + pkg_name, pkg_pipfile, + ) + backend = vcs_registry()._registry.get(requirement.vcs) + __vcs = backend(url=requirement.req.vcs_uri) + locked_rev = None + name = requirement.normalized_name + locked_rev = _obtain_vcs_req( + __vcs, src_dir.as_posix(), name, rev=pkg_pipfile.get("ref") + ) + if requirement.is_vcs: + requirement.req.ref = locked_rev + lockfile[name] = requirement.pipfile_entry[1] + reqs.append(requirement) + return reqs, lockfile + + def _venv_resolve_deps( deps, which, project, pre=False, verbose=False, clear=False, allow_global=False, pypi_mirror=None, ): - from .vendor import delegator - from . import resolver - import json + from pipenv import resolver if not deps: return [] resolver = escape_grouped_arguments(resolver.__file__.rstrip('co')) @@ -145,8 +202,8 @@ def do_lock( write=True, pypi_mirror=None, ): - """Executes the freeze functionality.""" - from .utils import get_vcs_deps + """Executes the freeze functionality. + """ cached_lockfile = {} if not pre: pre = project.settings.get('allow_prereleases') @@ -227,7 +284,7 @@ def do_lock( lockfile[settings['lockfile_key']].update(dep_lockfile) # Add refs for VCS installs. # TODO: be smarter about this. - vcs_reqs, vcs_lockfile = get_vcs_deps( + vcs_reqs, vcs_lockfile = _get_vcs_deps( project, pip_freeze, which=which, diff --git a/pipenv/operations/open.py b/pipenv/operations/open.py new file mode 100644 index 00000000..cd89d933 --- /dev/null +++ b/pipenv/operations/open.py @@ -0,0 +1,32 @@ +import os +import sys + +from pipenv.patched import crayons +from pipenv.vendor import click, delegator + +from pipenv.core import which + +from .ensure import ensure_project + + +def do_open(module, three, python): + # Ensure that virtualenv is available. + ensure_project(three=three, python=python, validate=False) + c = delegator.run( + '{0} -c "import {1}; print({1}.__file__);"'.format( + which('python'), module + ) + ) + try: + assert c.return_code == 0 + except AssertionError: + click.echo(crayons.red('Module not found!')) + sys.exit(1) + if '__init__.py' in c.out: + p = os.path.dirname(c.out.strip().rstrip('cdo')) + else: + p = c.out.strip().rstrip('cdo') + click.echo( + crayons.normal('Opening {0!r} in your EDITOR.'.format(p), bold=True) + ) + click.edit(filename=p) diff --git a/pipenv/operations/options.py b/pipenv/operations/options.py new file mode 100644 index 00000000..0522a809 --- /dev/null +++ b/pipenv/operations/options.py @@ -0,0 +1,134 @@ +"""Various utilities for "pipenv --XXXX". + +Global imports should be kept at a minimum to reduce start up time as much +as possible. +""" +import os +import sys + +from pipenv.patched import crayons +from pipenv.vendor import click + + +def do_completion(): + from pipenv import shells + from pipenv.vendor import click_completion + try: + shell = shells.detect_info()[0] + except shells.ShellDetectionFailure: + click.echo( + 'Fail to detect shell. Please provide the {0} environment ' + 'variable.'.format(crayons.normal('PIPENV_SHELL', bold=True)), + err=True, + ) + sys.exit(1) + print(click_completion.get_code(shell=shell, prog_name='pipenv')) + + +def do_man(): + from pipenv.utils import system_which + man = system_which('man') + if man: + path = os.path.join(os.path.dirname(__file__), 'pipenv.1') + os.execle(man, 'man', path, os.environ) + return # Shouldn't reach here. + click.echo( + 'man does not appear to be available on your system.', + err=True, + ) + click.get_current_context.exit(1) + + +def do_envs(): + from pipenv import environments + click.echo( + 'The following environment variables can be set, ' + 'to do various things:\n', + ) + for key in environments.__dict__: + if key.startswith('PIPENV'): + click.echo(' - {0}'.format(crayons.normal(key, bold=True))) + click.echo('\nYou can learn more at:\n {0}'.format( + crayons.green( + 'https://docs.pipenv.org/advanced/' + '#configuration-with-environment-variables' + )), + ) + + +def warn_in_virtualenv(): + # Only warn if pipenv isn't already active. + from pipenv.environments import PIPENV_USE_SYSTEM + if not PIPENV_USE_SYSTEM or 'PIPENV_ACTIVE' in os.environ: + return + from pipenv.patched import crayons + from pipenv.vendor import click + click.echo( + '{0}: Pipenv found itself running within a virtual environment, ' + 'so it will automatically use that environment, instead of ' + 'creating its own for any project. You can set ' + '{1} to force pipenv to ignore that environment and create ' + 'its own instead.'.format( + crayons.green('Courtesy Notice'), + crayons.normal('PIPENV_IGNORE_VIRTUALENVS=1', bold=True), + ), + err=True, + ) + + +def do_py(system=False): + from pipenv.core import which + try: + click.echo(which('python', allow_global=system)) + except AttributeError: + click.echo(crayons.red('No project found!')) + + +def do_venv(): + # There is no virtualenv yet. + from pipenv.core import project + if not project.virtualenv_exists: + click.echo( + crayons.red( + 'No virtualenv has been created for this project yet!' + ), + err=True, + ) + sys.exit(1) + click.echo(project.virtualenv_location) + + +def do_rm(): + # Abort if --system (or running in a virtualenv). + from pipenv.environments import PIPENV_USE_SYSTEM + if PIPENV_USE_SYSTEM: + click.echo( + crayons.red( + 'You are attempting to remove a virtualenv that ' + 'Pipenv did not create. Aborting.' + ) + ) + sys.exit(1) + + from pipenv.core import project + if not project.virtualenv_exists: + click.echo(crayons.red( + 'No virtualenv has been created for this project yet!', + bold=True, + ), err=True) + sys.exit(1) + + click.echo( + crayons.normal( + u'{0} ({1})…'.format( + crayons.normal('Removing virtualenv', bold=True), + crayons.green(project.virtualenv_location), + ) + ) + ) + + # Remove the virtualenv. + from ._utils import spinner + with spinner(): + from .operations.virtualenv import cleanup_virtualenv + cleanup_virtualenv(bare=True) diff --git a/pipenv/operations/run.py b/pipenv/operations/run.py index 54e5636d..dc2fcbe6 100644 --- a/pipenv/operations/run.py +++ b/pipenv/operations/run.py @@ -2,38 +2,16 @@ import os import sys from pipenv.patched import crayons -from pipenv.vendor import click, dotenv +from pipenv.vendor import click from pipenv.cmdparse import ScriptEmptyError from pipenv.core import project, which -from pipenv.environments import ( - PIPENV_DONT_LOAD_ENV, - PIPENV_DOTENV_LOCATION, -) from pipenv.utils import system_which +from ._utils import load_dot_env from .ensure import ensure_project -def _load_dot_env(): - """Loads .env file into sys.environ. - """ - if not PIPENV_DONT_LOAD_ENV: - # If the project doesn't exist yet, check current directory for a .env file - project_directory = project.project_directory or '.' - denv = dotenv.find_dotenv( - PIPENV_DOTENV_LOCATION or os.sep.join([project_directory, '.env']) - ) - if os.path.isfile(denv): - click.echo( - crayons.normal( - 'Loading .env environment variables...', bold=True - ), - err=True, - ) - dotenv.load_dotenv(denv, override=True) - - def _inline_activate_virtualenv(): try: activate_this = which('activate_this.py') @@ -105,7 +83,7 @@ def do_run(command, args, three=None, python=False): """ # Ensure that virtualenv is available. ensure_project(three=three, python=python, validate=False) - _load_dot_env() + load_dot_env() # Activate virtualenv under the current interpreter's environment _inline_activate_virtualenv() try: diff --git a/pipenv/operations/shell.py b/pipenv/operations/shell.py new file mode 100644 index 00000000..2f510837 --- /dev/null +++ b/pipenv/operations/shell.py @@ -0,0 +1,138 @@ +import os +import signal +import sys + +from pipenv.patched import crayons, pew +from pipenv.vendor import click, pexpect + +from pipenv.core import project +from pipenv.environments import ( + PIPENV_SHELL, + PIPENV_SHELL_FANCY, +) +from pipenv.utils import temp_environ + +from ._utils import load_dot_env +from .ensure import ensure_project + +# Backport required for earlier versions of Python. +if sys.version_info < (3, 3): + from .vendor.backports.shutil_get_terminal_size import get_terminal_size +else: + from shutil import get_terminal_size + + +def _activate_virtualenv(source=True): + """Returns the string to activate a virtualenv.""" + # Suffix and source command for other shells. + suffix = '' + command = ' .' if source else '' + # Support for fish shell. + if PIPENV_SHELL and 'fish' in PIPENV_SHELL: + suffix = '.fish' + command = 'source' + # Support for csh shell. + if PIPENV_SHELL and 'csh' in PIPENV_SHELL: + suffix = '.csh' + command = 'source' + # Escape any spaces located within the virtualenv path to allow + # for proper activation. + venv_location = project.virtualenv_location.replace(' ', r'\ ') + if source: + return '{2} {0}/bin/activate{1}'.format(venv_location, suffix, command) + else: + return '{0}/bin/activate'.format(venv_location) + + +def do_shell(three=None, python=False, fancy=False, shell_args=None): + # Load .env file. + load_dot_env() + # Use fancy mode for Windows. + if os.name == 'nt': + fancy = True + + # 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' + compat = (not fancy) + # Support shell compatibility mode. + if PIPENV_SHELL_FANCY: + compat = False + # Compatibility mode: + if compat: + if PIPENV_SHELL: + shell = os.path.abspath(PIPENV_SHELL) + else: + click.echo( + crayons.red( + 'Please ensure that the {0} environment variable ' + 'is set before activating shell.'.format( + crayons.normal('SHELL', bold=True) + ) + ), + err=True, + ) + sys.exit(1) + click.echo( + crayons.normal( + 'Spawning environment shell ({0}). Use {1} to leave.'.format( + crayons.red(shell), crayons.normal("'exit'", bold=True) + ), + bold=True, + ), + err=True, + ) + cmd = "{0} -i'".format(shell) + args = [] + # Standard (properly configured shell) mode: + else: + if project.is_venv_in_project(): + # use .venv as the target virtualenv name + workon_name = '.venv' + else: + workon_name = project.virtualenv_name + cmd = sys.executable + args = ['-m', 'pipenv.pew', 'workon', workon_name] + # Grab current terminal dimensions to replace the hardcoded default + # dimensions of pexpect + terminal_dimensions = get_terminal_size() + try: + with temp_environ(): + if project.is_venv_in_project(): + os.environ['WORKON_HOME'] = project.project_directory + c = pexpect.spawn( + cmd, + args, + dimensions=( + terminal_dimensions.lines, terminal_dimensions.columns + ), + ) + # Windows! + except AttributeError: + # import subprocess + # Tell pew to use the project directory as its workon_home + with temp_environ(): + if project.is_venv_in_project(): + os.environ['WORKON_HOME'] = project.project_directory + pew.pew.workon_cmd([workon_name]) + sys.exit(0) + # 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(escape_character=None) + c.close() + sys.exit(c.exitstatus) diff --git a/pipenv/operations/sync.py b/pipenv/operations/sync.py index 35c29a82..26abe5ea 100644 --- a/pipenv/operations/sync.py +++ b/pipenv/operations/sync.py @@ -10,7 +10,6 @@ from .init import do_init def do_sync( - ctx, dev=False, three=None, python=None, diff --git a/pipenv/operations/uninstall.py b/pipenv/operations/uninstall.py new file mode 100644 index 00000000..3c200dbf --- /dev/null +++ b/pipenv/operations/uninstall.py @@ -0,0 +1,151 @@ +import shutil +import sys + +from pipenv.patched import crayons +from pipenv.vendor import click, delegator, requirementslib + +from pipenv.core import BAD_PACKAGES, project, which_pip +from pipenv.environments import PIPENV_USE_SYSTEM +from pipenv.utils import escape_grouped_arguments + +from .ensure import ensure_project +from .lock import do_lock + + +def _purge(bare=False, downloads=False, allow_global=False, verbose=False): + """Executes the purge functionality.""" + if downloads: + if not bare: + click.echo( + crayons.normal(u'Clearing out downloads directory...', bold=True) + ) + shutil.rmtree(project.download_location) + return + + freeze = delegator.run( + '{0} freeze'.format( + escape_grouped_arguments(which_pip(allow_global=allow_global)) + ) + ).out + # Remove comments from the output, if any. + installed = [ + line + for line in freeze.splitlines() + if not line.lstrip().startswith('#') + ] + # Remove setuptools and friends from installed, if present. + for package_name in BAD_PACKAGES: + for i, package in enumerate(installed): + if package.startswith(package_name): + del installed[i] + actually_installed = [] + for package in installed: + try: + dep = requirementslib.Requirement.from_line(package) + except AssertionError: + dep = None + if dep and not dep.is_vcs and not dep.editable: + dep = dep.name + actually_installed.append(dep) + if not bare: + click.echo( + u'Found {0} installed package(s), purging...'.format( + len(actually_installed) + ) + ) + command = '{0} uninstall {1} -y'.format( + escape_grouped_arguments(which_pip(allow_global=allow_global)), + ' '.join(actually_installed), + ) + if verbose: + click.echo('$ {0}'.format(command)) + c = delegator.run(command) + if not bare: + click.echo(crayons.blue(c.out)) + click.echo(crayons.green('Environment now purged and fresh!')) + + +def do_uninstall( + package_name=False, + more_packages=False, + three=None, + python=False, + system=False, + lock=False, + all_dev=False, + all=False, + verbose=False, + keep_outdated=False, + pypi_mirror=None, +): + # Automatically use an activated virtualenv. + if PIPENV_USE_SYSTEM: + system = True + # 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: + click.echo( + crayons.normal( + u'Un-installing all packages from virtualenv...', bold=True + ) + ) + _purge(allow_global=system, verbose=verbose) + sys.exit(0) + # Uninstall [dev-packages], if --dev was provided. + if all_dev: + if 'dev-packages' not in project.parsed_pipfile: + click.echo( + crayons.normal( + 'No {0} to uninstall.'.format( + crayons.red('[dev-packages]') + ), + bold=True, + ) + ) + sys.exit(0) + click.echo( + crayons.normal( + u'Un-installing {0}...'.format(crayons.red('[dev-packages]')), + bold=True, + ) + ) + package_names = project.dev_packages.keys() + if package_name is False and not all_dev: + click.echo(crayons.red('No package provided!'), err=True) + sys.exit(1) + for package_name in package_names: + click.echo(u'Un-installing {0}...'.format(crayons.green(package_name))) + cmd = '{0} uninstall {1} -y'.format( + escape_grouped_arguments(which_pip(allow_global=system)), + package_name, + ) + if verbose: + click.echo('$ {0}'.format(cmd)) + c = delegator.run(cmd) + click.echo(crayons.blue(c.out)) + if pipfile_remove: + in_packages = project.get_package_name_in_pipfile( + package_name, dev=False) + in_dev_packages = project.get_package_name_in_pipfile( + package_name, dev=True) + if not in_dev_packages and not in_packages: + click.echo( + 'No package {0} to remove from Pipfile.'.format( + crayons.green(package_name) + ) + ) + continue + + click.echo( + u'Removing {0} from Pipfile...'.format( + crayons.green(package_name) + ) + ) + # Remove package from both packages and dev-packages. + project.remove_package_from_pipfile(package_name, dev=True) + project.remove_package_from_pipfile(package_name, dev=False) + if lock: + do_lock(system=system, keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) diff --git a/pipenv/operations/update.py b/pipenv/operations/update.py new file mode 100644 index 00000000..51fac673 --- /dev/null +++ b/pipenv/operations/update.py @@ -0,0 +1,97 @@ +import sys + +from pipenv.patched import crayons +from pipenv.vendor import click, delegator, requirementslib + +from pipenv.core import project, which +from pipenv.utils import pep423_name + +from .ensure import ensure_project +from .lock import do_lock +from .sync import do_sync + + +def _do_outdated(pypi_mirror=None): + packages = {} + results = delegator.run('{0} freeze'.format(which('pip'))).out.strip( + ).split( + '\n' + ) + results = filter(bool, results) + for result in results: + dep = requirementslib.Requirement.from_line(result) + packages.update(dep.as_pipfile()) + updated_packages = {} + lockfile = do_lock(write=False, pypi_mirror=pypi_mirror) + for section in ('develop', 'default'): + for package in lockfile[section]: + try: + updated_packages[package] = lockfile[section][package][ + 'version' + ] + except KeyError: + pass + outdated = [] + for package in packages: + norm_name = pep423_name(package) + if norm_name in updated_packages: + if updated_packages[norm_name] != packages[package]: + outdated.append( + (package, updated_packages[norm_name], packages[package]) + ) + for package, new_version, old_version in outdated: + click.echo( + 'Package {0!r} out-of-date: {1!r} installed, ' + '{2!r} available.'.format(package, old_version, new_version), + ) + sys.exit(bool(outdated)) + + +def do_update( + package, more_packages, three, python, pypi_mirror, verbose, clear, + keep_outdated, pre, dev, bare, sequential, dry_run, outdated, +): + ensure_project(three=three, python=python, warn=True) + if not outdated: + outdated = bool(dry_run) + if outdated: + _do_outdated(pypi_mirror=pypi_mirror) + if not package: + click.echo( + '{0} {1} {2} {3}{4}'.format( + crayons.white('Running', bold=True), + crayons.red('$ pipenv lock', bold=True), + crayons.white('then', bold=True), + crayons.red('$ pipenv sync', bold=True), + crayons.white('.', bold=True), + ) + ) + else: + for package in ([package] + list(more_packages)): + if package not in project.all_packages: + click.echo( + '{0}: {1} was not found in your Pipfile! Aborting.' + ''.format( + crayons.red('Warning', bold=True), + crayons.green(package, bold=True), + ), + err=True, + ) + sys.exit(1) + do_lock( + verbose=verbose, clear=clear, pre=pre, + keep_outdated=keep_outdated, pypi_mirror=pypi_mirror, + ) + do_sync( + dev=dev, + three=three, + python=python, + bare=bare, + dont_upgrade=False, + user=False, + verbose=verbose, + clear=clear, + unused=False, + sequential=sequential, + pypi_mirror=pypi_mirror, + ) diff --git a/pipenv/utils.py b/pipenv/utils.py index 31497df3..7143fe03 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -983,62 +983,6 @@ def resolve_ref(vcs_obj, target_dir, ref): return vcs_obj.get_revision_sha(target_dir, ref) -def obtain_vcs_req(vcs_obj, src_dir, name, rev=None): - target_dir = os.path.join(src_dir, name) - target_rev = vcs_obj.make_rev_options(rev) - if not os.path.exists(target_dir): - vcs_obj.obtain(target_dir) - if not vcs_obj.is_commit_id_equal(target_dir, rev) and not vcs_obj.is_commit_id_equal(target_dir, target_rev): - vcs_obj.update(target_dir, target_rev) - return vcs_obj.get_revision(target_dir) - - -def get_vcs_deps( - project, - pip_freeze=None, - which=None, - verbose=False, - clear=False, - pre=False, - allow_global=False, - dev=False, - pypi_mirror=None, -): - from .patched.notpip._internal.vcs import VcsSupport - from ._compat import TemporaryDirectory - - section = "vcs_dev_packages" if dev else "vcs_packages" - reqs = [] - lockfile = {} - try: - packages = getattr(project, section) - except AttributeError: - return [], [] - if not os.environ.get("PIP_SRC") and not project.virtualenv_location: - _src_dir = TemporaryDirectory(prefix='pipenv-', suffix='-src') - src_dir = Path(_src_dir.name) - else: - src_dir = Path( - os.environ.get("PIP_SRC", os.path.join(project.virtualenv_location, "src")) - ) - src_dir.mkdir(mode=0o775, exist_ok=True) - vcs_registry = VcsSupport - for pkg_name, pkg_pipfile in packages.items(): - requirement = Requirement.from_pipfile(pkg_name, pkg_pipfile) - backend = vcs_registry()._registry.get(requirement.vcs) - __vcs = backend(url=requirement.req.vcs_uri) - locked_rev = None - name = requirement.normalized_name - locked_rev = obtain_vcs_req( - __vcs, src_dir.as_posix(), name, rev=pkg_pipfile.get("ref") - ) - if requirement.is_vcs: - requirement.req.ref = locked_rev - lockfile[name] = requirement.pipfile_entry[1] - reqs.append(requirement) - return reqs, lockfile - - def fs_str(string): """Encodes a string into the proper filesystem encoding