Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show progressbar on generation hashes in verbose mode #743

Merged
merged 4 commits into from
May 2, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions piptools/_compat/contextlib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Ported from python 3.7 contextlib.py
class nullcontext(object):
"""Context manager that does no additional processing.
Used as a stand-in for a normal context manager, when a particular
block of code is only sometimes used with a normal context manager:
cm = optional_cm if condition else nullcontext()
with cm:
# Perform operation, using optional_cm if condition is True
"""

def __init__(self, enter_result=None):
self.enter_result = enter_result

def __enter__(self):
return self.enter_result

def __exit__(self, *excinfo):
pass
39 changes: 34 additions & 5 deletions piptools/repositories/pypi.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# coding: utf-8
from __future__ import absolute_import, division, print_function, unicode_literals

import collections
import hashlib
import os
from contextlib import contextmanager
Expand All @@ -16,11 +17,14 @@
RequirementSet,
TemporaryDirectory,
Wheel,
contextlib,
is_file_url,
url_to_path,
)
from ..cache import CACHE_DIR
from ..click import progressbar
from ..exceptions import NoCandidateFound
from ..logging import log
from ..utils import (
fs_str,
is_pinned_requirement,
Expand All @@ -43,6 +47,9 @@ def RequirementTracker():
except ImportError:
from pip.wheel import WheelCache

FILE_CHUNK_SIZE = 4096
FileStream = collections.namedtuple("File", "stream size")
atugushev marked this conversation as resolved.
Show resolved Hide resolved


class PyPIRepository(BaseRepository):
DEFAULT_INDEX_URL = PyPI.simple_url
Expand Down Expand Up @@ -278,15 +285,30 @@ def get_hashes(self, ireq):
)
matching_candidates = candidates_by_version[matching_versions[0]]

log.debug(" {}".format(ireq.name))

return {
self._get_file_hash(candidate.location) for candidate in matching_candidates
}

def _get_file_hash(self, location):
log.debug(" Hashing {}".format(location.url_without_fragment))
h = hashlib.new(FAVORITE_HASH)
with open_local_or_remote_file(location, self.session) as fp:
for chunk in iter(lambda: fp.read(8096), b""):
h.update(chunk)
with open_local_or_remote_file(location, self.session) as f:
# Chunks to iterate
chunks = iter(lambda: f.stream.read(FILE_CHUNK_SIZE), b"")

# Choose a context manager depending on verbosity
if log.verbosity >= 1:
iter_length = f.size / FILE_CHUNK_SIZE if f.size else None
context_manager = progressbar(chunks, length=iter_length, label=" ")
else:
context_manager = contextlib.nullcontext(chunks)

# Iterate over the chosen context manager
with context_manager as bar:
for chunk in bar:
h.update(chunk)
return ":".join([FAVORITE_HASH, h.hexdigest()])

@contextmanager
Expand Down Expand Up @@ -340,13 +362,20 @@ def open_local_or_remote_file(link, session):
if os.path.isdir(local_path):
raise ValueError("Cannot open directory for read: {}".format(url))
else:
st = os.stat(local_path)
atugushev marked this conversation as resolved.
Show resolved Hide resolved
with open(local_path, "rb") as local_file:
yield local_file
yield FileStream(stream=local_file, size=st.st_size)
else:
# Remote URL
headers = {"Accept-Encoding": "identity"}
response = session.get(url, headers=headers, stream=True)

# Content length must be int or None
content_length = response.headers.get("content-length")
if content_length is not None:
content_length = int(content_length)
atugushev marked this conversation as resolved.
Show resolved Hide resolved

try:
yield response.raw
yield FileStream(stream=response.raw, size=content_length)
finally:
response.close()
2 changes: 2 additions & 0 deletions piptools/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ def resolve_hashes(self, ireqs):
"""
Finds acceptable hashes for all of the given InstallRequirements.
"""
log.debug("")
log.debug("Generating hashes:")
with self.repository.allow_all_wheels():
return {ireq: self.repository.get_hashes(ireq) for ireq in ireqs}

Expand Down
13 changes: 13 additions & 0 deletions tests/test_cli_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,19 @@ def test_generate_hashes_with_editable(runner):
assert expected in out.output


def test_generate_hashes_verbose(runner):
"""
The hashes generation process should show a progress.
"""
with open("requirements.in", "w") as fp:
fp.write("pytz==2017.2")

out = runner.invoke(cli, ["--generate-hashes", "-v"])

expected_verbose_text = "Generating hashes:\n pytz\n"
assert expected_verbose_text in out.output


@fail_below_pip9
def test_filter_pip_markers(runner):
"""
Expand Down