Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/3376.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fall back to the bundled seed wheel and remove the embed update log when the log in the app data folder holds JSON of
the wrong shape, instead of failing to create the environment - by :user:`pasmud`.
15 changes: 12 additions & 3 deletions src/virtualenv/seed/wheels/periodic_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import os
import ssl
import sys
from contextlib import suppress
from datetime import datetime, timedelta, timezone
from itertools import groupby
from pathlib import Path
Expand All @@ -28,7 +29,7 @@
if TYPE_CHECKING:
from collections.abc import Generator

from virtualenv.app_data.base import AppData
from virtualenv.app_data.base import AppData, ContentStore

LOGGER = logging.getLogger(__name__)
GRACE_PERIOD_CI = timedelta(hours=1) # prevent version switch in the middle of a CI run
Expand Down Expand Up @@ -192,8 +193,16 @@ def from_dict(cls, dictionary: dict[str, object] | None) -> UpdateLog:

@classmethod
def from_app_data(cls, app_data: AppData, distribution: str, for_py_version: str) -> UpdateLog:
raw_json = app_data.embed_update_log(distribution, for_py_version).read()
return cls.from_dict(raw_json)
store: Final[ContentStore] = app_data.embed_update_log(distribution, for_py_version)
try:
return cls.from_dict(store.read())
except (AttributeError, KeyError, TypeError, ValueError):
# another virtualenv version or an interrupted run can leave valid JSON of the wrong shape in the shared
# app data; drop it as read() drops invalid JSON, since an empty log falls back to the bundled wheel
LOGGER.warning("removing malformed embed update log of %s", distribution)
with suppress(OSError): # another process may remove or rewrite the file at the same time
store.remove()
return cls(None, None, [], None)

def to_dict(self) -> dict[str, object]:
return {
Expand Down
38 changes: 37 additions & 1 deletion tests/unit/seed/wheels/test_periodic_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import hashlib
import json
import logging
import os
import subprocess
import sys
Expand All @@ -12,7 +13,7 @@
from itertools import zip_longest
from pathlib import Path
from textwrap import dedent
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Final
from urllib.error import URLError

import pytest
Expand Down Expand Up @@ -42,6 +43,8 @@

from pytest_mock import MockerFixture

from virtualenv.app_data.via_disk_folder import EmbedDistributionUpdateStoreDisk


@pytest.fixture(autouse=True)
def _clear_pypi_info_cache() -> Generator[None, None, None]:
Expand Down Expand Up @@ -251,6 +254,39 @@ def test_periodic_update_skip(u_log, mocker, for_py_version, session_app_data, t
assert result is None


@pytest.mark.parametrize(
"stored",
[
pytest.param([1, 2, 3], id="not-a-mapping"),
pytest.param("oops", id="string"),
pytest.param({"versions": "abc"}, id="versions-not-a-list"),
pytest.param({"versions": ["abc"]}, id="version-not-a-mapping"),
pytest.param({"completed": 5, "started": None, "versions": []}, id="completed-not-a-datetime"),
],
)
def test_periodic_update_drops_malformed_log(
stored: list[int] | str | dict[str, str | int | list[str] | None],
tmp_path: Path,
for_py_version: str,
caplog: pytest.LogCaptureFixture,
) -> None:
caplog.set_level(logging.WARNING)
app_data: Final[AppDataDiskFolder] = AppDataDiskFolder(str(tmp_path))
log: Final[EmbedDistributionUpdateStoreDisk] = app_data.embed_update_log("setuptools", for_py_version)
log.write(stored)
wheel: Final[Wheel | None] = get_embed_wheel("setuptools", for_py_version)

result: Final[Wheel | None] = periodic_update(
"setuptools", None, for_py_version, wheel, [], app_data, False, os.environ
)

assert (result, log.exists(), caplog.messages) == (
wheel,
False,
["removing malformed embed update log of setuptools"],
)


_UPDATE_YES = {
"never_started": UpdateLog(started=None, completed=None, versions=[], periodic=False),
"started_1_hour": UpdateLog(
Expand Down
Loading