Skip to content

Commit 64e8758

Browse files
committed
feat: add a solver.min-release-age-exclude-source config option
1 parent 874e2fc commit 64e8758

File tree

7 files changed

+126
-9
lines changed

7 files changed

+126
-9
lines changed

docs/configuration.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,26 @@ regardless of their upload age.
442442
poetry config solver.min-release-age-exclude "my-package,other-package"
443443
```
444444

445+
### `solver.min-release-age-exclude-source`
446+
447+
**Type**: `string`
448+
449+
**Default**: *not set*
450+
451+
**Environment Variable**: `POETRY_SOLVER_MIN_RELEASE_AGE_EXCLUDE_SOURCE`
452+
453+
*Introduced in 2.4.0*
454+
455+
A comma-separated list of source names or URLs that should be excluded from the
456+
[`solver.min-release-age`](#solvermin-release-age) filter.
457+
All packages from these sources will always be considered by the solver,
458+
regardless of their upload age.
459+
Sources can be referenced by the name defined in `pyproject.toml` or by URL.
460+
461+
```bash
462+
poetry config solver.min-release-age-exclude-source "private-repo,https://example.com/simple/"
463+
```
464+
445465
### `system-git-client`
446466

447467
**Type**: `boolean`

src/poetry/config/config.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def int_normalizer(val: str) -> int:
4444
return int(val)
4545

4646

47+
def str_list_normalizer(val: str) -> list[str]:
48+
return [vs for v in val.split(",") if (vs := v.strip())]
49+
50+
4751
def build_config_setting_validator(val: str) -> bool:
4852
try:
4953
value = build_config_setting_normalizer(val)
@@ -114,9 +118,8 @@ def normalize(cls, policy: str) -> list[str]:
114118

115119
return list(
116120
{
117-
name.strip() if cls.is_reserved(name) else canonicalize_name(name)
118-
for name in policy.strip().split(",")
119-
if name
121+
name if cls.is_reserved(name) else canonicalize_name(name)
122+
for name in str_list_normalizer(policy)
120123
}
121124
)
122125

@@ -176,6 +179,7 @@ class Config:
176179
"lazy-wheel": True,
177180
"min-release-age": 0,
178181
"min-release-age-exclude": None,
182+
"min-release-age-exclude-source": None,
179183
},
180184
"system-git-client": False,
181185
"keyring": {
@@ -411,6 +415,9 @@ def _get_normalizer(name: str) -> Callable[[str], Any]:
411415
}:
412416
return PackageFilterPolicy.normalize
413417

418+
if name == "solver.min-release-age-exclude-source":
419+
return str_list_normalizer
420+
414421
if name.startswith("installer.build-config-settings."):
415422
return build_config_setting_normalizer
416423

src/poetry/console/commands/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from poetry.config.config import build_config_setting_normalizer
2020
from poetry.config.config import build_config_setting_validator
2121
from poetry.config.config import int_normalizer
22+
from poetry.config.config import str_list_normalizer
2223
from poetry.config.config_source import UNSET
2324
from poetry.config.config_source import ConfigSourceMigration
2425
from poetry.config.config_source import PropertyNotFoundError
@@ -107,6 +108,10 @@ def unique_config_values(self) -> dict[str, tuple[Any, Any]]:
107108
PackageFilterPolicy.validator,
108109
PackageFilterPolicy.normalize,
109110
),
111+
"solver.min-release-age-exclude-source": (
112+
lambda val: bool(val.strip()),
113+
str_list_normalizer,
114+
),
110115
"keyring.enabled": (boolean_validator, boolean_normalizer),
111116
"python.installation-dir": (str, lambda val: str(Path(val))),
112117
}

src/poetry/repositories/http_repository.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,21 @@ def __init__(
8484
self._min_release_age_cutoff: datetime | None = None
8585
self._min_release_age_exclude: set[NormalizedName] = set()
8686
if self._min_release_age:
87-
self._min_release_age_cutoff = datetime.now(tz=timezone.utc) - timedelta(
88-
days=self._min_release_age
87+
exclude_sources: set[str] = set(
88+
config.get("solver.min-release-age-exclude-source") or []
8989
)
90-
self._min_release_age_exclude = {
91-
canonicalize_name(n)
92-
for n in (config.get("solver.min-release-age-exclude") or [])
93-
}
90+
if self._is_name_excluded_from_min_release_age(
91+
exclude_sources
92+
) or self._is_url_excluded_from_min_release_age(exclude_sources):
93+
self._min_release_age = 0
94+
else:
95+
self._min_release_age_cutoff = datetime.now(
96+
tz=timezone.utc
97+
) - timedelta(days=self._min_release_age)
98+
self._min_release_age_exclude = {
99+
canonicalize_name(n)
100+
for n in (config.get("solver.min-release-age-exclude") or [])
101+
}
94102
self._age_filtered_versions: dict[NormalizedName, set[Version]] = {}
95103
# We are tracking if a domain supports range requests or not to avoid
96104
# unnecessary requests.
@@ -101,6 +109,12 @@ def __init__(
101109
# - False: The domain does not support range requests for the files we tried.
102110
self._supports_range_requests: dict[str, bool] = {}
103111

112+
def _is_name_excluded_from_min_release_age(self, exclude_sources: set[str]) -> bool:
113+
return self.name.lower() in {s.lower() for s in exclude_sources}
114+
115+
def _is_url_excluded_from_min_release_age(self, exclude_sources: set[str]) -> bool:
116+
return self.url.rstrip("/") in {s.rstrip("/") for s in exclude_sources}
117+
104118
@property
105119
def session(self) -> Authenticator:
106120
return self._authenticator

tests/config/test_config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from poetry.config.config import Config
1616
from poetry.config.config import boolean_normalizer
1717
from poetry.config.config import int_normalizer
18+
from poetry.config.config import str_list_normalizer
1819
from poetry.utils.password_manager import PasswordManager
1920
from tests.helpers import flatten_dict
2021
from tests.helpers import isolated_environment
@@ -37,6 +38,18 @@ def get_options_based_on_normalizer(normalizer: Normalizer) -> Iterator[str]:
3738
yield k
3839

3940

41+
@pytest.mark.parametrize(
42+
("value", "expected"),
43+
[
44+
("foo, bar , baz", ["foo", "bar", "baz"]),
45+
(", ,", []),
46+
("", []),
47+
],
48+
)
49+
def test_str_list_normalizer(value: str, expected: list[str]) -> None:
50+
assert str_list_normalizer(value) == expected
51+
52+
4053
@pytest.mark.parametrize(
4154
("name", "value"),
4255
[
@@ -45,6 +58,7 @@ def get_options_based_on_normalizer(normalizer: Normalizer) -> Iterator[str]:
4558
("requests.max-retries", 0),
4659
("solver.min-release-age", 0),
4760
("solver.min-release-age-exclude", None),
61+
("solver.min-release-age-exclude-source", None),
4862
],
4963
)
5064
def test_config_get_default_value(

tests/console/commands/test_config.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def test_list_displays_default_value_if_not_set(
7777
solver.lazy-wheel = true
7878
solver.min-release-age = 0
7979
solver.min-release-age-exclude = null
80+
solver.min-release-age-exclude-source = null
8081
system-git-client = false
8182
virtualenvs.create = true
8283
virtualenvs.in-project = null
@@ -114,6 +115,7 @@ def test_list_displays_set_get_setting(
114115
solver.lazy-wheel = true
115116
solver.min-release-age = 0
116117
solver.min-release-age-exclude = null
118+
solver.min-release-age-exclude-source = null
117119
system-git-client = false
118120
virtualenvs.create = false
119121
virtualenvs.in-project = null
@@ -172,6 +174,7 @@ def test_unset_setting(
172174
solver.lazy-wheel = true
173175
solver.min-release-age = 0
174176
solver.min-release-age-exclude = null
177+
solver.min-release-age-exclude-source = null
175178
system-git-client = false
176179
virtualenvs.create = true
177180
virtualenvs.in-project = null
@@ -208,6 +211,7 @@ def test_unset_repo_setting(
208211
solver.lazy-wheel = true
209212
solver.min-release-age = 0
210213
solver.min-release-age-exclude = null
214+
solver.min-release-age-exclude-source = null
211215
system-git-client = false
212216
virtualenvs.create = true
213217
virtualenvs.in-project = null
@@ -345,6 +349,7 @@ def test_list_displays_set_get_local_setting(
345349
solver.lazy-wheel = true
346350
solver.min-release-age = 0
347351
solver.min-release-age-exclude = null
352+
solver.min-release-age-exclude-source = null
348353
system-git-client = false
349354
virtualenvs.create = false
350355
virtualenvs.in-project = null
@@ -391,6 +396,7 @@ def test_list_must_not_display_sources_from_pyproject_toml(
391396
solver.lazy-wheel = true
392397
solver.min-release-age = 0
393398
solver.min-release-age-exclude = null
399+
solver.min-release-age-exclude-source = null
394400
system-git-client = false
395401
virtualenvs.create = true
396402
virtualenvs.in-project = null
@@ -651,6 +657,23 @@ def test_config_solver_min_release_age_exclude(
651657
assert repo._min_release_age_exclude == {"my-pkg", "other-pkg"}
652658

653659

660+
def test_config_solver_min_release_age_exclude_source(
661+
tester: CommandTester, command_tester_factory: CommandTesterFactory
662+
) -> None:
663+
tester.execute("--local solver.min-release-age-exclude-source")
664+
assert tester.io.fetch_output().strip() == "null"
665+
666+
tester.io.clear_output()
667+
tester.execute(
668+
"--local solver.min-release-age-exclude-source"
669+
" 'private-repo,https://example.com/simple/'"
670+
)
671+
tester.execute("--local solver.min-release-age-exclude-source")
672+
output = tester.io.fetch_output().strip()
673+
assert "private-repo" in output
674+
assert "https://example.com/simple/" in output
675+
676+
654677
current_config = """\
655678
[experimental]
656679
system-git-client = true

tests/repositories/test_http_repository.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,40 @@ def test_min_release_age_exclude_default(config: Config) -> None:
9696
assert repo._min_release_age_exclude == set()
9797

9898

99+
@pytest.mark.parametrize(
100+
("exclude_sources", "expect_cutoff"),
101+
[
102+
([], True),
103+
(["bar"], True), # name does not match
104+
(["foo"], False), # matches repo name
105+
(["FOO"], False), # matches repo name (repo names are case-insensitive)
106+
(["https://foo.com"], False), # matches repo URL
107+
(["https://foo.com/"], False), # matches repo URL with trailing slash
108+
(["other", "https://foo.com"], False), # URL in list
109+
],
110+
)
111+
def test_min_release_age_exclude_source(
112+
config: Config,
113+
exclude_sources: list[str],
114+
expect_cutoff: bool,
115+
) -> None:
116+
config.merge(
117+
{
118+
"solver": {
119+
"min-release-age": 7,
120+
"min-release-age-exclude-source": exclude_sources,
121+
}
122+
}
123+
)
124+
repo = MockRepository(config=config)
125+
if expect_cutoff:
126+
assert repo._min_release_age == 7
127+
assert repo._min_release_age_cutoff is not None
128+
else:
129+
assert repo._min_release_age == 0
130+
assert repo._min_release_age_cutoff is None
131+
132+
99133
@pytest.mark.parametrize(
100134
("links", "expected"),
101135
[

0 commit comments

Comments
 (0)