Skip to content

Commit

Permalink
Issue #2563: Read cached wheels from ~/.cache/pip
Browse files Browse the repository at this point in the history
This won't put wheels into that directory, but will read them if they
are there. --no-cache-dir will disable reading such wheels.
  • Loading branch information
rbtcollins committed Apr 13, 2015
1 parent 786ff8c commit 4926409
Show file tree
Hide file tree
Showing 13 changed files with 116 additions and 20 deletions.
17 changes: 17 additions & 0 deletions docs/reference/pip_install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,23 @@ Windows
:file:`<CSIDL_LOCAL_APPDATA>\\pip\\Cache`


Wheel cache
***********

Pip will read from the subdirectory ``wheels`` within the pip cache dir and use
any packages found there. This is disabled via the same ``no-cache-dir`` option
that disables the HTTP cache. The internal structure of that cache is not part
of the Pip API. As of 7.0 pip uses a subdirectory per sdist that wheels were
built from, and wheels within that subdirectory.

Pip attempts to choose the best wheels from those built in preference to
building a new wheel. Note that this means when a package has both optional
C extensions and builds `py` tagged wheels when the C extension can't be built
that pip will not attempt to build a better wheel for Python's that would have
supported it, once any generic wheel is built. To correct this, make sure that
the wheel's are built with Python specific tags - e.g. pp on Pypy.


Hash Verification
+++++++++++++++++

Expand Down
2 changes: 2 additions & 0 deletions pip/basecommand.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ def populate_requirement_set(requirement_set, args, options, finder,
requirement_set.add_requirement(
InstallRequirement.from_line(
name, None, isolated=options.isolated_mode,
cache_root=options.cache_dir
)
)

Expand All @@ -298,6 +299,7 @@ def populate_requirement_set(requirement_set, args, options, finder,
name,
default_vcs=options.default_vcs,
isolated=options.isolated_mode,
cache_root=options.cache_dir
)
)

Expand Down
3 changes: 2 additions & 1 deletion pip/commands/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ def run(self, options, args):
local_only=options.local,
user_only=options.user,
skip_regex=options.skip_requirements_regex,
isolated=options.isolated_mode)
isolated=options.isolated_mode,
cache_root=options.cache_dir)

for line in freeze(**freeze_kwargs):
sys.stdout.write(line + '\n')
1 change: 1 addition & 0 deletions pip/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ def run(self, options, args):
delete=build_delete) as build_dir:
requirement_set = RequirementSet(
build_dir=build_dir,
cache_root=options.cache_dir,
src_dir=options.src_dir,
download_dir=options.download_dir,
upgrade=options.upgrade,
Expand Down
1 change: 1 addition & 0 deletions pip/commands/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def find_packages_latest_versions(self, options):
for dist in installed_packages:
req = InstallRequirement.from_line(
dist.key, None, isolated=options.isolated_mode,
cache_root=options.cache_dir,
)
typ = 'unknown'
try:
Expand Down
5 changes: 4 additions & 1 deletion pip/commands/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def run(self, options, args):

requirement_set = RequirementSet(
build_dir=None,
cache_root=options.cache_dir,
src_dir=None,
download_dir=None,
isolated=options.isolated_mode,
Expand All @@ -54,13 +55,15 @@ def run(self, options, args):
requirement_set.add_requirement(
InstallRequirement.from_line(
name, isolated=options.isolated_mode,
cache_root=options.cache_dir,
)
)
for filename in options.requirements:
for req in parse_requirements(
filename,
options=options,
session=session):
session=session,
cache_root=options.cache_dir):
requirement_set.add_requirement(req)
if not requirement_set.has_requirements:
raise InstallationError(
Expand Down
1 change: 1 addition & 0 deletions pip/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ def run(self, options, args):
delete=build_delete) as build_dir:
requirement_set = RequirementSet(
build_dir=build_dir,
cache_root=options.cache_dir,
src_dir=options.src_dir,
download_dir=None,
ignore_dependencies=options.ignore_dependencies,
Expand Down
5 changes: 4 additions & 1 deletion pip/operations/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def freeze(
find_links=None, local_only=None, user_only=None, skip_regex=None,
find_tags=False,
default_vcs=None,
isolated=False):
isolated=False,
cache_root=None):
find_links = find_links or []
skip_match = None

Expand Down Expand Up @@ -75,11 +76,13 @@ def freeze(
line,
default_vcs=default_vcs,
isolated=isolated,
cache_root=cache_root,
)
else:
line_req = InstallRequirement.from_line(
line,
isolated=isolated,
cache_root=cache_root,
)

if not line_req.name:
Expand Down
19 changes: 9 additions & 10 deletions pip/req/req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def parser_exit(self, msg):


def parse_requirements(filename, finder=None, comes_from=None, options=None,
session=None):
session=None, cache_root=None):
"""
Parse a requirements file and yield InstallRequirement instances.
Expand All @@ -87,7 +87,6 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None,
:param options: Global options.
:param session: Instance of pip.download.PipSession.
"""

