From cf969dffa1b1d6bdd4b2c17fff4f0d6d871e6818 Mon Sep 17 00:00:00 2001 From: Jesper Nielsen <44195043+jesnie@users.noreply.github.com> Date: Thu, 31 Aug 2023 18:13:49 +0200 Subject: [PATCH] Allow coersion from releases to specifiers and requirements. (#36) --- README.md | 8 ++- TODO.txt | 13 ----- compreq/__init__.py | 4 ++ compreq/lazy.py | 82 +++++++++++++++++++++++++------ compreq/versiontokens.py | 3 +- tests/test_lazy.py | 98 ++++++++++++++++++++++++++++++++++--- tests/test_versiontokens.py | 53 ++++++++++---------- 7 files changed, 197 insertions(+), 64 deletions(-) delete mode 100644 TODO.txt diff --git a/README.md b/README.md index 3ba83a6..3f391b4 100644 --- a/README.md +++ b/README.md @@ -74,4 +74,10 @@ with cr.PoetryPyprojectFile.open() as pyproject: ) ``` -Or see [requirements.py](https://github.com/jesnie/compreq/blob/main/requirements.py). \ No newline at end of file +Or see [requirements.py](https://github.com/jesnie/compreq/blob/main/requirements.py). + + +# References: + +https://peps.python.org/pep-0440/ +https://packaging.pypa.io/en/stable diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index 4c94c89..0000000 --- a/TODO.txt +++ /dev/null @@ -1,13 +0,0 @@ -* Make a release a requirment? - - -Alternatives: -- https://github.com/dependabot -- https://docs.renovatebot.com/python/ - -Reference: -https://peps.python.org/pep-0440/ -https://packaging.pypa.io/en/stable - -Example: -https://matplotlib.org/stable/devel/min_dep_policy.html diff --git a/compreq/__init__.py b/compreq/__init__.py index fd4edd4..2d8e74d 100644 --- a/compreq/__init__.py +++ b/compreq/__init__.py @@ -26,6 +26,7 @@ EagerLazyRelease, EagerLazyReleaseSet, EagerLazyRequirementSet, + EagerLazySpecifier, EagerLazySpecifierSet, EagerLazyVersion, LazyRelease, @@ -37,6 +38,7 @@ LazyVersion, PreLazyReleaseSet, ProdLazyReleaseSet, + ReleaseLazySpecifier, ReleaseLazyVersion, SpecifierLazyReleaseSet, SpecifierOperator, @@ -153,6 +155,7 @@ "EagerLazyRelease", "EagerLazyReleaseSet", "EagerLazyRequirementSet", + "EagerLazySpecifier", "EagerLazySpecifierSet", "EagerLazyVersion", "FloorLazyVersion", @@ -187,6 +190,7 @@ "REL_MINOR", "RelativeToFirstNonZeroLevel", "Release", + "ReleaseLazySpecifier", "ReleaseLazyVersion", "ReleaseSet", "RequirementSet", diff --git a/compreq/lazy.py b/compreq/lazy.py index 2d94870..507c48a 100644 --- a/compreq/lazy.py +++ b/compreq/lazy.py @@ -313,8 +313,7 @@ def get_specifier_operator(op: AnySpecifierOperator) -> SpecifierOperator: raise AssertionError(f"Unknown type of operator: {type(op)}") -@dataclass(order=True, frozen=True) -class LazySpecifier: +class LazySpecifier(ABC): """ Strategy for computing a `Specifier` in the context of a distribution. @@ -324,14 +323,9 @@ class LazySpecifier: lazy_specifier_set = lazy_specifier_1 & lazy_specifier_2 """ - op: SpecifierOperator - version: LazyVersion - + @abstractmethod async def resolve(self, context: DistributionContext) -> Specifier: """Compute the `Specifier`.""" - op = self.op - version = await self.version.resolve(context) - return Specifier(f"{op.value}{version}") @overload def __and__(self, rhs: AnySpecifierSet) -> LazySpecifierSet: @@ -356,7 +350,27 @@ def __rand__(self, lhs: AnyRequirement) -> LazySpecifierSet | LazyRequirement: return compose(lhs, self) -AnySpecifier: TypeAlias = str | Specifier | LazySpecifier +@dataclass(order=True, frozen=True) +class EagerLazySpecifier(LazySpecifier): + op: SpecifierOperator + version: LazyVersion + + async def resolve(self, context: DistributionContext) -> Specifier: + op = self.op + version = await self.version.resolve(context) + return Specifier(f"{op.value}{version}") + + +@dataclass(order=True, frozen=True) +class ReleaseLazySpecifier(LazySpecifier): + release: LazyRelease + + async def resolve(self, context: DistributionContext) -> Specifier: + release = await self.release.resolve(context) + return Specifier(f"=={release.version}") + + +AnySpecifier: TypeAlias = str | Release | LazyRelease | Specifier | LazySpecifier """Type alias for anything that can be converted to a `LazySpecifier`.""" @@ -364,10 +378,14 @@ def get_lazy_specifier(specifier: AnySpecifier) -> LazySpecifier: """Get a `LazySpecifier` for the given specifier-like value.""" if isinstance(specifier, str): specifier = Specifier(specifier) + if isinstance(specifier, Release): + specifier = EagerLazyRelease(specifier) + if isinstance(specifier, LazyRelease): + specifier = ReleaseLazySpecifier(specifier) if isinstance(specifier, Specifier): op = get_specifier_operator(specifier.operator) version = get_lazy_version(specifier.version) - specifier = LazySpecifier(op, version) + specifier = EagerLazySpecifier(op, version) if isinstance(specifier, LazySpecifier): return specifier raise AssertionError(f"Unknown type of specifier: {type(specifier)}") @@ -429,7 +447,9 @@ async def resolve(self, context: DistributionContext) -> SpecifierSet: return SpecifierSet(",".join(str(s) for s in specifiers)) -AnySpecifierSet: TypeAlias = str | Specifier | LazySpecifier | SpecifierSet | LazySpecifierSet +AnySpecifierSet: TypeAlias = ( + str | Release | LazyRelease | Specifier | LazySpecifier | SpecifierSet | LazySpecifierSet +) """Type alias for anything that can be converted to a `LazySpecifierSet`.""" @@ -437,7 +457,7 @@ def get_lazy_specifier_set(specifier_set: AnySpecifierSet) -> LazySpecifierSet: """Get a `LazySpecifierSet` for the given specifier-set-like value.""" if isinstance(specifier_set, str): specifier_set = SpecifierSet(specifier_set) - if isinstance(specifier_set, Specifier): + if isinstance(specifier_set, (Release, LazyRelease, Specifier)): specifier_set = get_lazy_specifier(specifier_set) if isinstance(specifier_set, LazySpecifier): specifier_set = EagerLazySpecifierSet(frozenset([specifier_set])) @@ -575,6 +595,8 @@ async def resolve(self, context: Context) -> Requirement: AnyRequirement: TypeAlias = ( str + | Release + | LazyRelease | Specifier | LazySpecifier | SpecifierSet @@ -589,6 +611,16 @@ def get_lazy_requirement(requirement: AnyRequirement) -> LazyRequirement: """Get a `LazyRequirement` for the given requirement-like value.""" if isinstance(requirement, str): requirement = Requirement(requirement) + if isinstance(requirement, Release): + requirement = EagerLazyRelease(requirement) + if isinstance(requirement, LazyRelease): + distribution = requirement.get_distribution() + assert distribution is not None, requirement + requirement = replace( + EMPTY_REQUIREMENT, + distribution=distribution, + specifier=get_lazy_specifier_set(requirement), + ) if isinstance(requirement, (Specifier, LazySpecifier, SpecifierSet)): requirement = get_lazy_specifier_set(requirement) if isinstance(requirement, LazySpecifierSet): @@ -609,17 +641,24 @@ def get_lazy_requirement(requirement: AnyRequirement) -> LazyRequirement: @overload -def compose(lhs: AnySpecifierSet, rhs: AnySpecifierSet) -> LazySpecifierSet: +def compose( + lhs: str | Specifier | LazySpecifier | SpecifierSet | LazySpecifierSet, + rhs: str | Specifier | LazySpecifier | SpecifierSet | LazySpecifierSet, +) -> LazySpecifierSet: ... @overload -def compose(lhs: AnyRequirement, rhs: Requirement | LazyRequirement) -> LazyRequirement: +def compose( + lhs: AnyRequirement, rhs: Release | LazyRelease | Requirement | LazyRequirement +) -> LazyRequirement: ... @overload -def compose(lhs: Requirement | LazyRequirement, rhs: AnyRequirement) -> LazyRequirement: +def compose( + lhs: Release | LazyRelease | Requirement | LazyRequirement, rhs: AnyRequirement +) -> LazyRequirement: ... @@ -714,6 +753,8 @@ async def resolve(self, context: Context) -> RequirementSet: AnyRequirementSet: TypeAlias = ( str + | Release + | LazyRelease | Specifier | LazySpecifier | SpecifierSet @@ -732,7 +773,16 @@ def get_lazy_requirement_set(requirement_set: AnyRequirementSet) -> LazyRequirem """Get a `LazyRequirementSet` for the given requirement-set-like value.""" if isinstance( requirement_set, - (str, Specifier, LazySpecifier, SpecifierSet, LazySpecifierSet, Requirement), + ( + str, + Release, + LazyRelease, + Specifier, + LazySpecifier, + SpecifierSet, + LazySpecifierSet, + Requirement, + ), ): requirement_set = get_lazy_requirement(requirement_set) if isinstance(requirement_set, LazyRequirement): diff --git a/compreq/versiontokens.py b/compreq/versiontokens.py index b53b292..d397b8e 100644 --- a/compreq/versiontokens.py +++ b/compreq/versiontokens.py @@ -3,6 +3,7 @@ from compreq.lazy import ( AnySpecifierOperator, AnyVersion, + EagerLazySpecifier, LazySpecifier, SpecifierOperator, get_lazy_version, @@ -18,7 +19,7 @@ class VersionToken: def require(self, op: AnySpecifierOperator, version: AnyVersion) -> LazySpecifier: op = get_specifier_operator(op) version = get_lazy_version(version) - return LazySpecifier(op, version) + return EagerLazySpecifier(op, version) def __call__(self, op: AnySpecifierOperator, version: AnyVersion) -> LazySpecifier: return self.require(op, version) diff --git a/tests/test_lazy.py b/tests/test_lazy.py index 52afb41..a41d381 100644 --- a/tests/test_lazy.py +++ b/tests/test_lazy.py @@ -327,31 +327,49 @@ def test_get_specifier_operator( assert cr.get_specifier_operator(op) == expected -async def test_lazy_specifier() -> None: +async def test_eager_lazy_specifier() -> None: op = cr.SpecifierOperator.LT version = MagicMock(cr.LazyVersion) version.resolve.return_value = Version("1.5.0") - lazy = cr.LazySpecifier(op, version) + lazy = cr.EagerLazySpecifier(op, version) context = MagicMock(cr.DistributionContext) assert Specifier("<1.5.0") == await lazy.resolve(context) version.resolve.assert_called_once_with(context) +async def test_release_lazy_specifier() -> None: + release = MagicMock(cr.LazyRelease) + release.resolve.return_value = fake_release(version="1.7.8") + lazy = cr.ReleaseLazySpecifier(release) + + context = MagicMock(cr.DistributionContext) + assert Specifier("==1.7.8") == await lazy.resolve(context) + release.resolve.assert_called_once_with(context) + + @pytest.mark.parametrize( "specifier,expected", [ ( ">=1.1.0", - cr.LazySpecifier(cr.SpecifierOperator.GE, cr.EagerLazyVersion(Version("1.1.0"))), + cr.EagerLazySpecifier(cr.SpecifierOperator.GE, cr.EagerLazyVersion(Version("1.1.0"))), + ), + ( + fake_release(version="2.1.4"), + cr.ReleaseLazySpecifier(cr.EagerLazyRelease(fake_release(version="2.1.4"))), + ), + ( + cr.EagerLazyRelease(fake_release(version="2.1.4")), + cr.ReleaseLazySpecifier(cr.EagerLazyRelease(fake_release(version="2.1.4"))), ), ( Specifier(">=1.2.0"), - cr.LazySpecifier(cr.SpecifierOperator.GE, cr.EagerLazyVersion(Version("1.2.0"))), + cr.EagerLazySpecifier(cr.SpecifierOperator.GE, cr.EagerLazyVersion(Version("1.2.0"))), ), ( - cr.LazySpecifier(cr.SpecifierOperator.GE, cr.EagerLazyVersion(Version("1.3.0"))), - cr.LazySpecifier(cr.SpecifierOperator.GE, cr.EagerLazyVersion(Version("1.3.0"))), + cr.EagerLazySpecifier(cr.SpecifierOperator.GE, cr.EagerLazyVersion(Version("1.3.0"))), + cr.EagerLazySpecifier(cr.SpecifierOperator.GE, cr.EagerLazyVersion(Version("1.3.0"))), ), ], ) @@ -391,6 +409,22 @@ async def test_composite_lazy_specifier_set() -> None: "specifier_set,expected", [ (">=1.1.0", cr.EagerLazySpecifierSet(frozenset([cr.get_lazy_specifier(">=1.1.0")]))), + ( + fake_release(version="2.1.4"), + cr.EagerLazySpecifierSet( + frozenset( + [cr.ReleaseLazySpecifier(cr.EagerLazyRelease(fake_release(version="2.1.4")))] + ) + ), + ), + ( + cr.EagerLazyRelease(fake_release(version="2.1.4")), + cr.EagerLazySpecifierSet( + frozenset( + [cr.ReleaseLazySpecifier(cr.EagerLazyRelease(fake_release(version="2.1.4")))] + ) + ), + ), ( Specifier(">=1.2.0"), cr.EagerLazySpecifierSet(frozenset([cr.get_lazy_specifier(">=1.2.0")])), @@ -494,6 +528,26 @@ async def test_lazy_requirement__url() -> None: marker=None, ), ), + ( + fake_release(version="1.2.3"), + cr.LazyRequirement( + distribution="foo.bar", + url=None, + extras=frozenset(), + specifier=cr.get_lazy_specifier_set(fake_release(version="1.2.3")), + marker=None, + ), + ), + ( + cr.EagerLazyRelease(fake_release(version="1.2.3")), + cr.LazyRequirement( + distribution="foo.bar", + url=None, + extras=frozenset(), + specifier=cr.get_lazy_specifier_set(fake_release(version="1.2.3")), + marker=None, + ), + ), ( Specifier("==1.2.0"), cr.LazyRequirement( @@ -1067,6 +1121,38 @@ def test_compose__specifier_sets() -> None: ) ), ), + ( + fake_release(version="1.2.3"), + cr.EagerLazyRequirementSet( + frozenset( + [ + cr.LazyRequirement( + distribution="foo.bar", + url=None, + extras=frozenset(), + specifier=cr.get_lazy_specifier_set(fake_release(version="1.2.3")), + marker=None, + ), + ] + ) + ), + ), + ( + cr.EagerLazyRelease(fake_release(version="1.2.3")), + cr.EagerLazyRequirementSet( + frozenset( + [ + cr.LazyRequirement( + distribution="foo.bar", + url=None, + extras=frozenset(), + specifier=cr.get_lazy_specifier_set(fake_release(version="1.2.3")), + marker=None, + ), + ] + ) + ), + ), ( Specifier("==1.2.0"), cr.EagerLazyRequirementSet( diff --git a/tests/test_versiontokens.py b/tests/test_versiontokens.py index 68dff3c..0f72b5e 100644 --- a/tests/test_versiontokens.py +++ b/tests/test_versiontokens.py @@ -1,42 +1,41 @@ import pytest from packaging.version import Version -import compreq.operators as o -from compreq import EagerLazyVersion, LazySpecifier, SpecifierOperator +import compreq as cr @pytest.mark.parametrize( "specifier,expected", [ ( - o.version.require("<=", "1.0.0"), - LazySpecifier(SpecifierOperator.LE, EagerLazyVersion(Version("1.0.0"))), + cr.version.require("<=", "1.0.0"), + cr.EagerLazySpecifier(cr.SpecifierOperator.LE, cr.EagerLazyVersion(Version("1.0.0"))), ), ( - o.version(">=", "1.0.0"), - LazySpecifier(SpecifierOperator.GE, EagerLazyVersion(Version("1.0.0"))), + cr.version(">=", "1.0.0"), + cr.EagerLazySpecifier(cr.SpecifierOperator.GE, cr.EagerLazyVersion(Version("1.0.0"))), ), - (o.version.compatible("1.1.0"), o.version("~=", "1.1.0")), - (o.version.exclude("1.2.0"), o.version("!=", "1.2.0")), - (o.version.ne("1.3.0"), o.version("!=", "1.3.0")), - (o.version != "1.4.0", o.version("!=", "1.4.0")), - (o.version.match("1.5.0"), o.version("==", "1.5.0")), - (o.version.eq("1.6.0"), o.version("==", "1.6.0")), - (o.version == "1.7.0", o.version("==", "1.7.0")), - (o.version.less("1.8.0"), o.version("<", "1.8.0")), - (o.version.lt("1.9.0"), o.version("<", "1.9.0")), - (o.version < "1.10.0", o.version("<", "1.10.0")), - (o.version.greater("1.11.0"), o.version(">", "1.11.0")), - (o.version.gt("1.12.0"), o.version(">", "1.12.0")), - (o.version > "1.13.0", o.version(">", "1.13.0")), - (o.version.less_or_equal("1.14.0"), o.version("<=", "1.14.0")), - (o.version.le("1.15.0"), o.version("<=", "1.15.0")), - (o.version <= "1.16.0", o.version("<=", "1.16.0")), - (o.version.greater_or_equal("1.17.0"), o.version(">=", "1.17.0")), - (o.version.ge("1.18.0"), o.version(">=", "1.18.0")), - (o.version >= "1.19.0", o.version(">=", "1.19.0")), - (o.version.arbitrary_equal("1.20.0"), o.version("===", "1.20.0")), + (cr.version.compatible("1.1.0"), cr.version("~=", "1.1.0")), + (cr.version.exclude("1.2.0"), cr.version("!=", "1.2.0")), + (cr.version.ne("1.3.0"), cr.version("!=", "1.3.0")), + (cr.version != "1.4.0", cr.version("!=", "1.4.0")), + (cr.version.match("1.5.0"), cr.version("==", "1.5.0")), + (cr.version.eq("1.6.0"), cr.version("==", "1.6.0")), + (cr.version == "1.7.0", cr.version("==", "1.7.0")), + (cr.version.less("1.8.0"), cr.version("<", "1.8.0")), + (cr.version.lt("1.9.0"), cr.version("<", "1.9.0")), + (cr.version < "1.10.0", cr.version("<", "1.10.0")), + (cr.version.greater("1.11.0"), cr.version(">", "1.11.0")), + (cr.version.gt("1.12.0"), cr.version(">", "1.12.0")), + (cr.version > "1.13.0", cr.version(">", "1.13.0")), + (cr.version.less_or_equal("1.14.0"), cr.version("<=", "1.14.0")), + (cr.version.le("1.15.0"), cr.version("<=", "1.15.0")), + (cr.version <= "1.16.0", cr.version("<=", "1.16.0")), + (cr.version.greater_or_equal("1.17.0"), cr.version(">=", "1.17.0")), + (cr.version.ge("1.18.0"), cr.version(">=", "1.18.0")), + (cr.version >= "1.19.0", cr.version(">=", "1.19.0")), + (cr.version.arbitrary_equal("1.20.0"), cr.version("===", "1.20.0")), ], ) -def test_version_token(specifier: LazySpecifier, expected: LazySpecifier) -> None: +def test_version_token(specifier: cr.LazySpecifier, expected: cr.LazySpecifier) -> None: assert specifier == expected