Skip to content

Commit 4308ddb

Browse files
authored
feature: store version update source in embed JSON file (#2273)
1 parent 51408e6 commit 4308ddb

File tree

7 files changed

+216
-41
lines changed

7 files changed

+216
-41
lines changed

docs/changelog/2265.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Try using previous updates of ``pip``, ``setuptools`` & ``wheel``
2+
when inside an update grace period rather than always falling back
3+
to embedded wheels - by :user:`mayeut`.

docs/changelog/2266.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
New patch versions of ``pip``, ``setuptools`` & ``wheel`` are now
2+
returned in the expected timeframe. - by :user:`mayeut`.

docs/changelog/2267.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Manual upgrades of ``pip``, ``setuptools`` & ``wheel`` are
2+
not discarded by a periodic update - 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-
│ └── 1
17+
│ └── 2 -> 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" / "1", distribution)
104+
return EmbedDistributionUpdateStoreDisk(self.lock / "wheel" / for_py_version / "embed" / "2", distribution)
105105

106106
@property
107107
def house(self):

src/virtualenv/seed/wheels/periodic_update.py

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@
3636
pass # pragma: no cov
3737

3838

39+
GRACE_PERIOD_CI = timedelta(hours=1) # prevent version switch in the middle of a CI run
40+
GRACE_PERIOD_MINOR = timedelta(days=28)
41+
UPDATE_PERIOD = timedelta(days=14)
42+
UPDATE_ABORTED_DELAY = timedelta(hours=1)
43+
44+
3945
def periodic_update(distribution, of_version, for_py_version, wheel, search_dirs, app_data, do_periodic_update, env):
4046
if do_periodic_update:
4147
handle_auto_update(distribution, for_py_version, wheel, search_dirs, app_data, env)
@@ -48,20 +54,20 @@ def _update_wheel(ver):
4854
return updated_wheel
4955

5056
u_log = UpdateLog.from_app_data(app_data, distribution, for_py_version)
51-
u_log_older_than_hour = now - u_log.completed > timedelta(hours=1) if u_log.completed is not None else False
5257
if of_version is None:
5358
for _, group in groupby(u_log.versions, key=lambda v: v.wheel.version_tuple[0:2]):
54-
version = next(group) # use only latest patch version per minor, earlier assumed to be buggy
55-
if wheel is not None and Path(version.filename).name == wheel.name:
56-
break
57-
if u_log.periodic is False or (u_log_older_than_hour and version.use(now)):
58-
wheel = _update_wheel(version)
59-
break
60-
elif u_log.periodic is False or u_log_older_than_hour:
59+
# use only latest patch version per minor, earlier assumed to be buggy
60+
all_patches = list(group)
61+
ignore_grace_period_minor = any(version for version in all_patches if version.use(now))
62+
for version in all_patches:
63+
if wheel is not None and Path(version.filename).name == wheel.name:
64+
return wheel
65+
if version.use(now, ignore_grace_period_minor):
66+
return _update_wheel(version)
67+
else:
6168
for version in u_log.versions:
6269
if version.wheel.version == of_version:
63-
wheel = _update_wheel(version)
64-
break
70+
return _update_wheel(version)
6571

6672
return wheel
6773

@@ -88,41 +94,52 @@ def load_datetime(value):
8894

8995

9096
class NewVersion(object):
91-
def __init__(self, filename, found_date, release_date):
97+
def __init__(self, filename, found_date, release_date, source):
9298
self.filename = filename
9399
self.found_date = found_date
94100
self.release_date = release_date
101+
self.source = source
95102

96103
@classmethod
97104
def from_dict(cls, dictionary):
98105
return cls(
99106
filename=dictionary["filename"],
100107
found_date=load_datetime(dictionary["found_date"]),
101108
release_date=load_datetime(dictionary["release_date"]),
109+
source=dictionary["source"],
102110
)
103111

104112
def to_dict(self):
105113
return {
106114
"filename": self.filename,
107115
"release_date": dump_datetime(self.release_date),
108116
"found_date": dump_datetime(self.found_date),
117+
"source": self.source,
109118
}
110119

111-
def use(self, now):
112-
compare_from = self.release_date or self.found_date
113-
return now - compare_from >= timedelta(days=28)
120+
def use(self, now, ignore_grace_period_minor=False, ignore_grace_period_ci=False):
121+
if self.source == "manual":
122+
return True
123+
elif self.source == "periodic":
124+
if self.found_date < now - GRACE_PERIOD_CI or ignore_grace_period_ci:
125+
if not ignore_grace_period_minor:
126+
compare_from = self.release_date or self.found_date
127+
return now - compare_from >= GRACE_PERIOD_MINOR
128+
return True
129+
return False
114130

115131
def __repr__(self):
116-
return "{}(filename={}), found_date={}, release_date={})".format(
132+
return "{}(filename={}), found_date={}, release_date={}, source={})".format(
117133
self.__class__.__name__,
118134
self.filename,
119135
self.found_date,
120136
self.release_date,
137+
self.source,
121138
)
122139

123140
def __eq__(self, other):
124141
return type(self) == type(other) and all(
125-
getattr(self, k) == getattr(other, k) for k in ["filename", "release_date", "found_date"]
142+
getattr(self, k) == getattr(other, k) for k in ["filename", "release_date", "found_date", "source"]
126143
)
127144

128145
def __ne__(self, other):
@@ -170,12 +187,12 @@ def needs_update(self):
170187
if self.completed is None: # never completed
171188
return self._check_start(now)
172189
else:
173-
if now - self.completed <= timedelta(days=14):
190+
if now - self.completed <= UPDATE_PERIOD:
174191
return False
175192
return self._check_start(now)
176193

177194
def _check_start(self, now):
178-
return self.started is None or now - self.started > timedelta(hours=1)
195+
return self.started is None or now - self.started > UPDATE_ABORTED_DELAY
179196

180197

181198
def trigger_update(distribution, for_py_version, wheel, search_dirs, app_data, env, periodic):
@@ -231,12 +248,24 @@ def _run_do_update(app_data, distribution, embed_filename, for_py_version, perio
231248
embed_update_log = app_data.embed_update_log(distribution, for_py_version)
232249
u_log = UpdateLog.from_dict(embed_update_log.read())
233250
now = datetime.now()
251+
if periodic:
252+
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
257+
else:
258+
source = "manual"
259+
# mark everything as source "manual"
260+
for version in u_log.versions:
261+
version.source = source
262+
234263
if wheel_filename is not None:
235264
dest = wheelhouse / wheel_filename.name
236265
if not dest.exists():
237266
copy2(str(wheel_filename), str(wheelhouse))
238267
last, last_version, versions = None, None, []
239-
while last is None or not last.use(now):
268+
while last is None or not last.use(now, ignore_grace_period_ci=True):
240269
download_time = datetime.now()
241270
dest = acquire.download_wheel(
242271
distribution=distribution,
@@ -250,7 +279,7 @@ def _run_do_update(app_data, distribution, embed_filename, for_py_version, perio
250279
if dest is None or (u_log.versions and u_log.versions[0].filename == dest.name):
251280
break
252281
release_date = release_date_for_wheel_path(dest.path)
253-
last = NewVersion(filename=dest.path.name, release_date=release_date, found_date=download_time)
282+
last = NewVersion(filename=dest.path.name, release_date=release_date, found_date=download_time, source=source)
254283
logging.info("detected %s in %s", last, datetime.now() - download_time)
255284
versions.append(last)
256285
last_wheel = Wheel(Path(last.filename))

tests/unit/seed/wheels/test_bundle.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import absolute_import, unicode_literals
22

33
import os
4+
from datetime import datetime
45

56
import pytest
67

78
from virtualenv.app_data import AppDataDiskFolder
89
from virtualenv.seed.wheels.bundle import from_bundle
910
from virtualenv.seed.wheels.embed import get_embed_wheel
11+
from virtualenv.seed.wheels.periodic_update import dump_datetime
1012
from virtualenv.seed.wheels.util import Version, Wheel
1113
from virtualenv.util.path import Path
1214

@@ -23,17 +25,19 @@ def next_pip_wheel(for_py_version):
2325
@pytest.fixture(scope="module")
2426
def app_data(tmp_path_factory, for_py_version, next_pip_wheel):
2527
temp_folder = tmp_path_factory.mktemp("module-app-data")
28+
now = dump_datetime(datetime.now())
2629
app_data_ = AppDataDiskFolder(str(temp_folder))
2730
app_data_.embed_update_log("pip", for_py_version).write(
2831
{
29-
"completed": "2000-01-01T00:00:00.000000Z",
32+
"completed": now,
3033
"periodic": True,
31-
"started": "2000-01-01T00:00:00.000000Z",
34+
"started": now,
3235
"versions": [
3336
{
3437
"filename": next_pip_wheel.name,
3538
"found_date": "2000-01-01T00:00:00.000000Z",
3639
"release_date": "2000-01-01T00:00:00.000000Z",
40+
"source": "periodic",
3741
}
3842
],
3943
}

0 commit comments

Comments
 (0)