if session is None:
raise TypeError(
"parse_requirements() missing 1 required keyword argument: "
Expand All @@ -99,15 +98,15 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None,
)

parser = parse_content(
filename, content, finder, comes_from, options, session
filename, content, finder, comes_from, options, session, cache_root
)

for item in parser:
yield item


def parse_content(filename, content, finder=None, comes_from=None,
options=None, session=None):
options=None, session=None, cache_root=None):

# Split, sanitize and join lines with continuations.
content = content.splitlines()
Expand All @@ -129,8 +128,8 @@ def parse_content(filename, content, finder=None, comes_from=None,
comes_from = '-r %s (line %s)' % (filename, line_number)
isolated = options.isolated_mode if options else False
yield InstallRequirement.from_line(
req, comes_from, isolated=isolated, options=opts
)
req, comes_from, isolated=isolated, options=opts,
cache_root=cache_root)

# ---------------------------------------------------------------------
elif linetype == REQUIREMENT_EDITABLE:
Expand All @@ -139,8 +138,8 @@ def parse_content(filename, content, finder=None, comes_from=None,
default_vcs = options.default_vcs if options else None
yield InstallRequirement.from_editable(
value, comes_from=comes_from,
default_vcs=default_vcs, isolated=isolated
)
default_vcs=default_vcs, isolated=isolated,
cache_root=cache_root)

# ---------------------------------------------------------------------
elif linetype == REQUIREMENT_FILE:
Expand All @@ -152,8 +151,8 @@ def parse_content(filename, content, finder=None, comes_from=None,
req_url = os.path.join(os.path.dirname(filename), value)
# TODO: Why not use `comes_from='-r {} (line {})'` here as well?
parser = parse_requirements(
req_url, finder, comes_from, options, session
)
req_url, finder, comes_from, options, session,
cache_root=cache_root)
for req in parser:
yield req

Expand Down
25 changes: 20 additions & 5 deletions pip/req/req_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ class InstallRequirement(object):

def __init__(self, req, comes_from, source_dir=None, editable=False,
link=None, as_egg=False, update=True, editable_options=None,
pycompile=True, markers=None, isolated=False, options=None):
pycompile=True, markers=None, isolated=False, options=None,
cache_root=None):
self.extras = ()
if isinstance(req, six.string_types):
req = pkg_resources.Requirement.parse(req)
Expand All @@ -88,6 +89,7 @@ def __init__(self, req, comes_from, source_dir=None, editable=False,
editable_options = {}

self.editable_options = editable_options
self._cache_root = cache_root
self.link = link
self.as_egg = as_egg
self.markers = markers
Expand Down Expand Up @@ -118,7 +120,7 @@ def __init__(self, req, comes_from, source_dir=None, editable=False,

@classmethod
def from_editable(cls, editable_req, comes_from=None, default_vcs=None,
isolated=False, options=None):
isolated=False, options=None, cache_root=None):
from pip.index import Link

name, url, extras_override, editable_options = parse_editable(
Expand All @@ -133,15 +135,18 @@ def from_editable(cls, editable_req, comes_from=None, default_vcs=None,
link=Link(url),
editable_options=editable_options,
isolated=isolated,
options=options if options else {})
options=options if options else {},
cache_root=cache_root)

if extras_override is not None:
res.extras = extras_override

return res

@classmethod
def from_line(cls, name, comes_from=None, isolated=False, options=None):
def from_line(
cls, name, comes_from=None, isolated=False, options=None,
cache_root=None):
"""Creates an InstallRequirement from a name, which might be a
requirement, directory containing 'setup.py', filename, or URL.
"""
Expand Down Expand Up @@ -208,7 +213,7 @@ def from_line(cls, name, comes_from=None, isolated=False, options=None):

options = options if options else {}
return cls(req, comes_from, link=link, markers=markers,
isolated=isolated, options=options)
isolated=isolated, options=options, cache_root=cache_root)

