Skip to content

Commit e79fa0e

Browse files
authored
Merge pull request #6851 from sbidoul/pip6640-sbi
Cache wheels built from immutable VCS requirements
2 parents 5b77910 + a20395d commit e79fa0e

File tree

8 files changed

+119
-5
lines changed

8 files changed

+119
-5
lines changed

docs/html/reference/pip_install.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,9 @@ and use any packages found there. This is disabled via the same
573573
of that is not part of the pip API. As of 7.0, pip makes a subdirectory for
574574
each sdist that wheels are built from and places the resulting wheels inside.
575575

576+
As of version 20.0, pip also caches wheels when building from an immutable Git
577+
reference (i.e. a commit hash).
578+
576579
Pip attempts to choose the best wheels from those built in preference to
577580
building a new wheel. Note that this means when a package has both optional
578581
C extensions and builds ``py`` tagged wheels when the C extension can't be built

news/6640.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Cache wheels built from Git requirements that are considered immutable,
2+
because they point to a commit hash.

src/pip/_internal/vcs/git.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from pip._vendor.six.moves.urllib import request as urllib_request
1313

1414
from pip._internal.exceptions import BadCommand
15-
from pip._internal.utils.misc import display_path
15+
from pip._internal.utils.misc import display_path, hide_url
1616
from pip._internal.utils.subprocess import make_command
1717
from pip._internal.utils.temp_dir import TempDirectory
1818
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
@@ -59,6 +59,23 @@ class Git(VersionControl):
5959
def get_base_rev_args(rev):
6060
return [rev]
6161

62+
def is_immutable_rev_checkout(self, url, dest):
63+
# type: (str, str) -> bool
64+
_, rev_options = self.get_url_rev_options(hide_url(url))
65+
if not rev_options.rev:
66+
return False
67+
if not self.is_commit_id_equal(dest, rev_options.rev):
68+
# the current commit is different from rev,
69+
# which means rev was something else than a commit hash
70+
return False
71+
# return False in the rare case rev is both a commit hash
72+
# and a tag or a branch; we don't want to cache in that case
73+
# because that branch/tag could point to something else in the future
74+
is_tag_or_branch = bool(
75+
self.get_revision_sha(dest, rev_options.rev)[0]
76+
)
77+
return not is_tag_or_branch
78+
6279
def get_git_version(self):
6380
VERSION_PFX = 'git version '
6481
version = self.run_command(['version'], show_stdout=False)

