version.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. """Version handling by a semver compatible version class."""
  2. import collections
  3. import re
  4. from functools import wraps
  5. from typing import (
  6. Any,
  7. Dict,
  8. Iterable,
  9. Optional,
  10. SupportsInt,
  11. Tuple,
  12. Union,
  13. cast,
  14. Callable,
  15. Collection,
  16. Type,
  17. TypeVar,
  18. )
  19. from ._types import (
  20. VersionTuple,
  21. VersionDict,
  22. VersionIterator,
  23. String,
  24. VersionPart,
  25. )
  26. # These types are required here because of circular imports
  27. Comparable = Union["Version", Dict[str, VersionPart], Collection[VersionPart], str]
  28. Comparator = Callable[["Version", Comparable], bool]
  29. T = TypeVar("T", bound="Version")
  30. def _comparator(operator: Comparator) -> Comparator:
  31. """Wrap a Version binary op method in a type-check."""
  32. @wraps(operator)
  33. def wrapper(self: "Version", other: Comparable) -> bool:
  34. comparable_types = (
  35. Version,
  36. dict,
  37. tuple,
  38. list,
  39. *String.__args__, # type: ignore
  40. )
  41. if not isinstance(other, comparable_types):
  42. return NotImplemented
  43. return operator(self, other)
  44. return wrapper
  45. def _cmp(a, b): # TODO: type hints
  46. """Return negative if a<b, zero if a==b, positive if a>b."""
  47. return (a > b) - (a < b)
  48. class Version:
  49. """
  50. A semver compatible version class.
  51. See specification at https://semver.org.
  52. :param major: version when you make incompatible API changes.
  53. :param minor: version when you add functionality in a backwards-compatible manner.
  54. :param patch: version when you make backwards-compatible bug fixes.
  55. :param prerelease: an optional prerelease string
  56. :param build: an optional build string
  57. """
  58. __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build")
  59. #: The names of the different parts of a version
  60. NAMES = tuple([item[1:] for item in __slots__])
  61. #: Regex for number in a prerelease
  62. _LAST_NUMBER = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+")
  63. #: Regex template for a semver version
  64. _REGEX_TEMPLATE = r"""
  65. ^
  66. (?P<major>0|[1-9]\d*)
  67. (?:
  68. \.
  69. (?P<minor>0|[1-9]\d*)
  70. (?:
  71. \.
  72. (?P<patch>0|[1-9]\d*)
  73. ){opt_patch}
  74. ){opt_minor}
  75. (?:-(?P<prerelease>
  76. (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)
  77. (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*
  78. ))?
  79. (?:\+(?P<build>
  80. [0-9a-zA-Z-]+
  81. (?:\.[0-9a-zA-Z-]+)*
  82. ))?
  83. $
  84. """
  85. #: Regex for a semver version
  86. _REGEX = re.compile(
  87. _REGEX_TEMPLATE.format(opt_patch="", opt_minor=""),
  88. re.VERBOSE,
  89. )
  90. #: Regex for a semver version that might be shorter
  91. _REGEX_OPTIONAL_MINOR_AND_PATCH = re.compile(
  92. _REGEX_TEMPLATE.format(opt_patch="?", opt_minor="?"),
  93. re.VERBOSE,
  94. )
  95. def __init__(
  96. self,
  97. major: SupportsInt,
  98. minor: SupportsInt = 0,
  99. patch: SupportsInt = 0,
  100. prerelease: Optional[Union[String, int]] = None,
  101. build: Optional[Union[String, int]] = None,
  102. ):
  103. # Build a dictionary of the arguments except prerelease and build
  104. version_parts = {"major": int(major), "minor": int(minor), "patch": int(patch)}
  105. for name, value in version_parts.items():
  106. if value < 0:
  107. raise ValueError(
  108. "{!r} is negative. A version can only be positive.".format(name)
  109. )
  110. self._major = version_parts["major"]
  111. self._minor = version_parts["minor"]
  112. self._patch = version_parts["patch"]
  113. self._prerelease = None if prerelease is None else str(prerelease)
  114. self._build = None if build is None else str(build)
  115. @classmethod
  116. def _nat_cmp(cls, a, b): # TODO: type hints
  117. def cmp_prerelease_tag(a, b):
  118. if isinstance(a, int) and isinstance(b, int):
  119. return _cmp(a, b)
  120. elif isinstance(a, int):
  121. return -1
  122. elif isinstance(b, int):
  123. return 1
  124. else:
  125. return _cmp(a, b)
  126. a, b = a or "", b or ""
  127. a_parts, b_parts = a.split("."), b.split(".")
  128. a_parts = [int(x) if re.match(r"^\d+$", x) else x for x in a_parts]
  129. b_parts = [int(x) if re.match(r"^\d+$", x) else x for x in b_parts]
  130. for sub_a, sub_b in zip(a_parts, b_parts):
  131. cmp_result = cmp_prerelease_tag(sub_a, sub_b)
  132. if cmp_result != 0:
  133. return cmp_result
  134. else:
  135. return _cmp(len(a), len(b))
  136. @property
  137. def major(self) -> int:
  138. """The major part of a version (read-only)."""
  139. return self._major
  140. @major.setter
  141. def major(self, value):
  142. raise AttributeError("attribute 'major' is readonly")
  143. @property
  144. def minor(self) -> int:
  145. """The minor part of a version (read-only)."""
  146. return self._minor
  147. @minor.setter
  148. def minor(self, value):
  149. raise AttributeError("attribute 'minor' is readonly")
  150. @property
  151. def patch(self) -> int:
  152. """The patch part of a version (read-only)."""
  153. return self._patch
  154. @patch.setter
  155. def patch(self, value):
  156. raise AttributeError("attribute 'patch' is readonly")
  157. @property
  158. def prerelease(self) -> Optional[str]:
  159. """The prerelease part of a version (read-only)."""
  160. return self._prerelease
  161. @prerelease.setter
  162. def prerelease(self, value):
  163. raise AttributeError("attribute 'prerelease' is readonly")
  164. @property
  165. def build(self) -> Optional[str]:
  166. """The build part of a version (read-only)."""
  167. return self._build
  168. @build.setter
  169. def build(self, value):
  170. raise AttributeError("attribute 'build' is readonly")
  171. def to_tuple(self) -> VersionTuple:
  172. """
  173. Convert the Version object to a tuple.
  174. .. versionadded:: 2.10.0
  175. Renamed :meth:`Version._astuple` to :meth:`Version.to_tuple` to
  176. make this function available in the public API.
  177. :return: a tuple with all the parts
  178. >>> semver.Version(5, 3, 1).to_tuple()
  179. (5, 3, 1, None, None)
  180. """
  181. return (self.major, self.minor, self.patch, self.prerelease, self.build)
  182. def to_dict(self) -> VersionDict:
  183. """
  184. Convert the Version object to an OrderedDict.
  185. .. versionadded:: 2.10.0
  186. Renamed :meth:`Version._asdict` to :meth:`Version.to_dict` to
  187. make this function available in the public API.
  188. :return: an OrderedDict with the keys in the order ``major``, ``minor``,
  189. ``patch``, ``prerelease``, and ``build``.
  190. >>> semver.Version(3, 2, 1).to_dict()
  191. OrderedDict([('major', 3), ('minor', 2), ('patch', 1), \
  192. ('prerelease', None), ('build', None)])
  193. """
  194. return collections.OrderedDict(
  195. (
  196. ("major", self.major),
  197. ("minor", self.minor),
  198. ("patch", self.patch),
  199. ("prerelease", self.prerelease),
  200. ("build", self.build),
  201. )
  202. )
  203. def __iter__(self) -> VersionIterator:
  204. """Return iter(self)."""
  205. yield from self.to_tuple()
  206. @staticmethod
  207. def _increment_string(string: str) -> str:
  208. """
  209. Look for the last sequence of number(s) in a string and increment.
  210. :param string: the string to search for.
  211. :return: the incremented string
  212. Source:
  213. http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1
  214. """
  215. match = Version._LAST_NUMBER.search(string)
  216. if match:
  217. next_ = str(int(match.group(1)) + 1)
  218. start, end = match.span(1)
  219. string = string[: max(end - len(next_), start)] + next_ + string[end:]
  220. return string
  221. def bump_major(self) -> "Version":
  222. """
  223. Raise the major part of the version, return a new object but leave self
  224. untouched.
  225. :return: new object with the raised major part
  226. >>> ver = semver.parse("3.4.5")
  227. >>> ver.bump_major()
  228. Version(major=4, minor=0, patch=0, prerelease=None, build=None)
  229. """
  230. cls = type(self)
  231. return cls(self._major + 1)
  232. def bump_minor(self) -> "Version":
  233. """
  234. Raise the minor part of the version, return a new object but leave self
  235. untouched.
  236. :return: new object with the raised minor part
  237. >>> ver = semver.parse("3.4.5")
  238. >>> ver.bump_minor()
  239. Version(major=3, minor=5, patch=0, prerelease=None, build=None)
  240. """
  241. cls = type(self)
  242. return cls(self._major, self._minor + 1)
  243. def bump_patch(self) -> "Version":
  244. """
  245. Raise the patch part of the version, return a new object but leave self
  246. untouched.
  247. :return: new object with the raised patch part
  248. >>> ver = semver.parse("3.4.5")
  249. >>> ver.bump_patch()
  250. Version(major=3, minor=4, patch=6, prerelease=None, build=None)
  251. """
  252. cls = type(self)
  253. return cls(self._major, self._minor, self._patch + 1)
  254. def bump_prerelease(self, token: Optional[str] = "rc") -> "Version":
  255. """
  256. Raise the prerelease part of the version, return a new object but leave
  257. self untouched.
  258. :param token: defaults to ``'rc'``
  259. :return: new :class:`Version` object with the raised prerelease part.
  260. The original object is not modified.
  261. >>> ver = semver.parse("3.4.5")
  262. >>> ver.bump_prerelease().prerelease
  263. 'rc.2'
  264. >>> ver.bump_prerelease('').prerelease
  265. '1'
  266. >>> ver.bump_prerelease(None).prerelease
  267. 'rc.1'
  268. """
  269. cls = type(self)
  270. if self._prerelease is not None:
  271. prerelease = self._prerelease
  272. elif token == "":
  273. prerelease = "0"
  274. elif token is None:
  275. prerelease = "rc.0"
  276. else:
  277. prerelease = str(token) + ".0"
  278. prerelease = cls._increment_string(prerelease)
  279. return cls(self._major, self._minor, self._patch, prerelease)
  280. def bump_build(self, token: Optional[str] = "build") -> "Version":
  281. """
  282. Raise the build part of the version, return a new object but leave self
  283. untouched.
  284. :param token: defaults to ``'build'``
  285. :return: new :class:`Version` object with the raised build part.
  286. The original object is not modified.
  287. >>> ver = semver.parse("3.4.5-rc.1+build.9")
  288. >>> ver.bump_build()
  289. Version(major=3, minor=4, patch=5, prerelease='rc.1', \
  290. build='build.10')
  291. """
  292. cls = type(self)
  293. if self._build is not None:
  294. build = self._build
  295. elif token == "":
  296. build = "0"
  297. elif token is None:
  298. build = "build.0"
  299. else:
  300. build = str(token) + ".0"
  301. # self._build or (token or "build") + ".0"
  302. build = cls._increment_string(build)
  303. if self._build is not None:
  304. build = self._build
  305. elif token == "":
  306. build = "0"
  307. elif token is None:
  308. build = "build.0"
  309. else:
  310. build = str(token) + ".0"
  311. # self._build or (token or "build") + ".0"
  312. build = cls._increment_string(build)
  313. return cls(self._major, self._minor, self._patch, self._prerelease, build)
  314. def compare(self, other: Comparable) -> int:
  315. """
  316. Compare self with other.
  317. :param other: the second version
  318. :return: The return value is negative if ver1 < ver2,
  319. zero if ver1 == ver2 and strictly positive if ver1 > ver2
  320. >>> semver.compare("2.0.0")
  321. -1
  322. >>> semver.compare("1.0.0")
  323. 1
  324. >>> semver.compare("2.0.0")
  325. 0
  326. >>> semver.compare(dict(major=2, minor=0, patch=0))
  327. 0
  328. """
  329. cls = type(self)
  330. if isinstance(other, String.__args__): # type: ignore
  331. other = cls.parse(other)
  332. elif isinstance(other, dict):
  333. other = cls(**other)
  334. elif isinstance(other, (tuple, list)):
  335. other = cls(*other)
  336. elif not isinstance(other, cls):
  337. raise TypeError(
  338. f"Expected str, bytes, dict, tuple, list, or {cls.__name__} instance, "
  339. f"but got {type(other)}"
  340. )
  341. v1 = self.to_tuple()[:3]
  342. v2 = other.to_tuple()[:3]
  343. x = _cmp(v1, v2)
  344. if x:
  345. return x
  346. rc1, rc2 = self.prerelease, other.prerelease
  347. rccmp = self._nat_cmp(rc1, rc2)
  348. if not rccmp:
  349. return 0
  350. if not rc1:
  351. return 1
  352. elif not rc2:
  353. return -1
  354. return rccmp
  355. def next_version(self, part: str, prerelease_token: str = "rc") -> "Version":
  356. """
  357. Determines next version, preserving natural order.
  358. .. versionadded:: 2.10.0
  359. This function is taking prereleases into account.
  360. The "major", "minor", and "patch" raises the respective parts like
  361. the ``bump_*`` functions. The real difference is using the
  362. "prerelease" part. It gives you the next patch version of the
  363. prerelease, for example:
  364. >>> str(semver.parse("0.1.4").next_version("prerelease"))
  365. '0.1.5-rc.1'
  366. :param part: One of "major", "minor", "patch", or "prerelease"
  367. :param prerelease_token: prefix string of prerelease, defaults to 'rc'
  368. :return: new object with the appropriate part raised
  369. """
  370. cls = type(self)
  371. # "build" is currently not used, that's why we use [:-1]
  372. validparts = cls.NAMES[:-1]
  373. if part not in validparts:
  374. raise ValueError(
  375. f"Invalid part. Expected one of {validparts}, but got {part!r}"
  376. )
  377. version = self
  378. if (version.prerelease or version.build) and (
  379. part == "patch"
  380. or (part == "minor" and version.patch == 0)
  381. or (part == "major" and version.minor == version.patch == 0)
  382. ):
  383. return version.replace(prerelease=None, build=None)
  384. # Only check the main parts:
  385. if part in cls.NAMES[:3]:
  386. return getattr(version, "bump_" + part)()
  387. if not version.prerelease:
  388. version = version.bump_patch()
  389. return version.bump_prerelease(prerelease_token)
  390. @_comparator
  391. def __eq__(self, other: Comparable) -> bool: # type: ignore
  392. return self.compare(other) == 0
  393. @_comparator
  394. def __ne__(self, other: Comparable) -> bool: # type: ignore
  395. return self.compare(other) != 0
  396. @_comparator
  397. def __lt__(self, other: Comparable) -> bool:
  398. return self.compare(other) < 0
  399. @_comparator
  400. def __le__(self, other: Comparable) -> bool:
  401. return self.compare(other) <= 0
  402. @_comparator
  403. def __gt__(self, other: Comparable) -> bool:
  404. return self.compare(other) > 0
  405. @_comparator
  406. def __ge__(self, other: Comparable) -> bool:
  407. return self.compare(other) >= 0
  408. def __getitem__(
  409. self, index: Union[int, slice]
  410. ) -> Union[int, Optional[str], Tuple[Union[int, str], ...]]:
  411. """
  412. self.__getitem__(index) <==> self[index] Implement getitem.
  413. If the part requested is undefined, or a part of the range requested
  414. is undefined, it will throw an index error.
  415. Negative indices are not supported.
  416. :param index: a positive integer indicating the
  417. offset or a :func:`slice` object
  418. :raises IndexError: if index is beyond the range or a part is None
  419. :return: the requested part of the version at position index
  420. >>> ver = semver.Version.parse("3.4.5")
  421. >>> ver[0], ver[1], ver[2]
  422. (3, 4, 5)
  423. """
  424. if isinstance(index, int):
  425. index = slice(index, index + 1)
  426. index = cast(slice, index)
  427. if (
  428. isinstance(index, slice)
  429. and (index.start is not None and index.start < 0)
  430. or (index.stop is not None and index.stop < 0)
  431. ):
  432. raise IndexError("Version index cannot be negative")
  433. part = tuple(
  434. filter(lambda p: p is not None, cast(Iterable, self.to_tuple()[index]))
  435. )
  436. if len(part) == 1:
  437. return part[0]
  438. elif not part:
  439. raise IndexError("Version part undefined")
  440. return part
  441. def __repr__(self) -> str:
  442. s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items())
  443. return "%s(%s)" % (type(self).__name__, s)
  444. def __str__(self) -> str:
  445. version = "%d.%d.%d" % (self.major, self.minor, self.patch)
  446. if self.prerelease:
  447. version += "-%s" % self.prerelease
  448. if self.build:
  449. version += "+%s" % self.build
  450. return version
  451. def __hash__(self) -> int:
  452. return hash(self.to_tuple()[:4])
  453. def finalize_version(self) -> "Version":
  454. """
  455. Remove any prerelease and build metadata from the version.
  456. :return: a new instance with the finalized version string
  457. >>> str(semver.Version.parse('1.2.3-rc.5').finalize_version())
  458. '1.2.3'
  459. """
  460. cls = type(self)
  461. return cls(self.major, self.minor, self.patch)
  462. def match(self, match_expr: str) -> bool:
  463. """
  464. Compare self to match a match expression.
  465. :param match_expr: optional operator and version; valid operators are
  466. ``<`` smaller than
  467. ``>`` greater than
  468. ``>=`` greator or equal than
  469. ``<=`` smaller or equal than
  470. ``==`` equal
  471. ``!=`` not equal
  472. :return: True if the expression matches the version, otherwise False
  473. >>> semver.Version.parse("2.0.0").match(">=1.0.0")
  474. True
  475. >>> semver.Version.parse("1.0.0").match(">1.0.0")
  476. False
  477. >>> semver.Version.parse("4.0.4").match("4.0.4")
  478. True
  479. """
  480. prefix = match_expr[:2]
  481. if prefix in (">=", "<=", "==", "!="):
  482. match_version = match_expr[2:]
  483. elif prefix and prefix[0] in (">", "<"):
  484. prefix = prefix[0]
  485. match_version = match_expr[1:]
  486. elif match_expr and match_expr[0] in "0123456789":
  487. prefix = "=="
  488. match_version = match_expr
  489. else:
  490. raise ValueError(
  491. "match_expr parameter should be in format <op><ver>, "
  492. "where <op> is one of "
  493. "['<', '>', '==', '<=', '>=', '!=']. "
  494. "You provided: %r" % match_expr
  495. )
  496. possibilities_dict = {
  497. ">": (1,),
  498. "<": (-1,),
  499. "==": (0,),
  500. "!=": (-1, 1),
  501. ">=": (0, 1),
  502. "<=": (-1, 0),
  503. }
  504. possibilities = possibilities_dict[prefix]
  505. cmp_res = self.compare(match_version)
  506. return cmp_res in possibilities
  507. @classmethod
  508. def parse(
  509. cls: Type[T], version: String, optional_minor_and_patch: bool = False
  510. ) -> T:
  511. """
  512. Parse version string to a Version instance.
  513. .. versionchanged:: 2.11.0
  514. Changed method from static to classmethod to
  515. allow subclasses.
  516. .. versionchanged:: 3.0.0
  517. Added optional parameter ``optional_minor_and_patch`` to allow
  518. optional minor and patch parts.
  519. :param version: version string
  520. :param optional_minor_and_patch: if set to true, the version string to parse \
  521. can contain optional minor and patch parts. Optional parts are set to zero.
  522. By default (False), the version string to parse has to follow the semver
  523. specification.
  524. :return: a new :class:`Version` instance
  525. :raises ValueError: if version is invalid
  526. :raises TypeError: if version contains the wrong type
  527. >>> semver.Version.parse('3.4.5-pre.2+build.4')
  528. Version(major=3, minor=4, patch=5, \
  529. prerelease='pre.2', build='build.4')
  530. """
  531. if isinstance(version, bytes):
  532. version = version.decode("UTF-8")
  533. elif not isinstance(version, String.__args__): # type: ignore
  534. raise TypeError("not expecting type '%s'" % type(version))
  535. if optional_minor_and_patch:
  536. match = cls._REGEX_OPTIONAL_MINOR_AND_PATCH.match(version)
  537. else:
  538. match = cls._REGEX.match(version)
  539. if match is None:
  540. raise ValueError(f"{version} is not valid SemVer string")
  541. matched_version_parts: Dict[str, Any] = match.groupdict()
  542. if not matched_version_parts["minor"]:
  543. matched_version_parts["minor"] = 0
  544. if not matched_version_parts["patch"]:
  545. matched_version_parts["patch"] = 0
  546. return cls(**matched_version_parts)
  547. def replace(self, **parts: Union[int, Optional[str]]) -> "Version":
  548. """
  549. Replace one or more parts of a version and return a new
  550. :class:`Version` object, but leave self untouched
  551. .. versionadded:: 2.9.0
  552. Added :func:`Version.replace`
  553. :param parts: the parts to be updated. Valid keys are:
  554. ``major``, ``minor``, ``patch``, ``prerelease``, or ``build``
  555. :return: the new :class:`~semver.version.Version` object with
  556. the changed parts
  557. :raises TypeError: if ``parts`` contain invalid keys
  558. """
  559. version = self.to_dict()
  560. version.update(parts)
  561. try:
  562. return Version(**version) # type: ignore
  563. except TypeError:
  564. unknownkeys = set(parts) - set(self.to_dict())
  565. error = "replace() got %d unexpected keyword argument(s): %s" % (
  566. len(unknownkeys),
  567. ", ".join(unknownkeys),
  568. )
  569. raise TypeError(error)
  570. @classmethod
  571. def is_valid(cls, version: str) -> bool:
  572. """
  573. Check if the string is a valid semver version.
  574. .. versionadded:: 2.9.1
  575. .. versionchanged:: 3.0.0
  576. Renamed from :meth:`~semver.version.Version.isvalid`
  577. :param version: the version string to check
  578. :return: True if the version string is a valid semver version, False
  579. otherwise.
  580. """
  581. try:
  582. cls.parse(version)
  583. return True
  584. except ValueError:
  585. return False
  586. def is_compatible(self, other: "Version") -> bool:
  587. """
  588. Check if current version is compatible with other version.
  589. The result is True, if either of the following is true:
  590. * both versions are equal, or
  591. * both majors are equal and higher than 0. Same for both minors.
  592. Both pre-releases are equal, or
  593. * both majors are equal and higher than 0. The minor of b's
  594. minor version is higher then a's. Both pre-releases are equal.
  595. The algorithm does *not* check patches.
  596. .. versionadded:: 3.0.0
  597. :param other: the version to check for compatibility
  598. :return: True, if ``other`` is compatible with the old version,
  599. otherwise False
  600. >>> Version(1, 1, 0).is_compatible(Version(1, 0, 0))
  601. False
  602. >>> Version(1, 0, 0).is_compatible(Version(1, 1, 0))
  603. True
  604. """
  605. if not isinstance(other, Version):
  606. raise TypeError(f"Expected a Version type but got {type(other)}")
  607. # All major-0 versions should be incompatible with anything but itself
  608. if (0 == self.major == other.major) and (self[:4] != other[:4]):
  609. return False
  610. return (
  611. (self.major == other.major)
  612. and (other.minor >= self.minor)
  613. and (self.prerelease == other.prerelease)
  614. )
  615. #: Keep the VersionInfo name for compatibility
  616. VersionInfo = Version