Skip to content

Commit 7b7658c

Browse files
committed
feat: add a solver.min-release-age-exclude config option
1 parent 84bf64e commit 7b7658c

8 files changed

Lines changed: 119 additions & 9 deletions

File tree

docs/configuration.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,25 @@ If a source does not provide upload times for a release,
423423
that release is not filtered out by this setting.
424424
{{% /note %}}
425425

426+
### `solver.min-release-age-exclude`
427+
428+
**Type**: `string`
429+
430+
**Default**: *not set*
431+
432+
**Environment Variable**: `POETRY_SOLVER_MIN_RELEASE_AGE_EXCLUDE`
433+
434+
*Introduced in 2.4.0*
435+
436+
A comma-separated list of package names that should be excluded from the
437+
[`solver.min-release-age`](#solvermin-release-age) filter.
438+
Versions of these packages will always be considered by the solver,
439+
regardless of their upload age.
440+
441+
```bash
442+
poetry config solver.min-release-age-exclude "my-package,other-package"
443+
```
444+
426445
### `system-git-client`
427446

428447
**Type**: `boolean`

src/poetry/config/config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ class Config:
175175
"solver": {
176176
"lazy-wheel": True,
177177
"min-release-age": 0,
178+
"min-release-age-exclude": None,
178179
},
179180
"system-git-client": False,
180181
"keyring": {
@@ -403,7 +404,11 @@ def _get_normalizer(name: str) -> Callable[[str], Any]:
403404
}:
404405
return int_normalizer
405406

406-
if name in ["installer.no-binary", "installer.only-binary"]:
407+
if name in {
408+
"installer.no-binary",
409+
"installer.only-binary",
410+
"solver.min-release-age-exclude",
411+
}:
407412
return PackageFilterPolicy.normalize
408413

409414
if name.startswith("installer.build-config-settings."):

src/poetry/console/commands/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]:
103103
),
104104
"solver.lazy-wheel": (boolean_validator, boolean_normalizer),
105105
"solver.min-release-age": (lambda val: int(val) >= 0, int_normalizer),
106+
"solver.min-release-age-exclude": (
107+
PackageFilterPolicy.validator,
108+
PackageFilterPolicy.normalize,
109+
),
106110
"keyring.enabled": (boolean_validator, boolean_normalizer),
107111
"python.installation-dir": (str, lambda val: str(Path(val))),
108112
}

src/poetry/repositories/http_repository.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import requests.adapters
1818

