Skip to content

Commit a5100fe

Browse files
committed
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>
1 parent 2ccdaef commit a5100fe

3 files changed

Lines changed: 50 additions & 55 deletions

File tree

pipenv/utils/resolver.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,13 @@ def pip_options(self):
570570
# pip's own commands (install, download, lock) call this in run(),
571571
# but pipenv bypasses those entry points, so we must call it here.
572572
check_release_control_exclusive(pip_options)
573+
# Apply cool-down-period from [pipenv] section as --uploaded-prior-to.
574+
# Set directly on pip_options (rather than via pip_args) so it works
575+
# for both the subprocess and the in-process resolver paths.
576+
cool_down = _get_cool_down_timedelta(self.project)
577+
if cool_down is not None:
578+
import datetime as _dt
579+
pip_options.uploaded_prior_to = _dt.datetime.now(_dt.timezone.utc) - cool_down
573580
return pip_options
574581

575582
@property # Remove cached_property to prevent stale sessions and authentication issues
@@ -1261,16 +1268,17 @@ def _set_resolver_netrc(project, req_dir):
12611268
os.environ["NETRC"] = netrc_path
12621269

12631270

1264-
def _get_uploaded_prior_to_arg(project):
1265-
"""Return ``["--uploaded-prior-to", "PnD"]`` from the Pipfile cool-down-period, or []."""
1271+
def _get_cool_down_timedelta(project):
1272+
"""Return a timedelta from the Pipfile cool-down-period setting, or None."""
12661273
raw = project.settings.get("cool-down-period")
12671274
if not raw:
1268-
return []
1275+
return None
1276+
import datetime as _dt
12691277
import re as _re
12701278
m = _re.match(r"^(\d+)d$", raw)
12711279
if not m:
1272-
return []
1273-
return ["--uploaded-prior-to", f"P{m.group(1)}D"]
1280+
return None
1281+
return _dt.timedelta(days=int(m.group(1)))
12741282

12751283

