Skip to content

Commit 319a540

Browse files
mayeutgaborbernat
andauthored
feature: cache downloaded wheel information (#2276)
Co-authored-by: Bernát Gábor <[email protected]>
1 parent 5f65057 commit 319a540

File tree

6 files changed

+199
-74
lines changed

6 files changed

+199
-74
lines changed

docs/changelog/2268.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add downloaded wheel information in the relevant JSON embed file to
2+
prevent additional downloads of the same wheel. - by :user:`mayeut`.

src/virtualenv/app_data/via_disk_folder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
│ │ └── <install class> -> CopyPipInstall / SymlinkPipInstall
1515
│ │ └── <wheel name> -> pip-20.1.1-py2.py3-none-any
1616
│ └── embed
17-
│ └── 2 -> json format versioning
17+
│ └── 3 -> json format versioning
1818
│ └── *.json -> for every distribution contains data about newer embed versions and releases
1919
└─── unzip <in zip app we cannot refer to some internal files, so first extract them>
2020
└── <virtualenv version>
@@ -101,7 +101,7 @@ def py_info_clear(self):
101101
filename.unlink()
102102

103103
def embed_update_log(self, distribution, for_py_version):
104-
return EmbedDistributionUpdateStoreDisk(self.lock / "wheel" / for_py_version / "embed" / "2", distribution)
104+
return EmbedDistributionUpdateStoreDisk(self.lock / "wheel" / for_py_version / "embed" / "3", distribution)
105105

106106
@property
107107
def house(self):

src/virtualenv/seed/wheels/acquire.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from virtualenv.util.subprocess import Popen, subprocess
1111

1212
from .bundle import from_bundle
13+
from .periodic_update import add_wheel_to_update_log
1314
from .util import Version, Wheel, discover_wheels
1415

1516

@@ -35,6 +36,8 @@ def get_wheel(distribution, version, for_py_version, search_dirs, download, app_
3536
to_folder=app_data.house,
3637
env=env,
3738
)
39+
if wheel is not None and app_data.can_update:
40+
add_wheel_to_update_log(wheel, for_py_version, app_data)
3841

3942
return wheel
4043

src/virtualenv/seed/wheels/periodic_update.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ def handle_auto_update(distribution, for_py_version, wheel, search_dirs, app_dat
8282
trigger_update(distribution, for_py_version, wheel, search_dirs, app_data, periodic=True, env=env)
8383

8484

85+
def add_wheel_to_update_log(wheel, for_py_version, app_data):
86+
embed_update_log = app_data.embed_update_log(wheel.distribution, for_py_version)
87+
logging.debug("adding %s information to %s", wheel.name, embed_update_log.file)
88+
u_log = UpdateLog.from_dict(embed_update_log.read())
89+
if any(version.filename == wheel.name for version in u_log.versions):
90+
logging.warning("%s already present in %s", wheel.name, embed_update_log.file)
91+
return
92+
# we don't need a release date for sources other than "periodic"
93+
version = NewVersion(wheel.name, datetime.now(), None, "download")
94+
u_log.versions.append(version) # always write at the end for proper updates
95+
embed_update_log.write(u_log.to_dict())
96+
97+
8598
DATETIME_FMT = "%Y-%m-%dT%H:%M:%S.%fZ"
8699

87100

@@ -248,23 +261,27 @@ def _run_do_update(app_data, distribution, embed_filename, for_py_version, perio
248261
embed_update_log = app_data.embed_update_log(distribution, for_py_version)
249262
u_log = UpdateLog.from_dict(embed_update_log.read())
250263
now = datetime.now()
264+
265+
update_versions, other_versions = [], []
266+
for version in u_log.versions:
267+
if version.source in {"periodic", "manual"}:
268+
update_versions.append(version)
269+
else:
270+
other_versions.append(version)
271+
251272
if periodic:
252273
source = "periodic"
253-
# mark everything not updated manually as source "periodic"
254-
for version in u_log.versions:
255-
if version.source != "manual":
256-
version.source = source
257274
else:
258275
source = "manual"
259-
# mark everything as source "manual"
260-
for version in u_log.versions:
261-
version.source = source
276+
# mark the most recent one as source "manual"
277+
if update_versions:
278+
update_versions[0].source = source
262279

263280
if wheel_filename is not None:
264281
dest = wheelhouse / wheel_filename.name
265282
if not dest.exists():
266283
copy2(str(wheel_filename), str(wheelhouse))
267-
last, last_version, versions = None, None, []
284+
last, last_version, versions, filenames = None, None, [], set()
268285
while last is None or not last.use(now, ignore_grace_period_ci=True):
269286
download_time = datetime.now()
270287
dest = acquire.download_wheel(
@@ -276,21 +293,24 @@ def _run_do_update(app_data, distribution, embed_filename, for_py_version, perio
276293
to_folder=wheelhouse,
277294
env=os.environ,
278295
)
279-
if dest is None or (u_log.versions and u_log.versions[0].filename == dest.name):
296+
if dest is None or (update_versions and update_versions[0].filename == dest.name):
280297
break
281298
release_date = release_date_for_wheel_path(dest.path)
282299
last = NewVersion(filename=dest.path.name, release_date=release_date, found_date=download_time, source=source)
283300
logging.info("detected %s in %s", last, datetime.now() - download_time)
284301
versions.append(last)
285-
last_wheel = Wheel(Path(last.filename))
302+
filenames.add(last.filename)
303+
last_wheel = last.wheel
286304
last_version = last_wheel.version
287305
if embed_version is not None:
288306
if embed_version >= last_wheel.version_tuple: # stop download if we reach the embed version
289307
break
290308
u_log.periodic = periodic
291309
if not u_log.periodic:
292310
u_log.started = now
293-
u_log.versions = versions + u_log.versions
311+
# update other_versions by removing version we just found
312+
other_versions = [version for version in other_versions if version.filename not in filenames]
313+
u_log.versions = versions + update_versions + other_versions
294314
u_log.completed = datetime.now()
295315
embed_update_log.write(u_log.to_dict())
296316
return versions
@@ -395,6 +415,7 @@ def _run_manual_upgrade(app_data, distribution, for_py_version, env):
395415

396416

397417
__all__ = (
418+
"add_wheel_to_update_log",
398419
"periodic_update",
399420
"do_update",
400421
"manual_upgrade",

tests/unit/seed/wheels/test_acquire.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,25 @@
22

33
import os
44
import sys
5+
from datetime import datetime
56
from subprocess import CalledProcessError
67

78
import pytest
89

10+
from virtualenv.app_data import AppDataDiskFolder
11+
from virtualenv.info import IS_PYPY, PY2
912
from virtualenv.seed.wheels.acquire import download_wheel, get_wheel, pip_wheel_env_run
1013
from virtualenv.seed.wheels.embed import BUNDLE_FOLDER, get_embed_wheel
14+
from virtualenv.seed.wheels.periodic_update import dump_datetime
1115
from virtualenv.seed.wheels.util import Wheel, discover_wheels
1216
from virtualenv.util.path import Path
1317

1418

19+
@pytest.fixture(autouse=True)
20+
def fake_release_date(mocker):
21+
mocker.patch("virtualenv.seed.wheels.periodic_update.release_date_for_wheel_path", return_value=None)
22+
23+
1524
def test_pip_wheel_env_run_could_not_find(session_app_data, mocker):
1625
mocker.patch("virtualenv.seed.wheels.acquire.from_bundle", return_value=None)
1726
with pytest.raises(RuntimeError, match="could not find the embedded pip"):
@@ -74,24 +83,64 @@ def test_download_fails(mocker, for_py_version, session_app_data):
7483
@pytest.fixture
7584
def downloaded_wheel(mocker):
7685
wheel = Wheel.from_path(Path("setuptools-0.0.0-py2.py3-none-any.whl"))
77-
mocker.patch("virtualenv.seed.wheels.acquire.download_wheel", return_value=wheel)
78-
yield wheel
86+
yield wheel, mocker.patch("virtualenv.seed.wheels.acquire.download_wheel", return_value=wheel)
7987

8088

8189
@pytest.mark.parametrize("version", ["bundle", "0.0.0"])
82-
def test_get_wheel_download_called(for_py_version, session_app_data, downloaded_wheel, version):
90+
def test_get_wheel_download_called(mocker, for_py_version, session_app_data, downloaded_wheel, version):
8391
distribution = "setuptools"
92+
write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write")
8493
wheel = get_wheel(distribution, version, for_py_version, [], True, session_app_data, False, os.environ)
8594
assert wheel is not None
86-
assert wheel.name == downloaded_wheel.name
95+
assert wheel.name == downloaded_wheel[0].name
96+
assert downloaded_wheel[1].call_count == 1
97+
assert write.call_count == 1
8798

8899

89100
@pytest.mark.parametrize("version", ["embed", "pinned"])
90-
def test_get_wheel_download_not_called(for_py_version, session_app_data, downloaded_wheel, version):
101+
def test_get_wheel_download_not_called(mocker, for_py_version, session_app_data, downloaded_wheel, version):
91102
distribution = "setuptools"
92103
expected = get_embed_wheel(distribution, for_py_version)
93104
if version == "pinned":
94105
version = expected.version
106+
write = mocker.patch("virtualenv.app_data.via_disk_folder.JSONStoreDisk.write")
95107
wheel = get_wheel(distribution, version, for_py_version, [], True, session_app_data, False, os.environ)
96108
assert wheel is not None
97109
assert wheel.name == expected.name
110+
assert downloaded_wheel[1].call_count == 0
111+
assert write.call_count == 0
112+
113+
114+
@pytest.mark.skipif(IS_PYPY and PY2, reason="mocker.spy failing on PyPy 2.x")
115+
def test_get_wheel_download_cached(tmp_path, freezer, mocker, for_py_version, downloaded_wheel):
116+
from virtualenv.app_data.via_disk_folder import JSONStoreDisk
117+
118+
app_data = AppDataDiskFolder(folder=str(tmp_path))
119+
expected = downloaded_wheel[0]
120+
write = mocker.spy(JSONStoreDisk, "write")
121+
# 1st call, not cached, download is called
122+
wheel = get_wheel(expected.distribution, expected.version, for_py_version, [], True, app_data, False, os.environ)
123+
assert wheel is not None
124+
assert wheel.name == expected.name
125+
assert downloaded_wheel[1].call_count == 1
126+
assert write.call_count == 1
127+
# 2nd call, cached, download is not called
128+
wheel = get_wheel(expected.distribution, expected.version, for_py_version, [], True, app_data, False, os.environ)
129+
assert wheel is not None
130+
assert wheel.name == expected.name
131+
assert downloaded_wheel[1].call_count == 1
132+
assert write.call_count == 1
133+
wrote_json = write.call_args[0][1]
134+
assert wrote_json == {
135+
"completed": None,
136+
"periodic": None,
137+
"started": None,
138+
"versions": [
139+
{
140+
"filename": expected.name,
141+
"release_date": None,
142+
"found_date": dump_datetime(datetime.now()),
143+
"source": "download",
144+
},
145+
],
146+
}

0 commit comments

Comments
 (0)