1919
from packaging.metadata import parse_email
20+
from packaging.utils import canonicalize_name
2021
from poetry.core.constraints.version import Version
2122
from poetry.core.constraints.version import VersionConstraint
2223
from poetry.core.constraints.version import parse_constraint
@@ -81,10 +82,15 @@ def __init__(
8182

8283
self._min_release_age = config.get("solver.min-release-age", 0)
8384
self._min_release_age_cutoff: datetime | None = None
85+
self._min_release_age_exclude: set[NormalizedName] = set()
8486
if self._min_release_age:
8587
self._min_release_age_cutoff = datetime.now(tz=timezone.utc) - timedelta(
8688
days=self._min_release_age
8789
)
90+
self._min_release_age_exclude = {
91+
canonicalize_name(n)
92+
for n in (config.get("solver.min-release-age-exclude") or [])
93+
}
8894
self._age_filtered_versions: dict[NormalizedName, set[Version]] = {}
8995
# We are tracking if a domain supports range requests or not to avoid
9096
# unnecessary requests.
@@ -173,7 +179,10 @@ def _find_packages(
173179
if constraint.allows(version)
174180
]
175181

176-
if self._min_release_age_cutoff is not None:
182+
if (
183+
self._min_release_age_cutoff is not None
184+
and name not in self._min_release_age_exclude
185+
):
177186
filtered_out: set[Version] = set()
178187
accepted: list[tuple[Version, str | bool]] = []
179188
for version, yanked in versions:

tests/console/commands/test_config.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def test_list_displays_default_value_if_not_set(
7676
requests.max-retries = 0
7777
solver.lazy-wheel = true
7878
solver.min-release-age = 0
79+
solver.min-release-age-exclude = null
7980
system-git-client = false
8081
virtualenvs.create = true
8182
virtualenvs.in-project = null
@@ -112,6 +113,7 @@ def test_list_displays_set_get_setting(
112113
requests.max-retries = 0
113114
solver.lazy-wheel = true
114115
solver.min-release-age = 0
116+
solver.min-release-age-exclude = null
115117
system-git-client = false
116118
virtualenvs.create = false
117119
virtualenvs.in-project = null
@@ -169,6 +171,7 @@ def test_unset_setting(
169171
requests.max-retries = 0
170172
solver.lazy-wheel = true
171173
solver.min-release-age = 0
174+
solver.min-release-age-exclude = null
172175
system-git-client = false
173176
virtualenvs.create = true
174177
virtualenvs.in-project = null
@@ -204,6 +207,7 @@ def test_unset_repo_setting(
204207
requests.max-retries = 0
205208
solver.lazy-wheel = true
206209
solver.min-release-age = 0
210+
solver.min-release-age-exclude = null
207211
system-git-client = false
208212
virtualenvs.create = true
209213
virtualenvs.in-project = null
@@ -340,6 +344,7 @@ def test_list_displays_set_get_local_setting(
340344
requests.max-retries = 0
341345
solver.lazy-wheel = true
342346
solver.min-release-age = 0
347+
solver.min-release-age-exclude = null
343348
system-git-client = false
344349
virtualenvs.create = false
345350
virtualenvs.in-project = null
@@ -385,6 +390,7 @@ def test_list_must_not_display_sources_from_pyproject_toml(
385390
requests.max-retries = 0
386391
solver.lazy-wheel = true
387392
solver.min-release-age = 0
393+
solver.min-release-age-exclude = null
388394
system-git-client = false
389395
virtualenvs.create = true
390396
virtualenvs.in-project = null
@@ -624,6 +630,27 @@ def test_config_solver_min_release_age(
624630
assert repo._min_release_age == 3
625631

626632

633+
def test_config_solver_min_release_age_exclude(
634+
tester: CommandTester, command_tester_factory: CommandTesterFactory
635+
) -> None:
636+
tester.execute("--local solver.min-release-age-exclude")
637+
assert tester.io.fetch_output().strip() == "null"
638+
639+
repo = LegacyRepository("foo", "https://foo.com")
640+
assert repo._min_release_age_exclude == set()
641+
642+
tester.io.clear_output()
643+
tester.execute("--local solver.min-release-age 3")
644+
tester.execute("--local solver.min-release-age-exclude 'my-pkg,Other-Pkg'")
645+
tester.execute("--local solver.min-release-age-exclude")
646+
output = tester.io.fetch_output().strip()
647+
assert "my-pkg" in output
648+
assert "other-pkg" in output
649+
650+
repo = LegacyRepository("foo", "https://foo.com")
651+
assert repo._min_release_age_exclude == {"my-pkg", "other-pkg"}
652+
653+
627654
current_config = """\
628655
[experimental]
629656
system-git-client = true

tests/repositories/test_http_repository.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,33 @@ def test_min_release_age_and_cutoff_is_set(
6969
assert repo._min_release_age_cutoff is None
7070

7171

72+
def test_min_release_age_exclude_is_set(config: Config) -> None:
73+
config.merge(
74+
{
75+
"solver": {
76+
"min-release-age": 1,
77+
"min-release-age-exclude": ["My-Package", "other-pkg"],
78+
}
79+
}
80+
)
81+
repo = MockRepository(config=config)
82+
assert repo._min_release_age_exclude == {"my-package", "other-pkg"}
83+
84+
85+
def test_min_release_age_exclude_is_not_set_without_min_release_age(
86+
config: Config,
87+
) -> None:
88+
config.merge({"solver": {"min-release-age-exclude": ["My-Package", "other-pkg"]}})
89+
repo = MockRepository(config=config)
90+
assert repo._min_release_age_exclude == set()
91+
92+
93+
def test_min_release_age_exclude_default(config: Config) -> None:
94+
config.merge({"solver": {"min-release-age": 1}})
95+
repo = MockRepository(config=config)
96+
assert repo._min_release_age_exclude == set()
97+
98+
7299
@pytest.mark.parametrize(
73100
("links", "expected"),
74101
[

tests/repositories/test_legacy_repository.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import pytest
1212
import requests
1313

14+
from packaging.utils import NormalizedName
1415
from packaging.utils import canonicalize_name
1516
from poetry.core.constraints.version import Version
1617
from poetry.core.packages.dependency import Dependency
@@ -286,16 +287,23 @@ def test_find_packages_yanked(
286287
assert [str(p.version) for p in packages] == expected
287288

288289

289-
def test_find_packages_min_release_age(legacy_repository: TestLegacyRepository) -> None:
290+
@pytest.mark.parametrize(
291+
"min_release_age_exclude", [set(), {"a", "b"}, {"ipython", "other"}]
292+
)
293+
def test_find_packages_min_release_age(
294+
legacy_repository: TestLegacyRepository,
295+
min_release_age_exclude: set[NormalizedName],
296+
) -> None:
290297
"""Versions with files uploaded within min-release-age days are filtered."""
291298
repo = legacy_repository
292299
# ipython fixture upload times:
293300
# 4.1.0rc1: 2016-01-26, 5.7.0: 2018-05-10, 7.5.0: 2019-04-25
294301
repo._min_release_age_cutoff = datetime(2018, 10, 6, tzinfo=timezone.utc)
302+
repo._min_release_age_exclude = min_release_age_exclude
295303
packages = repo.find_packages(Factory.create_dependency("ipython", "*"))
296304

297305
# HTML API does not provide upload time
298-
if repo.json:
306+
if repo.json and "ipython" not in min_release_age_exclude:
299307
expected_versions = ["5.7.0"]
300308
expected_filtered = {"ipython": {Version.parse("7.5.0")}}
301309
else:

tests/repositories/test_pypi_repository.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import pytest
1010

11+
from packaging.utils import NormalizedName
1112
from packaging.utils import canonicalize_name
1213
from poetry.core.constraints.version import Version
1314
from poetry.core.packages.dependency import Dependency
@@ -86,7 +87,12 @@ def test_find_packages_yanked(
8687
assert [str(p.version) for p in packages] == expected
8788

8889

89-
def test_find_packages_min_release_age(pypi_repository: PyPiRepository) -> None:
90+
@pytest.mark.parametrize(
91+
"min_release_age_exclude", [set(), {"a", "b"}, {"requests", "other"}]
92+
)
93+
def test_find_packages_min_release_age(
94+
pypi_repository: PyPiRepository, min_release_age_exclude: set[NormalizedName]
95+
) -> None:
9096
"""Versions with files uploaded within min-release-age days are filtered."""
9197
repo = pypi_repository
9298
# requests fixture upload times:
@@ -96,17 +102,22 @@ def test_find_packages_min_release_age(pypi_repository: PyPiRepository) -> None:
96102
# Cutoff = 2017-08-10. Versions with any file uploaded after that are filtered.
97103
# Note that 2.19.0 is uploaded in the future ;)
98104
repo._min_release_age_cutoff = datetime(2017, 8, 10, tzinfo=timezone.utc)
105+
repo._min_release_age_exclude = min_release_age_exclude
99106
packages = repo.find_packages(Factory.create_dependency("requests", ">=2.18.0"))
100107

101-
assert [str(p.version) for p in packages] == [
108+
expected_versions = [
102109
"2.18.0",
103110
"2.18.1",
104111
"2.18.2",
105112
"2.18.3",
106113
]
107-
assert repo._age_filtered_versions == {
108-
"requests": {Version.parse("2.18.4"), Version.parse("2.19.0")}
109-
}
114+
expected_filtered = {"requests": {Version.parse("2.18.4"), Version.parse("2.19.0")}}
115+
if "requests" in min_release_age_exclude:
116+
expected_versions += ["2.18.4", "2.19.0"]
117+
expected_filtered = {}
118+
119+
assert [str(p.version) for p in packages] == expected_versions
120+
assert repo._age_filtered_versions == expected_filtered
110121

111122

112123
def test_package(

0 commit comments

Comments
 (0)