From 4bdf4394ff172e745eae8ad3f25d73dac975e738 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 21 May 2017 20:07:00 -0700 Subject: [PATCH] progress --- Pipfile | 1 + Pipfile.lock | 22 +++--- pipenv/cli.py | 43 ++++++------ pipenv/progress.py | 171 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 30 deletions(-) create mode 100644 pipenv/progress.py diff --git a/Pipfile b/Pipfile index b683ecce..1d38fe55 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ Sphinx = "<=1.5.5" [packages] pew = ">=0.1.26" +appdirs = "*" [requires] python_version = "2.7" diff --git a/Pipfile.lock b/Pipfile.lock index 9683bd0a..74206572 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8cba5434b2ba425d05649334b063af088196a5208a5a7c9ae52be9e1467824b8" + "sha256": "0cee80b01328a89a9475d082839ad0200b8e13a11723515e7ac4ae567dbe0699" }, "requires": { "python_version": "2.7" @@ -17,9 +17,15 @@ "appdirs": { "version": "==1.4.3" }, + "backports.shutil_get_terminal_size": { + "version": "==1.0.0" + }, "packaging": { "version": "==16.8" }, + "pathlib": { + "version": "==1.0.1" + }, "pew": { "version": "==0.1.26" }, @@ -29,15 +35,12 @@ "pythonz-bd": { "version": "==1.11.4" }, - "requests": { - "version": "==2.14.2" - }, - "resumable-urlretrieve": { - "version": "==0.1.5" - }, "setuptools": { "version": "==35.0.2" }, + "shutilwhich": { + "version": "==1.1.0" + }, "six": { "version": "==1.10.0" }, @@ -76,6 +79,9 @@ "docutils": { "version": "==0.13.1" }, + "funcsigs": { + "version": "==1.0.2" + }, "imagesize": { "version": "==0.7.1" }, @@ -86,7 +92,7 @@ "version": "==16.8" }, "pbr": { - "version": "==3.0.0" + "version": "==3.0.1" }, "pexpect": { "version": "==4.2.1" diff --git a/pipenv/cli.py b/pipenv/cli.py index 6f2ef420..0093e84a 100644 --- a/pipenv/cli.py +++ b/pipenv/cli.py @@ -8,6 +8,7 @@ import distutils.spawn import shutil import signal +import appdirs import click import click_completion import crayons @@ -24,7 +25,7 @@ from .project import Project from .utils import (convert_deps_from_pip, convert_deps_to_pip, is_required_version, proper_case, pep423_name, split_vcs, recase_file) from .__version__ import __version__ -from . import pep508checker +from . import pep508checker, progress from .environments import PIPENV_COLORBLIND, PIPENV_NOSPIN, PIPENV_SHELL_COMPAT, PIPENV_VENV_IN_PROJECT # Backport required for earlier versions of Python. @@ -60,6 +61,7 @@ requests.packages.urllib3.disable_warnings(InsecureRequestWarning) project = Project() + def cleanup_virtualenv(bare=True): """Removes the virtualenv directory from the system.""" @@ -249,7 +251,7 @@ def do_install_dependencies(dev=False, only=False, bare=False, requirements=Fals del v['hash'] # Convert the deps to pip-compatible arguments. - hashed_deps_path = convert_deps_to_pip(deps) + hashed_deps = convert_deps_to_pip(deps, r=False) vcs_deps_path = convert_deps_to_pip(vcs_deps) # --requirements was passed. @@ -261,8 +263,21 @@ def do_install_dependencies(dev=False, only=False, bare=False, requirements=Fals sys.exit(0) # pip install: - with spinner(): - c = pip_install(r=hashed_deps_path, ignore_hashes=ignore_hashes, allow_global=allow_global) + for dep in progress.bar(hashed_deps): + + c = pip_install(dep, ignore_hashes=ignore_hashes, 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 'PACKAGES DO NOT MATCH THE HASHES' in c.err: + click.echo(crayons.yellow('You can supply the --ignore-hashes option to ' + '\'pipenv install\' to bypass this feature.')) + sys.exit(c.return_code) + + if len(vcs_deps): + with spinner(): + c = pip_install(r=vcs_deps_path, ignore_hashes=True, allow_global=allow_global) if c.return_code != 0: click.echo(crayons.red('An error occured while installing!')) @@ -272,23 +287,6 @@ def do_install_dependencies(dev=False, only=False, bare=False, requirements=Fals '\'pipenv install\' to bypass this feature.')) sys.exit(c.return_code) - if not bare: - click.echo(crayons.blue(format_pip_output(c.out, r=hashed_deps_path))) - - with spinner(): - c = pip_install(r=vcs_deps_path, ignore_hashes=True, 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 'PACKAGES DO NOT MATCH THE HASHES' in c.err: - click.echo(crayons.yellow('You can supply the --ignore-hashes option to ' - '\'pipenv install\' to bypass this feature.')) - sys.exit(c.return_code) - - if not bare: - click.echo(crayons.blue(format_pip_output(c.out, r=vcs_deps_path))) - # Cleanup the temp requirements file. if requirements: os.remove(hashed_deps_path) @@ -823,11 +821,12 @@ def install(package_name=False, more_packages=False, dev=False, three=False, pyt ensure_project(three=three, python=python) # Capture -e argument and assign it to following package_name. + more_packages = list(more_packages) if package_name == '-e': package_name = ' '.join(package_name, more_packages.pop(0)) # Allow more than one package to be provided. - package_names = (package_name,) + more_packages + package_names = [package_name,] + more_packages # Install all dependencies, if none was provided. if package_name is False: diff --git a/pipenv/progress.py b/pipenv/progress.py new file mode 100644 index 00000000..1d1fff55 --- /dev/null +++ b/pipenv/progress.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- + +""" +clint.textui.progress +~~~~~~~~~~~~~~~~~ + +This module provides the progressbar functionality. + +""" + +from __future__ import absolute_import + +import sys +import time + +STREAM = sys.stderr + +BAR_TEMPLATE = '%s[%s%s] %i/%i - %s\r' +MILL_TEMPLATE = '%s %s %i/%i\r' + +DOTS_CHAR = '.' +BAR_FILLED_CHAR = '=' +BAR_EMPTY_CHAR = ' ' +MILL_CHARS = ['|', '/', '-', '\\'] + +# How long to wait before recalculating the ETA +ETA_INTERVAL = 1 +# How many intervals (excluding the current one) to calculate the simple moving +# average +ETA_SMA_WINDOW = 9 + + +class Bar(object): + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.done() + return False # we're not suppressing exceptions + + def __init__(self, label='', width=32, hide=None, empty_char=BAR_EMPTY_CHAR, + filled_char=BAR_FILLED_CHAR, expected_size=None, every=1): + self.label = label + self.width = width + self.hide = hide + # Only show bar in terminals by default (better for piping, logging etc.) + if hide is None: + try: + self.hide = not STREAM.isatty() + except AttributeError: # output does not support isatty() + self.hide = True + self.empty_char = empty_char + self.filled_char = filled_char + self.expected_size = expected_size + self.every = every + self.start = time.time() + self.ittimes = [] + self.eta = 0 + self.etadelta = time.time() + self.etadisp = self.format_time(self.eta) + self.last_progress = 0 + if (self.expected_size): + self.show(0) + + def show(self, progress, count=None): + if count is not None: + self.expected_size = count + if self.expected_size is None: + raise Exception("expected_size not initialized") + self.last_progress = progress + if (time.time() - self.etadelta) > ETA_INTERVAL: + self.etadelta = time.time() + self.ittimes = \ + self.ittimes[-ETA_SMA_WINDOW:] + \ + [-(self.start - time.time()) / (progress+1)] + self.eta = \ + sum(self.ittimes) / float(len(self.ittimes)) * \ + (self.expected_size - progress) + self.etadisp = self.format_time(self.eta) + x = int(self.width * progress / self.expected_size) + if not self.hide: + if ((progress % self.every) == 0 or # True every "every" updates + (progress == self.expected_size)): # And when we're done + STREAM.write(BAR_TEMPLATE % ( + self.label, self.filled_char * x, + self.empty_char * (self.width - x), progress, + self.expected_size, self.etadisp)) + STREAM.flush() + + def done(self): + self.elapsed = time.time() - self.start + elapsed_disp = self.format_time(self.elapsed) + if not self.hide: + # Print completed bar with elapsed time + STREAM.write(BAR_TEMPLATE % ( + self.label, self.filled_char * self.width, + self.empty_char * 0, self.last_progress, + self.expected_size, elapsed_disp)) + STREAM.write('\n') + STREAM.flush() + + def format_time(self, seconds): + return time.strftime('%H:%M:%S', time.gmtime(seconds)) + + +def bar(it, label='', width=32, hide=None, empty_char=BAR_EMPTY_CHAR, + filled_char=BAR_FILLED_CHAR, expected_size=None, every=1): + """Progress iterator. Wrap your iterables with it.""" + + count = len(it) if expected_size is None else expected_size + + with Bar(label=label, width=width, hide=hide, empty_char=BAR_EMPTY_CHAR, + filled_char=BAR_FILLED_CHAR, expected_size=count, every=every) \ + as bar: + for i, item in enumerate(it): + yield item + bar.show(i + 1) + + +def dots(it, label='', hide=None, every=1): + """Progress iterator. Prints a dot for each item being iterated""" + + count = 0 + + if not hide: + STREAM.write(label) + + for i, item in enumerate(it): + if not hide: + if i % every == 0: # True every "every" updates + STREAM.write(DOTS_CHAR) + sys.stderr.flush() + + count += 1 + + yield item + + STREAM.write('\n') + STREAM.flush() + + +def mill(it, label='', hide=None, expected_size=None, every=1): + """Progress iterator. Prints a mill while iterating over the items.""" + + def _mill_char(_i): + if _i >= count: + return ' ' + else: + return MILL_CHARS[(_i // every) % len(MILL_CHARS)] + + def _show(_i): + if not hide: + if ((_i % every) == 0 or # True every "every" updates + (_i == count)): # And when we're done + + STREAM.write(MILL_TEMPLATE % ( + label, _mill_char(_i), _i, count)) + STREAM.flush() + + count = len(it) if expected_size is None else expected_size + + if count: + _show(0) + + for i, item in enumerate(it): + yield item + _show(i + 1) + + if not hide: + STREAM.write('\n') + STREAM.flush() \ No newline at end of file