diff --git a/setup.py b/setup.py index ea63e941..facd1382 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ url = "https://github.com/NethServer/python3-nethsec", license = "GPLv3", package_dir = {'': 'src'}, - packages = ['nethsec', 'nethsec.utils', 'nethsec.firewall', 'nethsec.mwan', 'nethsec.dpi', 'nethsec.ipsec', 'nethsec.ovpn', 'nethsec.users', 'nethsec.reverse_proxy', 'nethsec.inventory', 'nethsec.conntrack', 'nethsec.ldif', 'nethsec.objects', 'nethsec.snort'], + packages = ['nethsec', 'nethsec.utils', 'nethsec.firewall', 'nethsec.mwan', 'nethsec.dpi', 'nethsec.ipsec', 'nethsec.ovpn', 'nethsec.users', 'nethsec.reverse_proxy', 'nethsec.inventory', 'nethsec.conntrack', 'nethsec.ldif', 'nethsec.objects', 'nethsec.snort', 'nethsec.semver'], requires = [ "pyuci" ], classifiers = [ "Programming Language :: Python :: 3", diff --git a/src/nethsec/semver/README.md b/src/nethsec/semver/README.md new file mode 100644 index 00000000..a97ecb64 --- /dev/null +++ b/src/nethsec/semver/README.md @@ -0,0 +1,5 @@ +# python3-semver + +Manuallu imported from https://github.com/python-semver/python-semver/ + +Commit: cc5b1d370f74c7a1775da5cfcbaca954bbb9f3bd diff --git a/src/nethsec/semver/__about__.py b/src/nethsec/semver/__about__.py new file mode 100644 index 00000000..21494e68 --- /dev/null +++ b/src/nethsec/semver/__about__.py @@ -0,0 +1,37 @@ +""" +Metadata about semver. + +Contains information about semver's version, the implemented version +of the semver specifictation, author, maintainers, and description. + +.. autodata:: __author__ + +.. autodata:: __description__ + +.. autodata:: __maintainer__ + +.. autodata:: __version__ + +.. autodata:: SEMVER_SPEC_VERSION +""" + +#: Semver version +__version__ = "3.0.4" + +#: Original semver author +__author__ = "Kostiantyn Rybnikov" + +#: Author's email address +__author_email__ = "k-bx@k-bx.com" + +#: Current maintainer +__maintainer__ = ["Sebastien Celles", "Tom Schraitle"] + +#: Maintainer's email address +__maintainer_email__ = "s.celles@gmail.com" + +#: Short description about semver +__description__ = "Python helper for Semantic Versioning (https://semver.org)" + +#: Supported semver specification +SEMVER_SPEC_VERSION = "2.0.0" diff --git a/src/nethsec/semver/__init__.py b/src/nethsec/semver/__init__.py new file mode 100644 index 00000000..6e3fb7ba --- /dev/null +++ b/src/nethsec/semver/__init__.py @@ -0,0 +1,49 @@ +""" +Semver package major release 3. + +A Python module for semantic versioning. Simplifies comparing versions. +""" + +from .version import Version, VersionInfo +from .__about__ import ( + __version__, + __author__, + __maintainer__, + __author_email__, + __description__, + __maintainer_email__, + SEMVER_SPEC_VERSION, +) + +__all__ = [ + "bump_build", + "bump_major", + "bump_minor", + "bump_patch", + "compare", + "bump_prerelease", + "finalize_version", + "format_version", + "match", + "max_ver", + "min_ver", + "parse", + "parse_version_info", + "replace", + "cmd_bump", + "cmd_compare", + "cmd_nextver", + "cmd_check", + "createparser", + "process", + "main", + "Version", + "VersionInfo", + "__version__", + "__author__", + "__maintainer__", + "__author_email__", + "__description__", + "__maintainer_email__", + "SEMVER_SPEC_VERSION", +] diff --git a/src/nethsec/semver/_types.py b/src/nethsec/semver/_types.py new file mode 100644 index 00000000..7afb6ff0 --- /dev/null +++ b/src/nethsec/semver/_types.py @@ -0,0 +1,12 @@ +"""Typing for semver.""" + +from functools import partial +from typing import Union, Optional, Tuple, Dict, Iterable, Callable, TypeVar + +VersionPart = Union[int, Optional[str]] +VersionTuple = Tuple[int, int, int, Optional[str], Optional[str]] +VersionDict = Dict[str, VersionPart] +VersionIterator = Iterable[VersionPart] +String = Union[str, bytes] +F = TypeVar("F", bound=Callable) +Decorator = Union[Callable[..., F], partial] diff --git a/src/nethsec/semver/version.py b/src/nethsec/semver/version.py new file mode 100644 index 00000000..f9450f95 --- /dev/null +++ b/src/nethsec/semver/version.py @@ -0,0 +1,762 @@ +"""Version handling by a semver compatible version class.""" + +import re +from functools import wraps +from typing import ( + Any, + ClassVar, + Dict, + Iterable, + Optional, + Pattern, + SupportsInt, + Tuple, + Union, + cast, + Callable, + Collection, + Type, + TypeVar, +) + +from ._types import ( + VersionTuple, + VersionDict, + VersionIterator, + String, + VersionPart, +) + +# These types are required here because of circular imports +Comparable = Union["Version", Dict[str, VersionPart], Collection[VersionPart], str] +Comparator = Callable[["Version", Comparable], bool] + +T = TypeVar("T", bound="Version") +T_cmp = TypeVar("T_cmp", tuple, str, int) + + +def _comparator(operator: Comparator) -> Comparator: + """Wrap a Version binary op method in a type-check.""" + + @wraps(operator) + def wrapper(self: "Version", other: Comparable) -> bool: + comparable_types = ( + type(self), + dict, + tuple, + list, + *String.__args__, # type: ignore + ) + if not isinstance(other, comparable_types): + return NotImplemented + return operator(self, other) + + return wrapper + + +def _cmp(a: T_cmp, b: T_cmp) -> int: + """Return negative if ab.""" + return (a > b) - (a < b) + + +class Version: + """ + A semver compatible version class. + + See specification at https://semver.org. + + :param major: version when you make incompatible API changes. + :param minor: version when you add functionality in a backwards-compatible manner. + :param patch: version when you make backwards-compatible bug fixes. + :param prerelease: an optional prerelease string + :param build: an optional build string + """ + + __slots__ = ("_major", "_minor", "_patch", "_prerelease", "_build") + + #: The names of the different parts of a version + NAMES: ClassVar[Tuple[str, ...]] = tuple([item[1:] for item in __slots__]) + + #: Regex for number in a build + _LAST_NUMBER: ClassVar[Pattern[str]] = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") + #: Regex for number in a prerelease + _LAST_PRERELEASE: ClassVar[Pattern[str]] = re.compile(r"^(.*\.)?(\d+)$") + #: Regex template for a semver version + _REGEX_TEMPLATE: ClassVar[ + str + ] = r""" + ^ + (?P0|[1-9]\d*) + (?: + \. + (?P0|[1-9]\d*) + (?: + \. + (?P0|[1-9]\d*) + ){opt_patch} + ){opt_minor} + (?:-(?P + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) + (?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))* + ))? + (?:\+(?P + [0-9a-zA-Z-]+ + (?:\.[0-9a-zA-Z-]+)* + ))? + $ + """ + #: Regex for a semver version + _REGEX: ClassVar[Pattern[str]] = re.compile( + _REGEX_TEMPLATE.format(opt_patch="", opt_minor=""), + re.VERBOSE, + ) + #: Regex for a semver version that might be shorter + _REGEX_OPTIONAL_MINOR_AND_PATCH: ClassVar[Pattern[str]] = re.compile( + _REGEX_TEMPLATE.format(opt_patch="?", opt_minor="?"), + re.VERBOSE, + ) + + def __init__( + self, + major: SupportsInt, + minor: SupportsInt = 0, + patch: SupportsInt = 0, + prerelease: Optional[Union[String, int]] = None, + build: Optional[Union[String, int]] = None, + ): + # Build a dictionary of the arguments except prerelease and build + version_parts = {"major": int(major), "minor": int(minor), "patch": int(patch)} + + for name, value in version_parts.items(): + if value < 0: + raise ValueError( + "{!r} is negative. A version can only be positive.".format(name) + ) + + self._major = version_parts["major"] + self._minor = version_parts["minor"] + self._patch = version_parts["patch"] + self._prerelease = None if prerelease is None else str(prerelease) + self._build = None if build is None else str(build) + + @classmethod + def _nat_cmp(cls, a: Optional[str], b: Optional[str]) -> int: + def cmp_prerelease_tag(a, b): + if isinstance(a, int) and isinstance(b, int): + return _cmp(a, b) + elif isinstance(a, int): + return -1 + elif isinstance(b, int): + return 1 + else: + return _cmp(a, b) + + a_parts = [int(x) if x.isdigit() else x for x in (a or "").split(".")] + b_parts = [int(x) if x.isdigit() else x for x in (b or "").split(".")] + + for sub_a, sub_b in zip(a_parts, b_parts): + cmp_result = cmp_prerelease_tag(sub_a, sub_b) + if cmp_result != 0: + return cmp_result + + return _cmp(len(a_parts), len(b_parts)) + + @property + def major(self) -> int: + """The major part of a version (read-only).""" + return self._major + + @major.setter + def major(self, value): + raise AttributeError("attribute 'major' is readonly") + + @property + def minor(self) -> int: + """The minor part of a version (read-only).""" + return self._minor + + @minor.setter + def minor(self, value): + raise AttributeError("attribute 'minor' is readonly") + + @property + def patch(self) -> int: + """The patch part of a version (read-only).""" + return self._patch + + @patch.setter + def patch(self, value): + raise AttributeError("attribute 'patch' is readonly") + + @property + def prerelease(self) -> Optional[str]: + """The prerelease part of a version (read-only).""" + return self._prerelease + + @prerelease.setter + def prerelease(self, value): + raise AttributeError("attribute 'prerelease' is readonly") + + @property + def build(self) -> Optional[str]: + """The build part of a version (read-only).""" + return self._build + + @build.setter + def build(self, value): + raise AttributeError("attribute 'build' is readonly") + + def to_tuple(self) -> VersionTuple: + """ + Convert the Version object to a tuple. + + .. versionadded:: 2.10.0 + Renamed :meth:`Version._astuple` to :meth:`Version.to_tuple` to + make this function available in the public API. + + :return: a tuple with all the parts + + >>> semver.Version(5, 3, 1).to_tuple() + (5, 3, 1, None, None) + """ + return (self.major, self.minor, self.patch, self.prerelease, self.build) + + def to_dict(self) -> VersionDict: + """ + Convert the Version object to an dict. + + .. versionadded:: 2.10.0 + Renamed :meth:`Version._asdict` to :meth:`Version.to_dict` to + make this function available in the public API. + + :return: an dict with the keys in the order ``major``, ``minor``, + ``patch``, ``prerelease``, and ``build``. + + >>> semver.Version(3, 2, 1).to_dict() + {'major': 3, 'minor': 2, 'patch': 1, 'prerelease': None, 'build': None} + """ + return dict( + major=self.major, + minor=self.minor, + patch=self.patch, + prerelease=self.prerelease, + build=self.build, + ) + + def __iter__(self) -> VersionIterator: + """Return iter(self).""" + yield from self.to_tuple() + + @staticmethod + def _increment_prerelease(string: str) -> str: + """ + Check if the last part of a dot-separated string is numeric. If yes, + increase them. Else, add '.0' + + :param string: the prerelease version to increment + :return: the incremented string + """ + match = Version._LAST_PRERELEASE.search(string) + if match: + next_ = str(int(match.group(2)) + 1) + string = match.group(1) + next_ if match.group(1) else next_ + else: + string += ".0" + return string + + @staticmethod + def _increment_string(string: str) -> str: + """ + Look for the last sequence of number(s) in a string and increment. + + :param string: the string to search for. + :return: the incremented string + + Source: + http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 + """ + match = Version._LAST_NUMBER.search(string) + if match: + next_ = str(int(match.group(1)) + 1) + start, end = match.span(1) + string = string[: max(end - len(next_), start)] + next_ + string[end:] + return string + + def bump_major(self) -> "Version": + """ + Raise the major part of the version, return a new object but leave self + untouched. + + :return: new object with the raised major part + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_major() + Version(major=4, minor=0, patch=0, prerelease=None, build=None) + """ + cls = type(self) + return cls(self._major + 1) + + def bump_minor(self) -> "Version": + """ + Raise the minor part of the version, return a new object but leave self + untouched. + + :return: new object with the raised minor part + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_minor() + Version(major=3, minor=5, patch=0, prerelease=None, build=None) + """ + cls = type(self) + return cls(self._major, self._minor + 1) + + def bump_patch(self) -> "Version": + """ + Raise the patch part of the version, return a new object but leave self + untouched. + + :return: new object with the raised patch part + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_patch() + Version(major=3, minor=4, patch=6, prerelease=None, build=None) + """ + cls = type(self) + return cls(self._major, self._minor, self._patch + 1) + + def bump_prerelease( + self, + token: Optional[str] = "rc", + bump_when_empty: Optional[bool] = False + ) -> "Version": + """ + Raise the prerelease part of the version, return a new object but leave + self untouched. + + .. versionchanged:: 3.1.0 + Parameter `bump_when_empty` added. When set to true, bumps the patch version + when called with a version that has no prerelease segment, so the return + value will be considered a newer version. + + Adds `.0` to the prerelease if the last part of the dot-separated + prerelease is not a number. + + :param token: defaults to ``'rc'`` + :return: new :class:`Version` object with the raised prerelease part. + The original object is not modified. + + >>> ver = semver.parse("3.4.5") + >>> ver.bump_prerelease().prerelease + 'rc.1' + >>> ver.bump_prerelease('').prerelease + '1' + >>> ver.bump_prerelease(None).prerelease + 'rc.1' + >>> str(ver.bump_prerelease(bump_when_empty=True)) + '3.4.6-rc.1' + """ + cls = type(self) + patch = self._patch + if self._prerelease is not None: + prerelease = cls._increment_prerelease(self._prerelease) + else: + if bump_when_empty: + patch += 1 + if token == "": + prerelease = "1" + elif token is None: + prerelease = "rc.1" + else: + prerelease = str(token) + ".1" + + return cls(self._major, self._minor, patch, prerelease) + + def bump_build(self, token: Optional[str] = "build") -> "Version": + """ + Raise the build part of the version, return a new object but leave self + untouched. + + :param token: defaults to ``'build'`` + :return: new :class:`Version` object with the raised build part. + The original object is not modified. + + >>> ver = semver.parse("3.4.5-rc.1+build.9") + >>> ver.bump_build() + Version(major=3, minor=4, patch=5, prerelease='rc.1', \ +build='build.10') + """ + cls = type(self) + if self._build is not None: + build = self._build + elif token == "": + build = "0" + elif token is None: + build = "build.0" + else: + build = str(token) + ".0" + + build = cls._increment_string(build) + return cls(self._major, self._minor, self._patch, self._prerelease, build) + + def compare(self, other: Comparable) -> int: + """ + Compare self with other. + + :param other: the second version + :return: The return value is negative if ver1 < ver2, + zero if ver1 == ver2 and strictly positive if ver1 > ver2 + + >>> Version.parse("1.0.0").compare("2.0.0") + -1 + >>> Version.parse("2.0.0").compare("1.0.0") + 1 + >>> Version.parse("2.0.0").compare("2.0.0") + 0 + """ + cls = type(self) + if isinstance(other, String.__args__): # type: ignore + other = cls.parse(other) + elif isinstance(other, dict): + other = cls(**other) + elif isinstance(other, (tuple, list)): + other = cls(*other) + elif not isinstance(other, cls): + raise TypeError( + f"Expected str, bytes, dict, tuple, list, or {cls.__name__} instance, " + f"but got {type(other)}" + ) + + v1 = self.to_tuple()[:3] + v2 = other.to_tuple()[:3] + x = _cmp(v1, v2) + if x: + return x + + rc1, rc2 = self.prerelease, other.prerelease + rccmp = self._nat_cmp(rc1, rc2) + + if not rccmp: + return 0 + if not rc1: + return 1 + elif not rc2: + return -1 + + return rccmp + + def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": + """ + Determines next version, preserving natural order. + + .. versionadded:: 2.10.0 + + This function is taking prereleases into account. + The "major", "minor", and "patch" raises the respective parts like + the ``bump_*`` functions. The real difference is using the + "prerelease" part. It gives you the next patch version of the + prerelease, for example: + + >>> str(semver.parse("0.1.4").next_version("prerelease")) + '0.1.5-rc.1' + + :param part: One of "major", "minor", "patch", or "prerelease" + :param prerelease_token: prefix string of prerelease, defaults to 'rc' + :return: new object with the appropriate part raised + """ + cls = type(self) + # "build" is currently not used, that's why we use [:-1] + validparts = cls.NAMES[:-1] + if part not in validparts: + raise ValueError( + f"Invalid part. Expected one of {validparts}, but got {part!r}" + ) + version = self + if (version.prerelease or version.build) and ( + part == "patch" + or (part == "minor" and version.patch == 0) + or (part == "major" and version.minor == version.patch == 0) + ): + return version.replace(prerelease=None, build=None) + + # Only check the main parts: + if part in cls.NAMES[:3]: + return getattr(version, "bump_" + part)() + else: + return version.bump_prerelease(prerelease_token, bump_when_empty=True) + + @_comparator + def __eq__(self, other: Comparable) -> bool: # type: ignore + return self.compare(other) == 0 + + @_comparator + def __ne__(self, other: Comparable) -> bool: # type: ignore + return self.compare(other) != 0 + + @_comparator + def __lt__(self, other: Comparable) -> bool: + return self.compare(other) < 0 + + @_comparator + def __le__(self, other: Comparable) -> bool: + return self.compare(other) <= 0 + + @_comparator + def __gt__(self, other: Comparable) -> bool: + return self.compare(other) > 0 + + @_comparator + def __ge__(self, other: Comparable) -> bool: + return self.compare(other) >= 0 + + def __getitem__( + self, index: Union[int, slice] + ) -> Union[int, Optional[str], Tuple[Union[int, str], ...]]: + """ + self.__getitem__(index) <==> self[index] Implement getitem. + + If the part requested is undefined, or a part of the range requested + is undefined, it will throw an index error. + Negative indices are not supported. + + :param index: a positive integer indicating the + offset or a :func:`slice` object + :raises IndexError: if index is beyond the range or a part is None + :return: the requested part of the version at position index + + >>> ver = semver.Version.parse("3.4.5") + >>> ver[0], ver[1], ver[2] + (3, 4, 5) + """ + if isinstance(index, int): + index = slice(index, index + 1) + index = cast(slice, index) + + if ( + isinstance(index, slice) + and (index.start is not None and index.start < 0) + or (index.stop is not None and index.stop < 0) + ): + raise IndexError("Version index cannot be negative") + + part = tuple( + filter(lambda p: p is not None, cast(Iterable, self.to_tuple()[index])) + ) + + if len(part) == 1: + return part[0] + elif not part: + raise IndexError("Version part undefined") + return part + + def __repr__(self) -> str: + s = ", ".join("%s=%r" % (key, val) for key, val in self.to_dict().items()) + return "%s(%s)" % (type(self).__name__, s) + + def __str__(self) -> str: + version = "%d.%d.%d" % (self.major, self.minor, self.patch) + if self.prerelease: + version += "-%s" % self.prerelease + if self.build: + version += "+%s" % self.build + return version + + def __hash__(self) -> int: + return hash(self.to_tuple()[:4]) + + def finalize_version(self) -> "Version": + """ + Remove any prerelease and build metadata from the version. + + :return: a new instance with the finalized version string + + >>> str(semver.Version.parse('1.2.3-rc.5').finalize_version()) + '1.2.3' + """ + cls = type(self) + return cls(self.major, self.minor, self.patch) + + def match(self, match_expr: str) -> bool: + """ + Compare self to match a match expression. + + :param match_expr: optional operator and version; valid operators are + ``<`` smaller than + ``>`` greater than + ``>=`` greator or equal than + ``<=`` smaller or equal than + ``==`` equal + ``!=`` not equal + :return: True if the expression matches the version, otherwise False + + >>> semver.Version.parse("2.0.0").match(">=1.0.0") + True + >>> semver.Version.parse("1.0.0").match(">1.0.0") + False + >>> semver.Version.parse("4.0.4").match("4.0.4") + True + """ + prefix = match_expr[:2] + if prefix in (">=", "<=", "==", "!="): + match_version = match_expr[2:] + elif prefix and prefix[0] in (">", "<"): + prefix = prefix[0] + match_version = match_expr[1:] + elif match_expr and match_expr[0] in "0123456789": + prefix = "==" + match_version = match_expr + else: + raise ValueError( + "match_expr parameter should be in format , " + "where is one of " + "['<', '>', '==', '<=', '>=', '!=']. " + "You provided: %r" % match_expr + ) + + possibilities_dict = { + ">": (1,), + "<": (-1,), + "==": (0,), + "!=": (-1, 1), + ">=": (0, 1), + "<=": (-1, 0), + } + + possibilities = possibilities_dict[prefix] + cmp_res = self.compare(match_version) + + return cmp_res in possibilities + + @classmethod + def parse( + cls: Type[T], version: String, optional_minor_and_patch: bool = False + ) -> T: + """ + Parse version string to a Version instance. + + .. versionchanged:: 2.11.0 + Changed method from static to classmethod to + allow subclasses. + .. versionchanged:: 3.0.0 + Added optional parameter ``optional_minor_and_patch`` to allow + optional minor and patch parts. + + :param version: version string + :param optional_minor_and_patch: if set to true, the version string to parse \ + can contain optional minor and patch parts. Optional parts are set to zero. + By default (False), the version string to parse has to follow the semver + specification. + :return: a new :class:`Version` instance + :raises ValueError: if version is invalid + :raises TypeError: if version contains the wrong type + + >>> semver.Version.parse('3.4.5-pre.2+build.4') + Version(major=3, minor=4, patch=5, \ +prerelease='pre.2', build='build.4') + """ + if isinstance(version, bytes): + version = version.decode("UTF-8") + elif not isinstance(version, String.__args__): # type: ignore + raise TypeError("not expecting type '%s'" % type(version)) + + if optional_minor_and_patch: + match = cls._REGEX_OPTIONAL_MINOR_AND_PATCH.match(version) + else: + match = cls._REGEX.match(version) + if match is None: + raise ValueError(f"{version} is not valid SemVer string") + + matched_version_parts: Dict[str, Any] = match.groupdict() + if not matched_version_parts["minor"]: + matched_version_parts["minor"] = 0 + if not matched_version_parts["patch"]: + matched_version_parts["patch"] = 0 + + return cls(**matched_version_parts) + + def replace(self, **parts: Union[int, Optional[str]]) -> "Version": + """ + Replace one or more parts of a version and return a new :class:`Version` + object, but leave self untouched. + + .. versionadded:: 2.9.0 + Added :func:`Version.replace` + + :param parts: the parts to be updated. Valid keys are: + ``major``, ``minor``, ``patch``, ``prerelease``, or ``build`` + :return: the new :class:`~semver.version.Version` object with + the changed parts + :raises TypeError: if ``parts`` contain invalid keys + """ + version = self.to_dict() + version.update(parts) + try: + return type(self)(**version) # type: ignore + except TypeError: + unknownkeys = set(parts) - set(self.to_dict()) + error = "replace() got %d unexpected keyword argument(s): %s" % ( + len(unknownkeys), + ", ".join(unknownkeys), + ) + raise TypeError(error) + + @classmethod + def is_valid(cls, version: str) -> bool: + """ + Check if the string is a valid semver version. + + .. versionadded:: 2.9.1 + + .. versionchanged:: 3.0.0 + Renamed from :meth:`~semver.version.Version.isvalid` + + :param version: the version string to check + :return: True if the version string is a valid semver version, False + otherwise. + """ + try: + cls.parse(version) + return True + except ValueError: + return False + + def is_compatible(self, other: "Version") -> bool: + """ + Check if current version is compatible with other version. + + The result is True, if either of the following is true: + + * both versions are equal, or + * both majors are equal and higher than 0. Same for both minors. + Both pre-releases are equal, or + * both majors are equal and higher than 0. The minor of b's + minor version is higher then a's. Both pre-releases are equal. + + The algorithm does *not* check patches. + + .. versionadded:: 3.0.0 + + :param other: the version to check for compatibility + :return: True, if ``other`` is compatible with the old version, + otherwise False + + >>> Version(1, 1, 0).is_compatible(Version(1, 0, 0)) + False + >>> Version(1, 0, 0).is_compatible(Version(1, 1, 0)) + True + """ + if not isinstance(other, Version): + raise TypeError(f"Expected a Version type but got {type(other)}") + + # All major-0 versions should be incompatible with anything but itself + if (0 == self.major == other.major) and (self[:4] != other[:4]): + return False + + return ( + (self.major == other.major) + and (other.minor >= self.minor) + and (self.prerelease == other.prerelease) + ) + + +#: Keep the VersionInfo name for compatibility +VersionInfo = Version