src/pip/_internal/vcs/versioncontrol.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,20 @@ def get_base_rev_args(rev):
329329
"""
330330
raise NotImplementedError
331331

332+
def is_immutable_rev_checkout(self, url, dest):
333+
# type: (str, str) -> bool
334+
"""
335+
Return true if the commit hash checked out at dest matches
336+
the revision in url.
337+
338+
Always return False, if the VCS does not support immutable commit
339+
hashes.
340+
341+
This method does not check if there are local uncommitted changes
342+
in dest after checkout, as pip currently has no use case for that.
343+
"""
344+
return False
345+
332346
@classmethod
333347
def make_rev_options(cls, rev=None, extra_args=None):
334348
# type: (Optional[str], Optional[CommandArgs]) -> RevOptions

src/pip/_internal/wheel.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from pip._internal.utils.ui import open_spinner
5353
from pip._internal.utils.unpacking import unpack_file
5454
from pip._internal.utils.urls import path_to_url
55+
from pip._internal.vcs import vcs
5556

5657
if MYPY_CHECK_RUNNING:
5758
from typing import (
@@ -838,7 +839,15 @@ def should_cache(
838839
return False
839840

840841
if req.link and req.link.is_vcs:
841-
# VCS checkout. Build wheel just for this run.
842+
# VCS checkout. Build wheel just for this run
843+
# unless it points to an immutable commit hash in which
844+
# case it can be cached.
845+
assert not req.editable
846+
assert req.source_dir
847+
vcs_backend = vcs.get_backend_for_scheme(req.link.scheme)
848+
assert vcs_backend
849+
if vcs_backend.is_immutable_rev_checkout(req.link.url, req.source_dir):
850+
return True
842851
return False
843852

844853
link = req.link

tests/functional/test_install_vcs_git.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def _github_checkout(url_path, temp_dir, rev=None, egg=None, scheme=None):
6767
return local_url
6868

6969

70-
def _make_version_pkg_url(path, rev=None):
70+
def _make_version_pkg_url(path, rev=None, name="version_pkg"):
7171
"""
7272
Return a "git+file://" URL to the version_pkg test package.
7373
@@ -78,7 +78,7 @@ def _make_version_pkg_url(path, rev=None):
7878
"""
7979
file_url = _test_path_to_file_url(path)
8080
url_rev = '' if rev is None else '@{}'.format(rev)
81-
url = 'git+{}{}#egg=version_pkg'.format(file_url, url_rev)
81+
url = 'git+{}{}#egg={}'.format(file_url, url_rev, name)
8282

8383
return url
8484

@@ -476,3 +476,40 @@ def test_check_submodule_addition(script):
476476
script.venv / 'src/version-pkg/testpkg/static/testfile2'
477477
in update_result.files_created
478478
)
479+
480+
481+
def test_install_git_branch_not_cached(script, with_wheel):
482+
"""
483+
Installing git urls with a branch revision does not cause wheel caching.
484+
"""
485+
PKG = "gitbranchnotcached"
486+
repo_dir = _create_test_package(script, name=PKG)
487+
url = _make_version_pkg_url(repo_dir, rev="master", name=PKG)
488+
result = script.pip("install", url, "--only-binary=:all:")
489+
assert "Successfully built {}".format(PKG) in result.stdout, result.stdout
490+
script.pip("uninstall", "-y", PKG)
491+
# build occurs on the second install too because it is not cached
492+
result = script.pip("install", url)
493+
assert (
494+
"Successfully built {}".format(PKG) in result.stdout
495+
), result.stdout
496+
497+
498+
def test_install_git_sha_cached(script, with_wheel):
499+
"""
500+
Installing git urls with a sha revision does cause wheel caching.
501+
"""
502+
PKG = "gitshacached"
503+
repo_dir = _create_test_package(script, name=PKG)
504+
commit = script.run(
505+
'git', 'rev-parse', 'HEAD', cwd=repo_dir
506+
).stdout.strip()
507+
url = _make_version_pkg_url(repo_dir, rev=commit, name=PKG)
508+
result = script.pip("install", url)
509+
assert "Successfully built {}".format(PKG) in result.stdout, result.stdout
510+
script.pip("uninstall", "-y", PKG)
511+
# build does not occur on the second install because it is cached
512+
result = script.pip("install", url)
513+
assert (
514+
"Successfully built {}".format(PKG) not in result.stdout
515+
), result.stdout

tests/functional/test_vcs_git.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,20 @@ def test_is_commit_id_equal(script):
221221
assert not Git.is_commit_id_equal(version_pkg_path, 'abc123')
222222
# Also check passing a None value.
223223
assert not Git.is_commit_id_equal(version_pkg_path, None)
224+
225+
226+
def test_is_immutable_rev_checkout(script):
227+
version_pkg_path = _create_test_package(script)
228+
commit = script.run(
229+
'git', 'rev-parse', 'HEAD',
230+
cwd=version_pkg_path
231+
).stdout.strip()
232+
assert Git().is_immutable_rev_checkout(
233+
"git+https://g.c/o/r@" + commit, version_pkg_path
234+
)
235+
assert not Git().is_immutable_rev_checkout(
236+
"git+https://g.c/o/r", version_pkg_path
237+
)
238+
assert not Git().is_immutable_rev_checkout(
239+
"git+https://g.c/o/r@master", version_pkg_path
240+
)

tests/unit/test_wheel.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
MissingCallableSuffix,
2121
_raise_for_invalid_entrypoint,
2222
)
23-
from tests.lib import DATA_DIR, assert_paths_equal
23+
from tests.lib import DATA_DIR, _create_test_package, assert_paths_equal
2424

2525

2626
class ReqMock:
@@ -166,6 +166,21 @@ def check_binary_allowed(req):
166166
assert should_cache is expected
167167

168168

169+
def test_should_cache_git_sha(script, tmpdir):
170+
repo_path = _create_test_package(script, name="mypkg")
171+
commit = script.run(
172+
"git", "rev-parse", "HEAD", cwd=repo_path
173+
).stdout.strip()
174+
# a link referencing a sha should be cached
175+
url = "git+https://g.c/o/r@" + commit + "#egg=mypkg"
176+
req = ReqMock(link=Link(url), source_dir=repo_path)
177+
assert wheel.should_cache(req, check_binary_allowed=lambda r: True)
178+
# a link not referencing a sha should not be cached
179+
url = "git+https://g.c/o/r@master#egg=mypkg"
180+
req = ReqMock(link=Link(url), source_dir=repo_path)
181+
assert not wheel.should_cache(req, check_binary_allowed=lambda r: True)
182+
183+
169184
def test_format_command_result__INFO(caplog):
170185
caplog.set_level(logging.INFO)
171186
actual = wheel.format_command_result(

0 commit comments

Comments
 (0)