fix(cool-down-period): set uploaded_prior_to on pip_options instead of injecting into pip args

Passing --uploaded-prior-to via PIPENV_EXTRA_PIP_ARGS caused the subprocess
resolver to reject it as an unknown option. Instead, set pip_options.uploaded_prior_to
directly in Resolver.pip_options as a datetime object — the same pattern used for
other pip options like pre and cache_dir.

Rename _get_uploaded_prior_to_arg → _get_cool_down_timedelta to reflect that it now
returns a timedelta (or None) rather than a pip arg list. Update unit tests accordingly.

Switch the integration test from the private pypi fixture to pipenv_instance_pypi (real
PyPI), since pypiserver does not expose upload-time metadata and pip errors out rather
than silently ignoring the filter when --uploaded-prior-to is supplied.

Signed-off-by: Oz Tiram <oz.tiram@gmail.com>
This commit is contained in:
Oz Tiram
2026-04-29 14:58:18 +02:00
parent 52de026344
commit 71a05b917e
3 changed files with 49 additions and 54 deletions
+14 -9
View File
@@ -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 P<n>D 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(
+7 -7
View File
@@ -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 = "*"
+28 -38
View File
@@ -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)