Skip to content
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
10 changes: 7 additions & 3 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
ChangeLog
=========

2.9.1 (unreleased)
------------------
2.10.0 (unreleased)
-------------------

*New:*

- Nothing changed yet.
* `132 <https://github.com/rbarrois/python-semanticversion/issues/132>`_:
Ensure sorting a collection of versions is always stable, even with
build metadata.


2.9.0 (2022-02-06)
Expand Down
11 changes: 10 additions & 1 deletion docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,16 @@ Representing a version (the Version class)
The actual value of the attribute is considered an implementation detail; the only
guarantee is that ordering versions by their precedence_key will comply with semver precedence rules.

Note that the :attr:`~Version.build` isn't included in the precedence_key computatin.

.. warning::

.. versionchanged:: 2.10.0

The :attr:`~Version.build` is included in the precedence_key computation, but
only for ordering stability.
The only guarantee is that, for a given release of python-semanticversion, two versions'
:attr:`~Version.precedence_key` will always compare in the same direction if they include
build metadata; that ordering is an implementation detail and shouldn't be relied upon.

.. attribute:: partial

Expand Down
42 changes: 35 additions & 7 deletions semantic_version/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ def __init__(

self.partial = partial

# Cached precedence keys
# _cmp_precedence_key is used for semver-precedence comparison
self._cmp_precedence_key = self._build_precedence_key(with_build=False)
# _sort_precedence_key is used for self.precedence_key, esp. for sorted(...)
self._sort_precedence_key = self._build_precedence_key(with_build=True)

@classmethod
def _coerce(cls, value, allow_none=False):
if value is None and allow_none:
Expand Down Expand Up @@ -408,25 +414,47 @@ def __hash__(self):
# at least a field being `None`.
return hash((self.major, self.minor, self.patch, self.prerelease, self.build))

@property
def precedence_key(self):
def _build_precedence_key(self, with_build=False):
"""Build a precedence key.

The "build" component should only be used when sorting an iterable
of versions.
"""
if self.prerelease:
prerelease_key = tuple(
NumericIdentifier(part) if re.match(r'^[0-9]+$', part) else AlphaIdentifier(part)
NumericIdentifier(part) if part.isdigit() else AlphaIdentifier(part)
for part in self.prerelease
)
else:
prerelease_key = (
MaxIdentifier(),
)

if not with_build:
return (
self.major,
self.minor,
self.patch,
prerelease_key,
)

build_key = tuple(
NumericIdentifier(part) if part.isdigit() else AlphaIdentifier(part)
for part in self.build or ()
)

return (
self.major,
self.minor,
self.patch,
prerelease_key,
build_key,
)

@property
def precedence_key(self):
return self._sort_precedence_key

def __cmp__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
Expand Down Expand Up @@ -458,22 +486,22 @@ def __ne__(self, other):
def __lt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.precedence_key < other.precedence_key
return self._cmp_precedence_key < other._cmp_precedence_key

def __le__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.precedence_key <= other.precedence_key
return self._cmp_precedence_key <= other._cmp_precedence_key

def __gt__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.precedence_key > other.precedence_key
return self._cmp_precedence_key > other._cmp_precedence_key

def __ge__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self.precedence_key >= other.precedence_key
return self._cmp_precedence_key >= other._cmp_precedence_key


class SpecItem(object):
Expand Down
14 changes: 14 additions & 0 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,20 @@ def test_invalid_comparisons(self):
self.assertTrue(v != '0.1.0')
self.assertFalse(v == '0.1.0')

def test_stable_ordering(self):
a = [
base.Version('0.1.0'),
base.Version('0.1.0+a'),
base.Version('0.1.0+a.1'),
base.Version('0.1.1-a1'),
]
b = [a[1], a[3], a[0], a[2]]

self.assertEqual(
sorted(a, key=lambda v: v.precedence_key),
sorted(b, key=lambda v: v.precedence_key),
)

def test_bump_clean_versions(self):
# We Test each property explicitly as the == comparator for versions
# does not distinguish between prerelease or builds for equality.
Expand Down