mirror of
https://github.com/pypa/pipenv.git
synced 2026-05-06 08:26:42 -04:00
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:
@@ -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(
|
||||
|
||||
@@ -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 = "*"
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user