def __str__(self):
if self.req:
Expand Down Expand Up @@ -241,6 +246,16 @@ def populate_link(self, finder, upgrade):
if self.link is None:
self.link = finder.find_requirement(self, upgrade)

@property
def link(self):
return self._link

@link.setter
def link(self, link):
# Lookup a cached wheel, if possible.
link = pip.wheel.cached_wheel(self._cache_root, link)
self._link = link

@property
def specifier(self):
return self.req.specifier
Expand Down
7 changes: 6 additions & 1 deletion pip/req/req_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ def __init__(self, build_dir, src_dir, download_dir, upgrade=False,
ignore_installed=False, as_egg=False, target_dir=None,
ignore_dependencies=False, force_reinstall=False,
use_user_site=False, session=None, pycompile=True,
isolated=False, wheel_download_dir=None):
isolated=False, wheel_download_dir=None,
cache_root=None):
"""Create a RequirementSet.
:param wheel_download_dir: Where still-packed .whl files should be
Expand All @@ -149,6 +150,8 @@ def __init__(self, build_dir, src_dir, download_dir, upgrade=False,
:param download_dir: Where still packed archives should be written to.
If None they are not saved, and are deleted immediately after
unpacking.
:param cache_root: The root of the pip cache, for passing to
InstallRequirement.
"""
if session is None:
raise TypeError(
Expand Down Expand Up @@ -181,6 +184,7 @@ def __init__(self, build_dir, src_dir, download_dir, upgrade=False,
if wheel_download_dir:
wheel_download_dir = normalize_path(wheel_download_dir)
self.wheel_download_dir = wheel_download_dir
self._cache_root = cache_root
# Maps from install_req -> dependencies_of_install_req
self._dependencies = defaultdict(list)

Expand Down Expand Up @@ -512,6 +516,7 @@ def add_req(subreq):
str(subreq),
req_to_install,
isolated=self.isolated,
cache_root=self._cache_root,
)
more_reqs.extend(self.add_requirement(
sub_install_req, req_to_install.name))
Expand Down
48 changes: 48 additions & 0 deletions pip/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import compileall
import csv
import errno
import functools
import hashlib
import logging
Expand All @@ -20,6 +21,8 @@

from pip._vendor.six import StringIO

import pip
from pip.download import path_to_url
from pip.exceptions import InvalidWheelFilename, UnsupportedWheel
from pip.locations import distutils_scheme
from pip import pep425tags
Expand All @@ -39,6 +42,51 @@
logger = logging.getLogger(__name__)


def _cache_for_filename(cache_dir, sdistfilename):
"""Return a directory to store cached wheels in for sdistfilename.
Because there are M wheels for any one sdist, we provide a directory
to cache them in, and then consult that directory when looking up
cache hits.
:param cache_dir: The cache_dir being used by pip.
:param sdistfilename: The filename of the sdist for which this will cache
wheels.
"""
return os.path.join(cache_dir, 'wheels', sdistfilename)


def cached_wheel(cache_dir, link):
if not cache_dir:
return link
if not link:
return link
if link.is_wheel:
return link
root = _cache_for_filename(cache_dir, link.filename)
try:
wheel_names = os.listdir(root)
except OSError as e:
if e.errno == errno.ENOENT:
return link
raise
candidates = []
for wheel_name in wheel_names:
try:
wheel = Wheel(wheel_name)
except InvalidWheelFilename:
continue
if not wheel.supported():
# Built for a different python/arch/etc
continue
candidates.append((wheel.support_index_min(), wheel_name))
if not candidates:
return link
candidates.sort()
path = os.path.join(root, candidates[0][1])
return pip.index.Link(path_to_url(path), trusted=True)


def rehash(path, algo='sha256', blocksize=1 << 20):
"""Return (hash, length) for path using hashlib.new(algo)"""
h = hashlib.new(algo)
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def test_parse_content_requirements_file(self, monkeypatch):
import pip.req.req_file

def stub_parse_requirements(req_url, finder, comes_from, options,
session):
session, cache_root):
return [req]
parse_requirements_stub = stub(call=stub_parse_requirements)
monkeypatch.setattr(pip.req.req_file, 'parse_requirements',
Expand Down

0 comments on commit 4926409

Please sign in to comment.