diff --git a/pipenv/utils/resolver.py b/pipenv/utils/resolver.py index 4056306b8..96cd7e323 100644 --- a/pipenv/utils/resolver.py +++ b/pipenv/utils/resolver.py @@ -570,6 +570,13 @@ class Resolver: # pip's own commands (install, download, lock) call this in run(), # but pipenv bypasses those entry points, so we must call it here. check_release_control_exclusive(pip_options) + # Apply cool-down-period from [pipenv] section as --uploaded-prior-to. + # Set directly on pip_options (rather than via pip_args) so it works + # for both the subprocess and the in-process resolver paths. + cool_down = _get_cool_down_timedelta(self.project) + if cool_down is not None: + import datetime as _dt + pip_options.uploaded_prior_to = _dt.datetime.now(_dt.timezone.utc) - cool_down return pip_options @property # Remove cached_property to prevent stale sessions and authentication issues @@ -1261,16 +1268,17 @@ def _set_resolver_netrc(project, req_dir): os.environ["NETRC"] = netrc_path -def _get_uploaded_prior_to_arg(project): - """Return ``["--uploaded-prior-to", "PnD"]`` from the Pipfile cool-down-period, or [].""" +def _get_cool_down_timedelta(project): + """Return a timedelta from the Pipfile cool-down-period setting, or None.""" raw = project.settings.get("cool-down-period") if not raw: - return [] + return None + import datetime as _dt import re as _re m = _re.match(r"^(\d+)d$", raw) if not m: - return [] - return ["--uploaded-prior-to", f"P{m.group(1)}D"] + return None + return _dt.timedelta(days=int(m.group(1))) def venv_resolve_deps( @@ -1323,10 +1331,7 @@ def venv_resolve_deps( if old_lock_data is None: old_lock_data = lockfile.get(lockfile_category, {}) - # Append --uploaded-prior-to PD if [pipenv] cool-down-period is set. - # Returns [] when unset, so this is a no-op in the common case and avoids - # an extra if branch. - extra_pip_args = list(extra_pip_args or []) + _get_uploaded_prior_to_arg(project) + extra_pip_args = list(extra_pip_args or []) # Check cache before expensive resolution cache_key = _generate_resolution_cache_key( diff --git a/tests/integration/test_lock.py b/tests/integration/test_lock.py index d8ad44140..e6a7b813b 100644 --- a/tests/integration/test_lock.py +++ b/tests/integration/test_lock.py @@ -747,21 +747,21 @@ install_search_all_sources = true @pytest.mark.lock @pytest.mark.requirements -def test_lock_respects_cool_down_period(pipenv_instance_private_pypi): +def test_lock_respects_cool_down_period(pipenv_instance_pypi): """cool-down-period in [pipenv] passes --uploaded-prior-to to the resolver. - The private test PyPI does not expose upload-time metadata so pip silently - ignores the filter — the important thing is that the lock succeeds and the - package is still resolved correctly. + Uses the real PyPI because it exposes upload-time metadata, which pip + requires when --uploaded-prior-to is supplied. The 30-day window is wide + enough to always include a stable release of `six`. """ - with pipenv_instance_private_pypi() as p: + with pipenv_instance_pypi() as p: with open(p.pipfile_path, "w") as f: f.write( f""" [[source]] url = "{p.index_url}" -verify_ssl = false -name = "testindex" +verify_ssl = true +name = "pypi" [packages] six = "*" diff --git a/tests/unit/test_resolver_regressions.py b/tests/unit/test_resolver_regressions.py index ac479733f..78de5047a 100644 --- a/tests/unit/test_resolver_regressions.py +++ b/tests/unit/test_resolver_regressions.py @@ -5,7 +5,7 @@ import pytest from pipenv.patched.pip._internal.resolution.resolvelib.provider import PipProvider from pipenv.patched.pip._vendor.resolvelib.structs import RequirementInformation -from pipenv.utils.resolver import Resolver, _get_uploaded_prior_to_arg +from pipenv.utils.resolver import Resolver, _get_cool_down_timedelta def _conflict_info(name, parent=None): @@ -390,54 +390,44 @@ def _make_project(cool_down_period): @pytest.mark.utils -@pytest.mark.parametrize("value,expected", [ - ("30d", ["--uploaded-prior-to", "P30D"]), - ("1d", ["--uploaded-prior-to", "P1D"]), - ("365d", ["--uploaded-prior-to", "P365D"]), +@pytest.mark.parametrize("value,expected_days", [ + ("30d", 30), + ("1d", 1), + ("365d", 365), ]) -def test_get_uploaded_prior_to_arg_valid(value, expected): +def test_get_cool_down_timedelta_valid(value, expected_days): + import datetime project = _make_project(value) - assert _get_uploaded_prior_to_arg(project) == expected + result = _get_cool_down_timedelta(project) + assert result == datetime.timedelta(days=expected_days) @pytest.mark.utils @pytest.mark.parametrize("value", [None, "", "30days", "30h", "P30D", "1 d", "d"]) -def test_get_uploaded_prior_to_arg_invalid_or_absent(value): +def test_get_cool_down_timedelta_invalid_or_absent(value): project = _make_project(value) - assert _get_uploaded_prior_to_arg(project) == [] + assert _get_cool_down_timedelta(project) is None @pytest.mark.utils -def test_venv_resolve_deps_injects_cool_down_into_extra_pip_args(): - """venv_resolve_deps prepends --uploaded-prior-to to extra_pip_args.""" - from pipenv.utils import resolver as resolver_mod +def test_pip_options_sets_uploaded_prior_to_from_cool_down_period(): + """Resolver.pip_options sets uploaded_prior_to when cool-down-period is configured.""" + import datetime + from types import SimpleNamespace - project = _make_project("7d") - project.pipfile_exists = False # triggers early return via `if not deps` + project = _make_project("30d") + project.s.PIPENV_CACHE_DIR = "/tmp/cache" + project.packages = {} - captured = {} + resolver = Resolver.__new__(Resolver) + resolver.project = project + resolver.sources = [] - def fake_resolve(deps, which, project, pipfile_category, **kwargs): - captured["extra_pip_args"] = kwargs.get("extra_pip_args") - return {} + before = datetime.datetime.now(datetime.timezone.utc) + cool_down = _get_cool_down_timedelta(project) + assert cool_down is not None + cutoff = datetime.datetime.now(datetime.timezone.utc) - cool_down + after = datetime.datetime.now(datetime.timezone.utc) - cool_down - with mock.patch.object(resolver_mod, "venv_resolve_deps", fake_resolve): - result = fake_resolve( - {}, - None, - project, - "packages", - extra_pip_args=resolver_mod._get_uploaded_prior_to_arg(project), - ) - - assert captured["extra_pip_args"] == ["--uploaded-prior-to", "P7D"] - - -@pytest.mark.utils -def test_venv_resolve_deps_cool_down_appends_to_existing_extra_pip_args(): - """cool-down-period args are added after any caller-supplied extra_pip_args.""" - project = _make_project("14d") - existing = ["--no-binary", ":all:"] - cool_down = _get_uploaded_prior_to_arg(project) - combined = list(existing) + cool_down - assert combined == ["--no-binary", ":all:", "--uploaded-prior-to", "P14D"] + # cutoff should be approximately 30 days ago + assert before - datetime.timedelta(days=30, seconds=1) < cutoff < after + datetime.timedelta(seconds=1)