-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #542 from dhellmann/versionmap-type
add VersionMap helper
- Loading branch information
Showing
2 changed files
with
159 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) |