Skip to content

Commit

Permalink
pep440: support upper bound post/local release comparisons (#157)
Browse files Browse the repository at this point in the history
* tests: add coverage for poetry.core.version.pep440

* pep440: allow release tuples

* pep440: support post/local release comparisons

This change ensures that post and local releases are taken into
consideration when checking if version range allows a post release
local build release at upper and lower bounds.

The following conditions now hold for upper bound checks.

- `<=3.0.0` allows `3.0.0+local.1`, `3.0.0-1`
- `<=3.0.0+local.1` disallows `3.0.0+local.2`, allows `3.0.0-1`
- `<=3.0.0-1` allows `3.0.0+local.1`, `3.0.0`

Lower bound checks require no modification and works due to the
implicit version comparison of `poetry.core.pep440.PEP440Version`.
  • Loading branch information
abn committed Apr 2, 2021
1 parent 8996496 commit f5c6b64
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 5 deletions.
14 changes: 12 additions & 2 deletions poetry/core/semver/version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,20 @@ def allows(self, other: "Version") -> bool:
return False

if self.full_max is not None:
if other > self.full_max:
_this, _other = self.full_max, other

if not _this.is_local() and _other.is_local():
# allow weak equality to allow `3.0.0+local.1` for `<=3.0.0`
_other = _other.without_local()

if not _this.is_postrelease() and _other.is_postrelease():
# allow weak equality to allow `3.0.0-1` for `<=3.0.0`
_other = _other.without_postrelease()

if _other > _this:
return False

if not self._include_max and other == self.full_max:
if not self._include_max and _other == _this:
return False

return True
Expand Down
24 changes: 24 additions & 0 deletions poetry/core/version/pep440/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ def __post_init__(self):
if self.local is not None and not isinstance(self.local, tuple):
object.__setattr__(self, "local", (self.local,))

if isinstance(self.release, tuple):
object.__setattr__(self, "release", Release(*self.release))

# we do this here to handle both None and tomlkit string values
object.__setattr__(
self, "text", self.to_string() if not self.text else str(self.text)
Expand Down Expand Up @@ -137,6 +140,9 @@ def is_postrelease(self) -> bool:
def is_devrelease(self) -> bool:
return self.dev is not None

def is_local(self) -> bool:
return self.local is not None

def is_no_suffix_release(self) -> bool:
return not (self.pre or self.post or self.dev)

Expand Down Expand Up @@ -200,3 +206,21 @@ def first_prerelease(self) -> "PEP440Version":
return self.__class__(
epoch=self.epoch, release=self.release, pre=ReleaseTag(RELEASE_PHASE_ALPHA)
)

def replace(self, **kwargs):
return self.__class__(
**{
**{
k: getattr(self, k)
for k in self.__dataclass_fields__.keys()
if k not in ("_compare_key", "text")
}, # setup defaults with current values, excluding compare keys and text
**kwargs, # keys to replace
}
)

def without_local(self) -> "PEP440Version":
return self.replace(local=None)

def without_postrelease(self) -> "PEP440Version":
return self.replace(post=None)
140 changes: 137 additions & 3 deletions tests/semver/test_version_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,135 @@ def v300b1():
return Version.parse("3.0.0b1")


def test_allows_all(v003, v010, v080, v114, v123, v124, v140, v200, v234, v250, v300):
@pytest.mark.parametrize(
"base,other",
[
pytest.param(Version.parse("3.0.0"), Version.parse("3.0.0-1"), id="post"),
pytest.param(
Version.parse("3.0.0"), Version.parse("3.0.0+local.1"), id="local"
),
],
)
def test_allows_post_releases_with_max(base, other):
range = VersionRange(max=base, include_max=True)
assert range.allows(other)


@pytest.mark.parametrize(
"base,other",
[
pytest.param(Version.parse("3.0.0"), Version.parse("3.0.0-1"), id="post"),
pytest.param(
Version.parse("3.0.0"), Version.parse("3.0.0+local.1"), id="local"
),
],
)
def test_allows_post_releases_with_min(base, other):
range = VersionRange(min=base, include_min=True)
assert range.allows(other)


def test_allows_post_releases_with_post_and_local_min():
one = Version.parse("3.0.0+local.1")
two = Version.parse("3.0.0-1")
three = Version.parse("3.0.0-1+local.1")
four = Version.parse("3.0.0+local.2")

assert VersionRange(min=one, include_min=True).allows(two)
assert VersionRange(min=one, include_min=True).allows(three)
assert VersionRange(min=one, include_min=True).allows(four)

assert not VersionRange(min=two, include_min=True).allows(one)
assert VersionRange(min=two, include_min=True).allows(three)
assert not VersionRange(min=two, include_min=True).allows(four)

assert not VersionRange(min=three, include_min=True).allows(one)
assert not VersionRange(min=three, include_min=True).allows(two)
assert not VersionRange(min=three, include_min=True).allows(four)

assert not VersionRange(min=four, include_min=True).allows(one)
assert VersionRange(min=four, include_min=True).allows(two)
assert VersionRange(min=four, include_min=True).allows(three)


def test_allows_post_releases_with_post_and_local_max():
one = Version.parse("3.0.0+local.1")
two = Version.parse("3.0.0-1")
three = Version.parse("3.0.0-1+local.1")
four = Version.parse("3.0.0+local.2")

assert VersionRange(max=one, include_max=True).allows(two)
assert VersionRange(max=one, include_max=True).allows(three)
assert not VersionRange(max=one, include_max=True).allows(four)

assert VersionRange(max=two, include_max=True).allows(one)
assert VersionRange(max=two, include_max=True).allows(three)
assert VersionRange(max=two, include_max=True).allows(four)

assert VersionRange(max=three, include_max=True).allows(one)
assert VersionRange(max=three, include_max=True).allows(two)
assert VersionRange(max=three, include_max=True).allows(four)

assert VersionRange(max=four, include_max=True).allows(one)
assert VersionRange(max=four, include_max=True).allows(two)
assert VersionRange(max=four, include_max=True).allows(three)


@pytest.mark.parametrize(
"base,one,two",
[
pytest.param(
Version.parse("3.0.0"),
Version.parse("3.0.0-1"),
Version.parse("3.0.0-2"),
id="post",
),
pytest.param(
Version.parse("3.0.0"),
Version.parse("3.0.0+local.1"),
Version.parse("3.0.0+local.2"),
id="local",
),
],
)
def test_allows_post_releases_explicit_with_max(base, one, two):
range = VersionRange(max=one, include_max=True)
assert range.allows(base)
assert not range.allows(two)

range = VersionRange(max=two, include_max=True)
assert range.allows(base)
assert range.allows(one)


@pytest.mark.parametrize(
"base,one,two",
[
pytest.param(
Version.parse("3.0.0"),
Version.parse("3.0.0-1"),
Version.parse("3.0.0-2"),
id="post",
),
pytest.param(
Version.parse("3.0.0"),
Version.parse("3.0.0+local.1"),
Version.parse("3.0.0+local.2"),
id="local",
),
],
)
def test_allows_post_releases_explicit_with_min(base, one, two):
range = VersionRange(min=one, include_min=True)
assert not range.allows(base)
assert range.allows(two)

range = VersionRange(min=two, include_min=True)
assert not range.allows(base)
assert not range.allows(one)


def test_allows_all(v123, v124, v140, v250, v300):
assert VersionRange(v123, v250).allows_all(EmptyConstraint())

range = VersionRange(v123, v250, include_max=True)
Expand All @@ -84,7 +212,8 @@ def test_allows_all(v003, v010, v080, v114, v123, v124, v140, v200, v234, v250,
assert range.allows_all(v250)
assert not range.allows_all(v300)

# with no min

def test_allows_all_with_no_min(v080, v140, v250, v300):
range = VersionRange(max=v250)
assert range.allows_all(VersionRange(v080, v140))
assert not range.allows_all(VersionRange(v080, v300))
Expand All @@ -93,7 +222,8 @@ def test_allows_all(v003, v010, v080, v114, v123, v124, v140, v200, v234, v250,
assert range.allows_all(range)
assert not range.allows_all(VersionRange())

# with no max

def test_allows_all_with_no_max(v003, v010, v080, v140):
range = VersionRange(min=v010)
assert range.allows_all(VersionRange(v080, v140))
assert not range.allows_all(VersionRange(v003, v140))
Expand All @@ -102,6 +232,8 @@ def test_allows_all(v003, v010, v080, v114, v123, v124, v140, v200, v234, v250,
assert range.allows_all(range)
assert not range.allows_all(VersionRange())


def test_allows_all_bordering_range_not_more_inclusive(v010, v250):
# Allows bordering range that is not more inclusive
exclusive = VersionRange(v010, v250)
inclusive = VersionRange(v010, v250, True, True)
Expand All @@ -110,6 +242,8 @@ def test_allows_all(v003, v010, v080, v114, v123, v124, v140, v200, v234, v250,
assert not exclusive.allows_all(inclusive)
assert exclusive.allows_all(exclusive)


def test_allows_all_contained_unions(v010, v114, v123, v124, v140, v200, v234):
# Allows unions that are completely contained
range = VersionRange(v114, v200)
assert range.allows_all(VersionRange(v123, v124).union(v140))
Expand Down
Loading

0 comments on commit f5c6b64

Please sign in to comment.