Skip to content

Commit

Permalink
pep440: support post/local release comparisons
Browse files Browse the repository at this point in the history
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 bdf6080 commit 4431d67
Show file tree
Hide file tree
Showing 3 changed files with 171 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
22 changes: 22 additions & 0 deletions poetry/core/version/pep440/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,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 @@ -203,3 +206,22 @@ 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"
}, # setup defaults with current values, excluding compare keys
"text": None, # do not pass through text as this will change with replace
**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

0 comments on commit 4431d67

Please sign in to comment.