semver.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import re
  2. class Version:
  3. """
  4. This class is intended to provide utility methods to work with semver ranges.
  5. Right now it is limited to the simplest case: a ">=" operator followed by an exact version with no prerelease or build specification.
  6. Example: ">= 1.2.3"
  7. """
  8. @classmethod
  9. def from_str(cls, input):
  10. """
  11. :param str input: save exact formatted version e.g. 1.2.3
  12. :rtype: Version
  13. :raises: ValueError
  14. """
  15. parts = input.strip().split(".", 2)
  16. major = int(parts[0])
  17. minor = int(parts[1])
  18. patch = int(parts[2])
  19. return cls(major, minor, patch)
  20. STABLE_VERSION_RE = re.compile(r'^\d+\.\d+\.\d+$')
  21. @classmethod
  22. def is_stable(cls, v):
  23. """
  24. Verifies that the version is in a supported format.
  25. :param v:string with the version
  26. :return: bool
  27. """
  28. return cls.STABLE_VERSION_RE.match(v) is not None
  29. @classmethod
  30. def cmp(cls, a, b):
  31. """
  32. Compare two versions. Should be used with "cmp_to_key" wrapper in sorted(), min(), max()...
  33. For example:
  34. sorted(["1.2.3", "2.4.2", "1.2.7"], key=cmp_to_key(Version.cmp))
  35. :param a:string with version or Version instance
  36. :param b:string with version or Version instance
  37. :return: int
  38. :raises: ValueError
  39. """
  40. a_version = a if isinstance(a, cls) else cls.from_str(a)
  41. b_version = b if isinstance(b, cls) else cls.from_str(b)
  42. if a_version > b_version:
  43. return 1
  44. elif a_version < b_version:
  45. return -1
  46. else:
  47. return 0
  48. __slots__ = "_values"
  49. def __init__(self, major, minor, patch):
  50. """
  51. :param int major
  52. :param int minor
  53. :param int patch
  54. :raises ValueError
  55. """
  56. version_parts = {
  57. "major": major,
  58. "minor": minor,
  59. "patch": patch,
  60. }
  61. for name, value in version_parts.items():
  62. value = int(value)
  63. version_parts[name] = value
  64. if value < 0:
  65. raise ValueError("{!r} is negative. A version can only be positive.".format(name))
  66. self._values = (version_parts["major"], version_parts["minor"], version_parts["patch"])
  67. def __str__(self):
  68. return "{}.{}.{}".format(self._values[0], self._values[1], self._values[2])
  69. def __repr__(self):
  70. return '<Version({})>'.format(self)
  71. def __eq__(self, other):
  72. """
  73. :param Version|str other
  74. :rtype: bool
  75. """
  76. if isinstance(other, str):
  77. if self.is_stable(other):
  78. other = self.from_str(other)
  79. else:
  80. return False
  81. return self.as_tuple() == other.as_tuple()
  82. def __ne__(self, other):
  83. return not self == other
  84. def __gt__(self, other):
  85. """
  86. :param Version other
  87. :rtype: bool
  88. """
  89. return self.as_tuple() > other.as_tuple()
  90. def __ge__(self, other):
  91. """
  92. :param Version other
  93. :rtype: bool
  94. """
  95. return self.as_tuple() >= other.as_tuple()
  96. def __lt__(self, other):
  97. """
  98. :param Version other
  99. :rtype: bool
  100. """
  101. return self.as_tuple() < other.as_tuple()
  102. def __le__(self, other):
  103. """
  104. :param Version other
  105. :rtype: bool
  106. """
  107. return self.as_tuple() <= other.as_tuple()
  108. @property
  109. def major(self):
  110. """The major part of the version (read-only)."""
  111. return self._values[0]
  112. @major.setter
  113. def major(self, value):
  114. raise AttributeError("Attribute 'major' is readonly")
  115. @property
  116. def minor(self):
  117. """The minor part of the version (read-only)."""
  118. return self._values[1]
  119. @minor.setter
  120. def minor(self, value):
  121. raise AttributeError("Attribute 'minor' is readonly")
  122. @property
  123. def patch(self):
  124. """The patch part of the version (read-only)."""
  125. return self._values[2]
  126. @patch.setter
  127. def patch(self, value):
  128. raise AttributeError("Attribute 'patch' is readonly")
  129. def as_tuple(self):
  130. """
  131. :rtype: tuple
  132. """
  133. return self._values
  134. class Operator:
  135. EQ = "="
  136. GT = ">"
  137. GE = ">="
  138. LT = "<"
  139. LE = "<="
  140. class VersionRange:
  141. @classmethod
  142. def operator_is_ok(self, operator):
  143. return [Operator.GE, Operator.EQ, None].count(operator)
  144. @classmethod
  145. def from_str(cls, input):
  146. """
  147. :param str input
  148. :rtype: VersionRange
  149. :raises: ValueError
  150. """
  151. m = re.match(r"^\s*([<>=]+)?\s*(\d+\.\d+\.\d+)\s*$", input)
  152. res = m.groups() if m else None
  153. if not res or not cls.operator_is_ok(res[0]):
  154. raise ValueError(
  155. "Unsupported version range: '{}'. Currently we only support ranges with stable versions and GE / EQ: '>= 1.2.3' / '= 1.2.3' / '1.2.3'".format(
  156. input
  157. )
  158. )
  159. version = Version.from_str(res[1])
  160. return cls(res[0], version)
  161. __slots__ = ("_operator", "_version")
  162. def __init__(self, operator, version):
  163. """
  164. :param str operator
  165. :raises: ValueError
  166. """
  167. if not self.operator_is_ok(operator):
  168. raise ValueError("Unsupported range operator '{}'".format(operator))
  169. # None defaults to Operator.EQ
  170. self._operator = operator or Operator.EQ
  171. self._version = version
  172. @property
  173. def operator(self):
  174. """The comparison operator to be used (read-only)."""
  175. return self._operator
  176. @operator.setter
  177. def operator(self, value):
  178. raise AttributeError("Attribute 'operator' is readonly")
  179. @property
  180. def version(self):
  181. """Version to be used with the operator (read-only)."""
  182. return self._version
  183. @version.setter
  184. def version(self, value):
  185. raise AttributeError("Attribute 'version' is readonly")
  186. def is_satisfied_by(self, version):
  187. """
  188. :param Version version
  189. :rtype: bool
  190. :raises: ValueError
  191. """
  192. if self._operator == Operator.GE:
  193. return version >= self._version
  194. if self._operator == Operator.EQ:
  195. return version == self._version
  196. raise ValueError("Unsupported operator '{}'".format(self._operator))