From c415ee4cd87915191b1ca8df2d3fbf7e49d4bbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 24 Aug 2019 14:04:55 +0200 Subject: Add `Version.precedence_key`. This will be used in `sort(..., key=lambda v: v.precedence_key)`. Remove previous comparison/ordering implementation. The current implementation relies on a 4-tuple: - major, minor, patch (as integers) - natural order matches precedence rules - a tuple of identifiers for the prerelease component. The identifiers for the prerelease components are based on: - A `NumericIdentifier` class, that compares number using their natural order, and always compares lower than non-numeric identifiers - A `AlphaIdentifier` class for non-numeric identifiers; it compares versions using ASCII ordering. - A `MaxIdentifier` class, that compares higher to any other identifier; used to ensure that a non-prerelease version is greater than any of its prereleases. --- semantic_version/base.py | 255 +++++++++++++++++++---------------------------- 1 file changed, 103 insertions(+), 152 deletions(-) (limited to 'semantic_version') diff --git a/semantic_version/base.py b/semantic_version/base.py index b44227e..0e036ae 100644 --- a/semantic_version/base.py +++ b/semantic_version/base.py @@ -7,13 +7,6 @@ import re import warnings -def _to_int(value): - try: - return int(value), True - except ValueError: - return value, False - - def _has_leading_zero(value): return (value and value[0] == '0' @@ -21,58 +14,66 @@ def _has_leading_zero(value): and value != '0') -def base_cmp(x, y): - if x == y: - return 0 - elif x > y: - return 1 - elif x < y: - return -1 - else: - return NotImplemented +class MaxIdentifier: + __slots__ = [] + + def __repr__(self): + return 'MaxIdentifier()' + + def __eq__(self, other): + return isinstance(other, self.__class__) + +@functools.total_ordering +class NumericIdentifier: + __slots__ = ['value'] -def identifier_cmp(a, b): - """Compare two identifier (for pre-release/build components).""" + def __init__(self, value): + self.value = int(value) - a_cmp, a_is_int = _to_int(a) - b_cmp, b_is_int = _to_int(b) + def __repr__(self): + return 'NumericIdentifier(%r)' % self.value - if a_is_int and b_is_int: - # Numeric identifiers are compared as integers - return base_cmp(a_cmp, b_cmp) - elif a_is_int: - # Numeric identifiers have lower precedence - return -1 - elif b_is_int: - return 1 - else: - # Non-numeric identifiers are compared lexicographically - return base_cmp(a_cmp, b_cmp) + def __eq__(self, other): + if isinstance(other, NumericIdentifier): + return self.value == other.value + return NotImplemented + def __lt__(self, other): + if isinstance(other, MaxIdentifier): + return True + elif isinstance(other, AlphaIdentifier): + return True + elif isinstance(other, NumericIdentifier): + return self.value < other.value + else: + return NotImplemented -def identifier_list_cmp(a, b): - """Compare two identifier list (pre-release/build components). - The rule is: - - Identifiers are paired between lists - - They are compared from left to right - - If all first identifiers match, the longest list is greater. +@functools.total_ordering +class AlphaIdentifier: + __slots__ = ['value'] - >>> identifier_list_cmp(['1', '2'], ['1', '2']) - 0 - >>> identifier_list_cmp(['1', '2a'], ['1', '2b']) - -1 - >>> identifier_list_cmp(['1'], ['1', '2']) - -1 - """ - identifier_pairs = zip(a, b) - for id_a, id_b in identifier_pairs: - cmp_res = identifier_cmp(id_a, id_b) - if cmp_res != 0: - return cmp_res - # alpha1.3 < alpha1.3.1 - return base_cmp(len(a), len(b)) + def __init__(self, value): + self.value = value.encode('ascii') + + def __repr__(self): + return 'AlphaIdentifier(%r)' % self.value + + def __eq__(self, other): + if isinstance(other, AlphaIdentifier): + return self.value == other.value + return NotImplemented + + def __lt__(self, other): + if isinstance(other, MaxIdentifier): + return True + elif isinstance(other, NumericIdentifier): + return False + elif isinstance(other, AlphaIdentifier): + return self.value < other.value + else: + return NotImplemented class Version: @@ -388,127 +389,77 @@ class Version: ', partial=True' if self.partial else '', ) - @classmethod - def _comparison_functions(cls, partial=False): - """Retrieve comparison methods to apply on version components. - - This is a private API. - - Args: - partial (bool): whether to provide 'partial' or 'strict' matching. - - Returns: - 5-tuple of cmp-like functions. - """ - - def prerelease_cmp(a, b): - """Compare prerelease components. - - Special rule: a version without prerelease component has higher - precedence than one with a prerelease component. - """ - if a and b: - return identifier_list_cmp(a, b) - elif a: - # Versions with prerelease field have lower precedence - return -1 - elif b: - return 1 - else: - return 0 - - def build_cmp(a, b): - """Compare build metadata. - - Special rule: there is no ordering on build metadata. - """ - if a == b: - return 0 - else: - return NotImplemented - - def make_optional(orig_cmp_fun): - """Convert a cmp-like function to consider 'None == *'.""" - @functools.wraps(orig_cmp_fun) - def alt_cmp_fun(a, b): - if a is None or b is None: - return 0 - return orig_cmp_fun(a, b) - - return alt_cmp_fun - - if partial: - return [ - base_cmp, # Major is still mandatory - make_optional(base_cmp), - make_optional(base_cmp), - prerelease_cmp, - make_optional(build_cmp), - ] - else: - return [ - base_cmp, - base_cmp, - base_cmp, - prerelease_cmp, - build_cmp, - ] - - def __compare(self, other): - comparison_functions = self._comparison_functions(partial=self.partial or other.partial) - comparisons = zip(comparison_functions, self, other) - - for cmp_fun, self_field, other_field in comparisons: - cmp_res = cmp_fun(self_field, other_field) - if cmp_res != 0: - return cmp_res - - return 0 - def __hash__(self): # We don't include 'partial', since this is strictly equivalent to having # at least a field being `None`. return hash((self.major, self.minor, self.patch, self.prerelease, self.build)) - def __cmp__(self, other): - if not isinstance(other, self.__class__): - return NotImplemented - return self.__compare(other) + @property + def precedence_key(self): + if self.prerelease: + prerelease_key = tuple( + NumericIdentifier(part) if part.isdecimal() else AlphaIdentifier(part) + for part in self.prerelease + ) + else: + prerelease_key = ( + MaxIdentifier(), + ) - def __compare_helper(self, other, condition, notimpl_target): - """Helper for comparison. + return ( + self.major, + self.minor, + self.patch, + prerelease_key, + ) - Allows the caller to provide: - - The condition - - The return value if the comparison is meaningless (ie versions with - build metadata). - """ + def __cmp__(self, other): if not isinstance(other, self.__class__): return NotImplemented - - cmp_res = self.__cmp__(other) - if cmp_res is NotImplemented: - return notimpl_target - - return condition(cmp_res) + if self < other: + return -1 + elif self > other: + return 1 + elif self == other: + return 0 + else: + return NotImplemented def __eq__(self, other): - return self.__compare_helper(other, lambda x: x == 0, notimpl_target=False) + if not isinstance(other, self.__class__): + return NotImplemented + return ( + self.major == other.major + and self.minor == other.minor + and self.patch == other.patch + and (self.prerelease or ()) == (other.prerelease or ()) + and (self.build or ()) == (other.build or ()) + ) def __ne__(self, other): - return self.__compare_helper(other, lambda x: x != 0, notimpl_target=True) + if not isinstance(other, self.__class__): + return NotImplemented + return tuple(self) != tuple(other) def __lt__(self, other): - return self.__compare_helper(other, lambda x: x < 0, notimpl_target=False) + if not isinstance(other, self.__class__): + return NotImplemented + return self.precedence_key < other.precedence_key def __le__(self, other): - return self.__compare_helper(other, lambda x: x <= 0, notimpl_target=False) + if not isinstance(other, self.__class__): + return NotImplemented + return self.precedence_key <= other.precedence_key def __gt__(self, other): - return self.__compare_helper(other, lambda x: x > 0, notimpl_target=False) + if not isinstance(other, self.__class__): + return NotImplemented + return self.precedence_key > other.precedence_key def __ge__(self, other): - return self.__compare_helper(other, lambda x: x >= 0, notimpl_target=False) + if not isinstance(other, self.__class__): + return NotImplemented + return self.precedence_key >= other.precedence_key class SpecItem: @@ -600,7 +551,7 @@ class SpecItem: def compare(v1, v2): - return base_cmp(Version(v1), Version(v2)) + return Version(v1).__cmp__(Version(v2)) def match(spec, version): -- cgit v1.2.1