Skip to content

Commit

Permalink
Issue pypa#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 9, 2015
1 parent ac0d6dd commit 5b978b6
Show file tree
Hide file tree
Showing 12 changed files with 91 additions and 11 deletions.
10 changes: 10 additions & 0 deletions docs/reference/pip_install.rst
Expand Up @@ -350,6 +350,16 @@ 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.


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

Expand Down
2 changes: 2 additions & 0 deletions pip/basecommand.py
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
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
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
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
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
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
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
7 changes: 5 additions & 2 deletions pip/req/req_file.py
Expand Up @@ -25,7 +25,7 @@ def _remove_prefix(line, prefix):


def parse_requirements(filename, finder=None, comes_from=None, options=None,
session=None):
session=None, cache_root=None):
if session is None:
raise TypeError(
"parse_requirements() missing 1 required keyword argument: "
Expand Down Expand Up @@ -63,7 +63,8 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None,
req_url, finder,
comes_from=filename,
options=options,
session=session):
session=session,
cache_root=cache_root):
yield item
elif line.startswith(('-Z', '--always-unzip')):
# No longer used, but previously these were used in
Expand Down Expand Up @@ -127,11 +128,13 @@ def parse_requirements(filename, finder=None, comes_from=None, options=None,
comes_from=comes_from,
default_vcs=options.default_vcs if options else None,
isolated=options.isolated_mode if options else False,
cache_root=cache_root,
)
else:
req = InstallRequirement.from_line(
line,
comes_from,
isolated=options.isolated_mode if options else False,
cache_root=cache_root,
)
yield req
23 changes: 18 additions & 5 deletions pip/req/req_install.py
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):
pycompile=True, markers=None, isolated=False,
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):
isolated=False, cache_root=None):
from pip.index import Link

name, url, extras_override, editable_options = parse_editable(
Expand All @@ -132,15 +134,16 @@ def from_editable(cls, editable_req, comes_from=None, default_vcs=None,
editable=True,
link=Link(url),
editable_options=editable_options,
isolated=isolated)
isolated=isolated,
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):
def from_line(cls, name, comes_from=None, isolated=False, 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 @@ -206,7 +209,7 @@ def from_line(cls, name, comes_from=None, isolated=False):
req = name

return cls(req, comes_from, link=link, markers=markers,
isolated=isolated)
isolated=isolated, cache_root=cache_root)

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

def _get_link(self):
return self._link

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

link = property(_get_link, _set_link)

@property
def specifier(self):
return self.req.specifier
Expand Down
7 changes: 6 additions & 1 deletion pip/req/req_set.py
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 @@ -514,6 +518,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
37 changes: 37 additions & 0 deletions pip/wheel.py
Expand Up @@ -39,6 +39,43 @@
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 link.is_wheel:
return link
root = _cache_for_filename(cache_dir, link.filename)
try:
candidates = os.listdir(root)
except OSError as e:
if e.errno == errno.EEXIST:
return link
for candidate in candidates:
try:
wheel = Wheel(candidate)
except InvalidWheelFilename:
continue
if not wheel.supported():
# Built for a different python/arch/etc
continue
path = os.path.join(root, candidate)
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

0 comments on commit 5b978b6

Please sign in to comment.