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