feat: support cool-down-period in [pipenv] section

Reads the new `cool-down-period = "<n>d"` Pipfile setting introduced in
plette 2.2.1 and forwards it to pip's `--uploaded-prior-to P<n>D` flag
during dependency resolution, restricting the resolver to package versions
uploaded at least N days ago.

Includes unit tests for the arg-conversion helper and the injection into
`extra_pip_args`, an integration test verifying the lock succeeds with the
setting present, and documentation in docs/pipfile.md.

Signed-off-by: Oz Tiram <oz.tiram@gmail.com>
This commit is contained in:
Oz Tiram
2026-04-29 11:28:28 +02:00
parent dc9337d62a
commit cff758f706
5 changed files with 139 additions and 1 deletions
+19
View File
@@ -210,11 +210,30 @@ The `[pipenv]` section controls Pipenv's behavior:
```toml
[pipenv]
allow_prereleases = true # Allow pre-release versions
cool-down-period = "30d" # Only resolve packages uploaded at least N days ago
disable_pip_input = true # Prevent pipenv from asking for input
install_search_all_sources = true # Search all sources when installing from lock
sort_pipfile = true # Sort packages alphabetically
```
#### `cool-down-period`
Restricts the resolver to package versions that were uploaded to the index at least
the specified number of days ago. This gives newly-published releases time to be
vetted by the community before they are automatically pulled into your project.
The value must be a string in `<int>d` format (e.g. `"30d"` for 30 days). Internally
pipenv translates this to pip's `--uploaded-prior-to P30D` flag, which is only
effective against indexes that expose upload-time metadata as described in the
[Simple Repository API](https://packaging.python.org/en/latest/specifications/simple-repository-api/).
When the index does not provide upload-time metadata (e.g. most private mirrors) the
setting is accepted but has no filtering effect.
```toml
[pipenv]
cool-down-period = "30d" # ignore any release uploaded in the last 30 days
```
### Custom Package Categories
You can define custom package categories beyond the standard `packages` and `dev-packages`:
+4
View File
@@ -0,0 +1,4 @@
Added support for ``cool-down-period`` in the ``[pipenv]`` section of the Pipfile.
Setting ``cool-down-period = "30d"`` instructs the resolver to only consider
package versions uploaded at least the specified number of days ago, via pip's
``--uploaded-prior-to`` flag.
+17
View File
@@ -1261,6 +1261,18 @@ 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 []."""
raw = project.settings.get("cool-down-period")
if not raw:
return []
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"]
def venv_resolve_deps(
deps,
which,
@@ -1315,6 +1327,11 @@ 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)
# Check cache before expensive resolution
cache_key = _generate_resolution_cache_key(
deps,
+30
View File
@@ -743,3 +743,33 @@ install_search_all_sources = true
f.write(contents)
c = p.pipenv("install --skip-lock")
assert c.returncode == 0
@pytest.mark.lock
@pytest.mark.requirements
def test_lock_respects_cool_down_period(pipenv_instance_private_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.
"""
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
f.write(
f"""
[[source]]
url = "{p.index_url}"
verify_ssl = false
name = "testindex"
[packages]
six = "*"
[pipenv]
cool-down-period = "30d"
"""
)
c = p.pipenv("lock")
assert c.returncode == 0, c.stderr
assert "six" in p.lockfile["default"]
+69 -1
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
from pipenv.utils.resolver import Resolver, _get_uploaded_prior_to_arg
def _conflict_info(name, parent=None):
@@ -373,3 +373,71 @@ def test_process_resolver_results_does_not_scan_reverse_dependencies():
assert processed == [{"name": "requests", "version": "==2.32.0"}]
project.environment.reverse_dependencies.assert_not_called()
# ---------------------------------------------------------------------------
# cool-down-period / --uploaded-prior-to tests
# ---------------------------------------------------------------------------
def _make_project(cool_down_period):
"""Return a mock project whose [pipenv] section contains cool-down-period."""
project = mock.MagicMock()
settings = {}
if cool_down_period is not None:
settings["cool-down-period"] = cool_down_period
project.settings = settings
return project
@pytest.mark.utils
@pytest.mark.parametrize("value,expected", [
("30d", ["--uploaded-prior-to", "P30D"]),
("1d", ["--uploaded-prior-to", "P1D"]),
("365d", ["--uploaded-prior-to", "P365D"]),
])
def test_get_uploaded_prior_to_arg_valid(value, expected):
project = _make_project(value)
assert _get_uploaded_prior_to_arg(project) == expected
@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):
project = _make_project(value)
assert _get_uploaded_prior_to_arg(project) == []
@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
project = _make_project("7d")
project.pipfile_exists = False # triggers early return via `if not deps`
captured = {}
def fake_resolve(deps, which, project, pipfile_category, **kwargs):
captured["extra_pip_args"] = kwargs.get("extra_pip_args")
return {}
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"]