Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add VersionMap helper #542

Merged
merged 1 commit into from
Jan 30, 2025
Merged
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
57 changes: 57 additions & 0 deletions src/fromager/versionmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""VersionMap interface for managing package settings in plugins."""

import typing

from packaging.requirements import Requirement
from packaging.version import Version


class VersionMap:
def __init__(
self, initial_content: dict[Version | str, typing.Any] | None = None
) -> None:
"""Initialize the VersionMap

Stores the inputs associating versions and arbitrary data. If the
versions are strings, they are converted to Version instances
internally. Any exceptions from the conversion are propagated.
"""
self._content: dict[Version, typing.Any] = {}
for k, v in (initial_content or {}).items():
self.add(k, v)

def add(self, key: Version | str, value: typing.Any) -> None:
"""Add a single value associated with a version

String keys are converted to Version instances. Any exceptions from the
conversion are propagated.
"""
if not isinstance(key, Version):
key = Version(key)
self._content[key] = value

def versions(self) -> typing.Iterable[Version]:
"""Return the known versions, sorted in descending order."""
return reversed(sorted(self._content.keys()))

def lookup(
self,
req: Requirement,
constraint: Requirement | None = None,
allow_prerelease: bool = False,
) -> tuple[Version, typing.Any]:
"""Return the matching version and associated value.

Finds the known version that best matches the requirement and optional
constraint and returns a tuple containing that version and the
associated value.
"""
for version in self.versions():
if not req.specifier.contains(version, prereleases=allow_prerelease):
continue
if constraint and not constraint.specifier.contains(
version, prereleases=allow_prerelease
):
continue
return (version, self._content[version])
raise ValueError(f"No version matched {req} with constraint {constraint}")
102 changes: 102 additions & 0 deletions tests/test_versionmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import pytest
from packaging.requirements import Requirement
from packaging.version import Version

from fromager.versionmap import VersionMap


def test_initialize():
m = VersionMap(
{
"1.2": "value for 1.2",
Version("1.3"): "value for 1.3",
"1.0": "value for 1.0",
}
)
assert list(m.versions()) == [Version("1.3"), Version("1.2"), Version("1.0")]


def test_lookup():
m = VersionMap(
{
"1.2": "value for 1.2",
Version("1.3"): "value for 1.3",
"1.0": "value for 1.0",
}
)
assert m.lookup(Requirement("pkg")) == (Version("1.3"), "value for 1.3")
assert m.lookup(Requirement("pkg>1.0")) == (Version("1.3"), "value for 1.3")
assert m.lookup(Requirement("pkg<1.3")) == (Version("1.2"), "value for 1.2")


def test_prerelease():
m = VersionMap(
{
Version("0.4.1b0"): "value for 0.4.1b0",
"1.2": "value for 1.2",
Version("1.3"): "value for 1.3",
"1.0": "value for 1.0",
"1.5.0a0": "value for 1.5.0a0",
}
)
assert m.lookup(Requirement("pkg")) == (Version("1.3"), "value for 1.3")
assert m.lookup(Requirement("pkg>1.0")) == (Version("1.3"), "value for 1.3")
assert m.lookup(Requirement("pkg<1.3")) == (Version("1.2"), "value for 1.2")
assert m.lookup(Requirement("pkg"), allow_prerelease=True) == (
Version("1.5.0a0"),
"value for 1.5.0a0",
)
with pytest.raises(ValueError):
assert (
m.lookup(Requirement("pkg"), Requirement("pkg<1.0")) == "value for 0.4.1b"
)
assert m.lookup(
Requirement("pkg"), Requirement("pkg<1.0"), allow_prerelease=True
) == (Version("0.4.1b0"), "value for 0.4.1b0")


def test_only_prerelease():
m = VersionMap(
{
Version("0.4.1b0"): "value for 0.4.1b0",
Version("0.6b0"): "value for 0.6b0",
}
)
assert m.lookup(
Requirement("pkg"), constraint=Requirement("pkg<0.6b"), allow_prerelease=True
) == (
Version("0.4.1b0"),
"value for 0.4.1b0",
)


def test_with_constraint():
m = VersionMap(
{
"1.2": "value for 1.2",
Version("1.3"): "value for 1.3",
"1.0": "value for 1.0",
}
)
assert m.lookup(Requirement("pkg"), Requirement("pkg<1.3")) == (
Version("1.2"),
"value for 1.2",
)
assert m.lookup(Requirement("pkg>1.0"), Requirement("pkg==1.2")) == (
Version("1.2"),
"value for 1.2",
)


def test_no_match():
m = VersionMap(
{
"1.2": "value for 1.2",
Version("1.3"): "value for 1.3",
"1.0": "value for 1.0",
}
)
with pytest.raises(ValueError):
m.lookup(Requirement("pkg"), Requirement("pkg<1.0"))
with pytest.raises(ValueError):
m.lookup(Requirement("pkg>1.0"), Requirement("pkg<1.0"))
Loading