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
6 changes: 4 additions & 2 deletions src/manage/indexutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,11 @@ def find_all(self, tags, *, seen_ids=None, loose_company=False, with_prerelease=
seen_ids.add(i["id"].casefold())
yield i

def find_to_install(self, tag, *, loose_company=False, prefer_prerelease=False):
tag_list = [tag] if tag else []
def find_to_install(self, tag, *, loose_company=False, prefer_prerelease=None):
tag_list = [tag_or_range(tag)] if tag else []
LOGGER.debug("Finding %s to install", tag_list)
if prefer_prerelease is None:
prefer_prerelease = any(t.prefers_prerelease for t in tag_list)
for i in self.find_all(tag_list, loose_company=loose_company, with_prerelease=prefer_prerelease):
return {**i, "source": self.source_url}
if not loose_company:
Expand Down
36 changes: 30 additions & 6 deletions src/manage/installs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ def _make_sort_key(install):
)


def _make_sort_key_ignore_pre(install):
# Alternate sort key for "-dev" that treats prerelease as equal to release
return (
CompanyTag(install.get("company"), install.get("tag")),
Version(install["sort-version"]),
)


def _get_installs(install_dir):
for d in Path(install_dir).iterdir():
p = d / "__install__.json"
Expand Down Expand Up @@ -258,8 +266,6 @@ def get_matching_install_tags(
else:
matches.append((i, t))
matched_any = True
if single_tag and matched_any:
break
if LOGGER.would_log_to_console(DEBUG):
# Don't bother listing all installs unless the user has asked
# for console output.
Expand Down Expand Up @@ -298,20 +304,33 @@ def get_matching_install_tags(
# If none or only prereleases, keep them all
if default_platform:
default_platform = default_platform.casefold()
# Remember whether all source versions were prereleases, so we don't
# reset our results if all post-filter versions are prereleases.
all_pre = all(Version(i["sort-version"]).is_prerelease for i, t in best)
best2 = best
best = [(i, t) for i, t in best
if i.get("__any-platform")
or t["tag"].casefold().endswith(default_platform)]
LOGGER.debug("default_platform '%s' matched %s %s", default_platform,
len(best), "install" if len(best) == 1 else "installs")
if (
not best or
(not all_pre and all(Version(i["sort-version"]).is_prerelease for i, t in best))
):
if not best:
LOGGER.debug("Reusing unfiltered list because all candidates were "
"excluded")
best = best2
elif (not all_pre
and (not tag or not tag.prefers_prerelease)
and all(Version(i["sort-version"]).is_prerelease for i, t in best)):
LOGGER.debug("Reusing unfiltered list")
best = best2

if best and single_tag:
if tag.prefers_prerelease:
best2 = sorted(best, key=lambda i_t: _make_sort_key_ignore_pre(i_t[0]))
for i_t in best2:
if Version(i_t[0]["sort-version"]).is_prerelease:
return [i_t]
return best[:1]

return best


Expand Down Expand Up @@ -357,6 +376,11 @@ def get_install_to_run(
default_platform=default_platform,
)

if tag and tag.prefers_prerelease:
best2 = sorted(best, key=lambda i_t: _make_sort_key_ignore_pre(i_t[0]))
for i, t in best2:
if Version(i["sort-version"]).is_prerelease:
return _patch_install_to_run(i, t)
if best:
return _patch_install_to_run(*best[0])

Expand Down
16 changes: 10 additions & 6 deletions src/manage/list_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,15 +354,19 @@ def execute(cmd):
if first_exc:
raise SystemExit(1) from first_exc
if cmd.one:
# Pick the first non-prerelease that'll install for our platform
best = [i for i in installs
if any(t.endswith(plat) for t in i.get("install-for", []))]
for i in best:
if not i["sort-version"].is_prerelease:
installs = [i]
break
else:
if any(t.prefers_prerelease for t in tags):
# First result will be the latest prerelease (or release)
installs = best[:1] or installs
Comment on lines +359 to 361
else:
# Pick the first non-prerelease that'll install for our platform
for i in best:
if not i["sort-version"].is_prerelease:
installs = [i]
break
else:
installs = best[:1] or installs
elif cmd.install_dir:
try:
installs = cmd.get_installs(include_unmanaged=cmd.unmanaged)
Expand Down
24 changes: 24 additions & 0 deletions src/manage/tagutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ def startswith(self, other):
return not self.s
return self.s.startswith(other.s)

def endswith(self, other):
if not isinstance(other, type(self)):
return False
if not other.s:
return True
return self.s.endswith(other.s)

def __hash__(self):
return hash(self.s)

Expand Down Expand Up @@ -213,6 +220,12 @@ def match(self, pattern):
def satisfied_by(self, tag):
return tag.match(self)

@property
def prefers_prerelease(self, dev_text=_AscendingText("dev")):
if self._sortkey and self._sortkey[-1] == dev_text:
return True
return False

def __str__(self):
if self.is_core:
return f"{self.tag}{self.platform}"
Expand Down Expand Up @@ -340,6 +353,10 @@ def __init__(self, spec):
def __repr__(self):
return "<TagRange({})>".format(", ".join(repr(r) for r in self.ranges))

@property
def prefers_prerelease(self):
return any(r.prefers_prerelease for r in self.ranges)

def satisfied_by(self, tag):
return all(r(tag) for r in self.ranges)

Expand All @@ -359,6 +376,10 @@ def __init__(self, tag):
assert isinstance(tag, CompanyTag)
self.tag = tag

@property
def prefers_prerelease(self):
return self.tag.prefers_prerelease

def __repr__(self):
return f"{self.OP}{self.tag}"

Expand Down Expand Up @@ -404,6 +425,9 @@ class RangeExclude(Range):
def __call__(self, other):
return not other.matches_bound(self.tag)

@property
def prefers_prerelease(self):
return False

def tag_or_range(tag):
if not isinstance(tag, str):
Expand Down
5 changes: 5 additions & 0 deletions src/manage/verutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ class Version:
# Versions with more fields than this will be truncated.
MAX_FIELDS = 8

def __new__(self, s):
if isinstance(s, Version):
return s
return object.__new__(self)

def __init__(self, s):
import re
if isinstance(s, Version):
Expand Down
59 changes: 43 additions & 16 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,25 +276,45 @@ def make_install(tag, **kwargs):


def fake_get_installs(install_dir):
yield make_install("1.0")
yield make_install("1.0-32", sort_version="1.0", alias=[dict(name="py1.0.exe"), dict(name="py1.0-32.exe")])
yield make_install("1.0-64", sort_version="1.0", alias=[dict(name="py1.0.exe"), dict(name="py1.0-64.exe")])
yield make_install("2.0-64", sort_version="2.0")
yield make_install("2.0-arm64", sort_version="2.0")
yield make_install("3.0a1-32", sort_version="3.0a1")
yield make_install("3.0a1-64", sort_version="3.0a1")
yield make_install("1.1", company="Company", target="company.exe", targetw="companyw.exe")
yield make_install("1.1-64", sort_version="1.1", company="Company", target="company.exe", targetw="companyw.exe")
yield make_install("1.1-arm64", sort_version="1.1", company="Company", target="company.exe", targetw="companyw.exe")
yield make_install("2.1", sort_version="2.1", company="Company", target="company.exe", targetw="companyw.exe")
yield make_install("2.1-64", sort_version="2.1", company="Company", target="company.exe", targetw="companyw.exe")
installs = [
make_install("1.0"),
make_install("1.0-32", sort_version="1.0", alias=[dict(name="py1.0.exe"), dict(name="py1.0-32.exe")]),
make_install("1.0-64", sort_version="1.0", alias=[dict(name="py1.0.exe"), dict(name="py1.0-64.exe")]),
make_install("2.0-64", sort_version="2.0"),
make_install("2.0-arm64", sort_version="2.0"),
make_install("3.0a1-32", sort_version="3.0a1"),
make_install("3.0a1-64", sort_version="3.0a1"),
make_install("1.1", company="Company", target="company.exe", targetw="companyw.exe"),
make_install("1.1-64", sort_version="1.1", company="Company", target="company.exe", targetw="companyw.exe"),
make_install("1.1-arm64", sort_version="1.1", company="Company", target="company.exe", targetw="companyw.exe"),
make_install("2.1", sort_version="2.1", company="Company", target="company.exe", targetw="companyw.exe"),
make_install("2.1-64", sort_version="2.1", company="Company", target="company.exe", targetw="companyw.exe"),
]
installs.sort(key=manage.installs._make_sort_key)
return installs


def fake_get_installs2(install_dir):
yield make_install("1.0-32", sort_version="1.0")
yield make_install("3.0a1-32", sort_version="3.0a1", run_for=["3.0.1a1-32", "3.0-32", "3-32"])
yield make_install("3.0a1-64", sort_version="3.0a1", run_for=["3.0.1a1-64", "3.0-64", "3-64"])
yield make_install("3.0a1-arm64", sort_version="3.0a1", run_for=["3.0.1a1-arm64", "3.0-arm64", "3-arm64"])
installs = [
make_install("1.0-32", sort_version="1.0"),
make_install("3.0a1-32", sort_version="3.0a1", run_for=["3.0.1a1-32", "3.0-32", "3-32"]),
make_install("3.0a1-64", sort_version="3.0a1", run_for=["3.0.1a1-64", "3.0-64", "3-64"]),
make_install("3.0a1-arm64", sort_version="3.0a1", run_for=["3.0.1a1-arm64", "3.0-arm64", "3-arm64"]),
]
installs.sort(key=manage.installs._make_sort_key)
return installs


def fake_get_installs3(install_dir):
installs = [
make_install("1.0-32", sort_version="1.0"),
make_install("3.0-64", sort_version="3.0", run_for=["3.0.1-64", "3.0-64", "3-64", "3-dev-64"]),
make_install("3.0a1-32", sort_version="3.0a1", run_for=["3.0.1a1-32", "3.0-32", "3-32", "3-dev-32"]),
make_install("3.0a1-arm64", sort_version="3.0a1", run_for=["3.0.1a1-arm64", "3.0-arm64", "3-arm64", "3-dev-arm64"]),
make_install("3.1a1-64", sort_version="3.1a1", run_for=["3.1a1-64", "3.1-64", "3-64", "3-dev-64"]),
]
installs.sort(key=manage.installs._make_sort_key)
return installs


def fake_get_unmanaged_installs():
Expand All @@ -317,3 +337,10 @@ def patched_installs2(monkeypatch):
monkeypatch.setattr(manage.installs, "_get_installs", fake_get_installs2)
monkeypatch.setattr(manage.installs, "_get_unmanaged_installs", fake_get_unmanaged_installs)
monkeypatch.setattr(manage.installs, "_get_venv_install", fake_get_venv_install)


@pytest.fixture
def patched_installs3(monkeypatch):
monkeypatch.setattr(manage.installs, "_get_installs", fake_get_installs3)
monkeypatch.setattr(manage.installs, "_get_unmanaged_installs", fake_get_unmanaged_installs)
monkeypatch.setattr(manage.installs, "_get_venv_install", fake_get_venv_install)
34 changes: 34 additions & 0 deletions tests/test_installs.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,40 @@ def test_get_default_install_prerelease(patched_installs2):
assert m[0][0]["id"] == "PythonCore-3.0a1-32"


def test_get_default_install_prerelease_dev(patched_installs3):
inst = list(installs._get_installs("<none>"))
# Installs contains 3.0 for -64, and 3.0a1 for -32, -64 and -arm64

# Passing "-V:3" should always prefer a final release, rather than a
# prerelease, even if the platform isn't the default.
m = installs.get_matching_install_tags(inst, "3", None, "-32", single_tag=True)
assert m and m[0]
assert m[0][0]["id"] == "PythonCore-3.0-64"

m = installs.get_matching_install_tags(inst, "3", None, "-64", single_tag=True)
assert m and m[0]
assert m[0][0]["id"] == "PythonCore-3.0-64"

# Passing "-V:3-dev" should prefer a prerelease for the default platform,
# even if a final release for a different platform exists.
m = installs.get_matching_install_tags(inst, "3-dev", None, "-32", single_tag=True)
assert m and m[0]
assert m[0][0]["id"] == "PythonCore-3.0a1-32"

m = installs.get_matching_install_tags(inst, "3-dev", None, "-64", single_tag=True)
assert m and m[0]
assert m[0][0]["id"] == "PythonCore-3.1a1-64"

# Sanity check that explicit platforms always choose something matching
m = installs.get_matching_install_tags(inst, "3-32", None, "-32", single_tag=True)
assert m and m[0]
assert m[0][0]["id"] == "PythonCore-3.0a1-32"

m = installs.get_matching_install_tags(inst, "3-64", None, "-64", single_tag=True)
assert m and m[0]
assert m[0][0]["id"] == "PythonCore-3.0-64"


def test_get_install_to_run(patched_installs):
i = installs.get_install_to_run("<none>", None, "1.0")
assert i["id"] == "PythonCore-1.0"
Expand Down
20 changes: 19 additions & 1 deletion tests/test_tagutils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from manage.tagutils import CompanyTag, TagRange
from manage.tagutils import CompanyTag, TagRange, tag_or_range


@pytest.mark.parametrize("tag_str", [
Expand Down Expand Up @@ -42,6 +42,24 @@ def test_tag_match():
assert not CompanyTag("PythonCore\\4.56").match(CompanyTag("", "3"))


def test_prefer_prerelease():
assert not tag_or_range("3").prefers_prerelease
assert not tag_or_range("3-64").prefers_prerelease
assert not tag_or_range("3.13").prefers_prerelease
assert tag_or_range("3-dev").prefers_prerelease
assert tag_or_range("3.13-DEV").prefers_prerelease
assert tag_or_range("3.13-dev-64").prefers_prerelease

assert not tag_or_range(">3").prefers_prerelease
assert not tag_or_range("<3-64").prefers_prerelease
assert not tag_or_range("==3.13").prefers_prerelease
assert not tag_or_range("!=3.13").prefers_prerelease
assert not tag_or_range("!=3.13-dev").prefers_prerelease
assert tag_or_range(">3-dev").prefers_prerelease
assert tag_or_range("<3.13-DEV").prefers_prerelease
assert tag_or_range("==3.13-dev-64").prefers_prerelease


def test_tag_platform_match():
assert CompanyTag("3.10-64").match(CompanyTag("3.10"))
assert CompanyTag("3.10-64").match(CompanyTag("3.10-64"))
Expand Down
9 changes: 9 additions & 0 deletions tests/test_verutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
from manage.verutils import Version


def test_version_identity():
v1 = Version("1.0")
v2 = Version(v1)
v3 = Version("1.0")
assert v1 is v2
assert v1 is not v3
assert v1 == v3


@pytest.mark.parametrize("ver_str", [
"3", "3.1", "3.10", "3.1.2", "3.1.2.3",
"3a1", "3.1a1", "3.1.2a2", "3.1.2-a2", "3.1.a.4",
Expand Down