diff --git a/news/3840.bugfix.md b/news/3840.bugfix.md new file mode 100644 index 0000000000..0885331648 --- /dev/null +++ b/news/3840.bugfix.md @@ -0,0 +1 @@ +Cache the update check timestamp even when the version lookup fails, so that pdm no longer appears to hang on every command when PyPI is unreachable. diff --git a/src/pdm/cli/actions.py b/src/pdm/cli/actions.py index 819b0940f2..94a07f455c 100644 --- a/src/pdm/cli/actions.py +++ b/src/pdm/cli/actions.py @@ -409,15 +409,17 @@ def get_latest_version(project: Project, expire_after: int = 7 * 24 * 3600) -> s state = json.loads(cache_file.read_text()) current_time = datetime.datetime.now(datetime.timezone.utc).timestamp() if (last_check := state.get("last-check")) and current_time - last_check < expire_after: - return cast(str, state["latest-version"]) + return cast("str | None", state.get("latest-version")) try: latest_version = get_latest_pdm_version_from_pypi(project) except Exception as e: project.core.ui.warn(f"Failed to get latest version: {e}", verbosity=termui.Verbosity.NORMAL) latest_version = None - if latest_version is None: - return None - state.update({"latest-version": latest_version, "last-check": current_time}) + # Record the check time even on failure, so that an unreachable index doesn't make + # every subsequent command wait for the request to time out again. + if latest_version is not None: + state["latest-version"] = latest_version + state["last-check"] = current_time with contextlib.suppress(OSError): cache_file.write_text(json.dumps(state)) return latest_version diff --git a/tests/cli/test_others.py b/tests/cli/test_others.py index 45d33c68e9..d99c073b44 100644 --- a/tests/cli/test_others.py +++ b/tests/cli/test_others.py @@ -232,6 +232,15 @@ def test_show_update_hint(pdm, project, monkeypatch): assert "Run `pdm config check_update false` to disable the check." in r.stderr +def test_update_check_caches_failure(project, mocker): + mocked = mocker.patch("pdm.cli.actions.get_latest_pdm_version_from_pypi", side_effect=Exception("network is down")) + + assert actions.get_latest_version(project) is None + assert actions.get_latest_version(project) is None + + assert mocked.call_count == 1 + + @pytest.mark.usefixtures("repository") def test_export_with_platform_markers(pdm, project): pdm(