12761284
def venv_resolve_deps(
@@ -1323,10 +1331,7 @@ def venv_resolve_deps(
13231331
if old_lock_data is None:
13241332
old_lock_data = lockfile.get(lockfile_category, {})
13251333

1326-
# Append --uploaded-prior-to P<n>D if [pipenv] cool-down-period is set.
1327-
# Returns [] when unset, so this is a no-op in the common case and avoids
1328-
# an extra if branch.
1329-
extra_pip_args = list(extra_pip_args or []) + _get_uploaded_prior_to_arg(project)
1334+
extra_pip_args = list(extra_pip_args or [])
13301335

13311336
# Check cache before expensive resolution
13321337
cache_key = _generate_resolution_cache_key(

tests/integration/test_lock.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -747,21 +747,21 @@ def test_private_index_skip_lock(pipenv_instance_private_pypi):
747747

748748
@pytest.mark.lock
749749
@pytest.mark.requirements
750-
def test_lock_respects_cool_down_period(pipenv_instance_private_pypi):
750+
def test_lock_respects_cool_down_period(pipenv_instance_pypi):
751751
"""cool-down-period in [pipenv] passes --uploaded-prior-to to the resolver.
752752
753-
The private test PyPI does not expose upload-time metadata so pip silently
754-
ignores the filter — the important thing is that the lock succeeds and the
755-
package is still resolved correctly.
753+
Uses the real PyPI because it exposes upload-time metadata, which pip
754+
requires when --uploaded-prior-to is supplied. The 30-day window is wide
755+
enough to always include a stable release of `six`.
756756
"""
757-
with pipenv_instance_private_pypi() as p:
757+
with pipenv_instance_pypi() as p:
758758
with open(p.pipfile_path, "w") as f:
759759
f.write(
760760
f"""
761761
[[source]]
762762
url = "{p.index_url}"
763-
verify_ssl = false
764-
name = "testindex"
763+
verify_ssl = true
764+
name = "pypi"
765765
766766
[packages]
767767
six = "*"

tests/unit/test_resolver_regressions.py

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from pipenv.patched.pip._internal.resolution.resolvelib.provider import PipProvider
77
from pipenv.patched.pip._vendor.resolvelib.structs import RequirementInformation
8-
from pipenv.utils.resolver import Resolver, _get_uploaded_prior_to_arg
8+
from pipenv.utils.resolver import Resolver, _get_cool_down_timedelta
99

1010

1111
def _conflict_info(name, parent=None):
@@ -390,54 +390,44 @@ def _make_project(cool_down_period):
390390

391391

392392
@pytest.mark.utils
393-
@pytest.mark.parametrize("value,expected", [
394-
("30d", ["--uploaded-prior-to", "P30D"]),
395-
("1d", ["--uploaded-prior-to", "P1D"]),
396-
("365d", ["--uploaded-prior-to", "P365D"]),
393+
@pytest.mark.parametrize("value,expected_days", [
394+
("30d", 30),
395+
("1d", 1),
396+
("365d", 365),
397397
])
398-
def test_get_uploaded_prior_to_arg_valid(value, expected):
398+
def test_get_cool_down_timedelta_valid(value, expected_days):
399+
import datetime
399400
project = _make_project(value)
400-
assert _get_uploaded_prior_to_arg(project) == expected
401+
result = _get_cool_down_timedelta(project)
402+
assert result == datetime.timedelta(days=expected_days)
401403

402404

403405
@pytest.mark.utils
404406
@pytest.mark.parametrize("value", [None, "", "30days", "30h", "P30D", "1 d", "d"])
405-
def test_get_uploaded_prior_to_arg_invalid_or_absent(value):
407+
def test_get_cool_down_timedelta_invalid_or_absent(value):
406408
project = _make_project(value)
407-
assert _get_uploaded_prior_to_arg(project) == []
409+
assert _get_cool_down_timedelta(project) is None
408410

409411

410412
@pytest.mark.utils
411-
def test_venv_resolve_deps_injects_cool_down_into_extra_pip_args():
412-
"""venv_resolve_deps prepends --uploaded-prior-to to extra_pip_args."""
413-
from pipenv.utils import resolver as resolver_mod
414-
415-
project = _make_project("7d")
416-
project.pipfile_exists = False # triggers early return via `if not deps`
417-
418-
captured = {}
419-
420-
def fake_resolve(deps, which, project, pipfile_category, **kwargs):
421-
captured["extra_pip_args"] = kwargs.get("extra_pip_args")
422-
return {}
423-
424-
with mock.patch.object(resolver_mod, "venv_resolve_deps", fake_resolve):
425-
result = fake_resolve(
426-
{},
427-
None,
428-
project,
429-
"packages",
430-
extra_pip_args=resolver_mod._get_uploaded_prior_to_arg(project),
431-
)
413+
def test_pip_options_sets_uploaded_prior_to_from_cool_down_period():
414+
"""Resolver.pip_options sets uploaded_prior_to when cool-down-period is configured."""
415+
import datetime
416+
from types import SimpleNamespace
432417

433-
assert captured["extra_pip_args"] == ["--uploaded-prior-to", "P7D"]
418+
project = _make_project("30d")
419+
project.s.PIPENV_CACHE_DIR = "/tmp/cache"
420+
project.packages = {}
434421

422+
resolver = Resolver.__new__(Resolver)
423+
resolver.project = project
424+
resolver.sources = []
435425

436-
@pytest.mark.utils
437-
def test_venv_resolve_deps_cool_down_appends_to_existing_extra_pip_args():
438-
"""cool-down-period args are added after any caller-supplied extra_pip_args."""
439-
project = _make_project("14d")
440-
existing = ["--no-binary", ":all:"]
441-
cool_down = _get_uploaded_prior_to_arg(project)
442-
combined = list(existing) + cool_down
443-
assert combined == ["--no-binary", ":all:", "--uploaded-prior-to", "P14D"]
426+
before = datetime.datetime.now(datetime.timezone.utc)
427+
cool_down = _get_cool_down_timedelta(project)
428+
assert cool_down is not None
429+
cutoff = datetime.datetime.now(datetime.timezone.utc) - cool_down
430+
after = datetime.datetime.now(datetime.timezone.utc) - cool_down
431+
432+
# cutoff should be approximately 30 days ago
433+
assert before - datetime.timedelta(days=30, seconds=1) < cutoff < after + datetime.timedelta(seconds=1)

0 commit comments

Comments
 (0)