Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion src/manage/indexutils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .exceptions import InvalidFeedError
from .logging import LOGGER
from .tagutils import tag_or_range, install_matches_any
from .tagutils import CompanyTag, tag_or_range, install_matches_any
from .verutils import Version

SCHEMA = {
Expand Down Expand Up @@ -195,6 +195,37 @@ def _patch(source_url, v):
return _SCHEMA_PATCHES.get(v.get("schema"), lambda _, v: v)(source_url, v)


def _tag_prefers_prerelease(tag):
"""True when *tag* asks for a prerelease, such as '3-dev' or '3.16.0a1'.

Tag sorting splits '-dev' and 'a1' off the numeric version, so the version
component alone is not a prerelease. The suffix is what the request means.
"""
if not isinstance(tag, str):
return False
stripped = tag.strip()
if not stripped or stripped.lstrip(" =")[:1] in "<>!":
return False
try:
parsed = tag_or_range(stripped)
except ValueError:
return False
if not isinstance(parsed, CompanyTag):
return False
for part in parsed._sortkey:
text = str(part).casefold()
if text == "dev":
return True
# Free-threaded tags keep the 't' ABI marker on the suffix ('a1t').
if len(text) > 1 and text.endswith("t"):
text = text[:-1]
if len(text) >= 2 and text.startswith("rc") and text[2:].isdigit():
return True
if text[:1] in "abc" and text[1:].isdigit():
return True
return False


class Index:
def __init__(self, source_url, d):
try:
Expand Down Expand Up @@ -232,6 +263,12 @@ def find_all(self, tags, *, seen_ids=None, loose_company=False, with_prerelease=
yield i

def find_to_install(self, tag, *, loose_company=False, prefer_prerelease=False):
# Final releases publish the same '-dev' tags as prereleases, so a
# request such as '3-dev' matches an older final on the first pass and
# the prerelease fallback never runs. A tag that is itself a
# prerelease has to include prereleases while scanning newest-first.
if not prefer_prerelease and _tag_prefers_prerelease(tag):
prefer_prerelease = True
tag_list = [tag] if tag else []
LOGGER.debug("Finding %s to install", tag_list)
for i in self.find_all(tag_list, loose_company=loose_company, with_prerelease=prefer_prerelease):
Expand Down
58 changes: 58 additions & 0 deletions tests/test_indexutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,64 @@ def test_install_lookup():
assert index.find_to_install("<3.12,!=3.10")["tag"] == "3.11.3"


def _feed_package(sort, tag, install_for):
data = fake_install_data(sort, sort_version=sort)
data["tag"] = tag
data["install-for"] = list(install_for)
return data


def test_find_to_install_dev_tag_prefers_prerelease():
# gh-428: finals publish the same '-dev' tags as prereleases, so '3-dev'
# used to stop at 3.15.0. '3.16-dev' already worked because no final
# matched that tag and the prerelease fallback ran.
def listed(full, minor):
major = minor.partition(".")[0]
return [
full, minor, major,
f"{minor}-dev", f"{major}-dev",
f"{full}-64", f"{minor}-64", f"{major}-64",
f"{minor}-dev-64", f"{major}-dev-64",
]

only_major = iu.Index("https://localhost/", {
"versions": [
_feed_package(
"3.16.0a1", "3.16-dev",
listed("3.16.0a1", "3.16"),
),
_feed_package(
"3.15.0", "3.15",
listed("3.15.0", "3.15"),
),
],
})
assert str(only_major.find_to_install("3-dev")["sort-version"]) == "3.16.0a1"
assert str(only_major.find_to_install(r"PythonCore\3-dev")["sort-version"]) == "3.16.0a1"
assert str(only_major.find_to_install("3-dev-64")["sort-version"]) == "3.16.0a1"
assert str(only_major.find_to_install("3.16-dev")["sort-version"]) == "3.16.0a1"
assert str(only_major.find_to_install("3")["sort-version"]) == "3.15.0"
assert str(only_major.find_to_install("3.15-dev")["sort-version"]) == "3.15.0"

# A newer prerelease of an already-final minor must win for '-dev' only.
same_minor = iu.Index("https://localhost/", {
"versions": [
_feed_package(
"3.16.1a1", "3.16-dev",
listed("3.16.1a1", "3.16"),
),
_feed_package(
"3.16.0", "3.16",
listed("3.16.0", "3.16"),
),
],
})
assert str(same_minor.find_to_install("3-dev")["sort-version"]) == "3.16.1a1"
assert str(same_minor.find_to_install("3.16-dev")["sort-version"]) == "3.16.1a1"
assert str(same_minor.find_to_install("3")["sort-version"]) == "3.16.0"
assert str(same_minor.find_to_install("3.16")["sort-version"]) == "3.16.0"


def test_select_package():
from manage.install_command import select_package

Expand Down