Source code for tank.util.version

# Copyright (c) 2016 Shotgun Software Inc.
#
# CONFIDENTIAL AND PROPRIETARY
#
# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit
# Source Code License included in this distribution package. See LICENSE.
# By accessing, using, copying or modifying this work you indicate your
# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights
# not expressly granted therein are reserved by Shotgun Software Inc.
import warnings
import contextlib
import sys

if sys.version_info[0:2] < (3, 10):
    from distutils.version import LooseVersion
else:
    try:
        from setuptools._distutils.version import LooseVersion
    except ModuleNotFoundError:
        # Known issue with VRED 16. Unable to load the setuptools module
        from distutils.version import LooseVersion

from . import sgre as re
from ..errors import TankError


GITHUB_HASH_RE = re.compile("^[0-9a-fA-F]{7,40}$")


def is_version_head(version):
    """
    Returns if the specified version is HEAD or MASTER. The comparison is case insensitive.

    :param version: Version to test.

    :returns: True if version is HEAD or MASTER, false otherwise.
    """
    return version.lower() in ["head", "master"]


def _is_git_commit(version):
    """
    Returns if the version looks like a git commit id.

    :param version: Version to test.

    :returns: True if the version is a commit id, false otherwise.
    """
    return GITHUB_HASH_RE.match(version) is not None


[docs]def is_version_newer(a, b): """ Is the version string ``a`` newer than ``b``? If one of the version is `master`, `head`, or formatted like a git commit sha, it is considered more recent than the other version. :raises TankError: Raised if the two versions are different git commit shas, as they can't be compared. :returns: ``True`` if ``a`` is newer than ``b`` but not equal, ``False`` otherwise. """ return _compare_versions(a, b)
[docs]def is_version_older(a, b): """ Is the version string ``a`` older than ``b``? If one of the version is `master`, `head`, or formatted like a git commit sha, it is considered more recent than the other version. :raises TankError: Raised if the two versions are different git commit shas, as they can't be compared. :returns: ``True`` if ``a`` is older than ``b`` but not equal, ``False`` otherwise. """ return _compare_versions(b, a)
[docs]def is_version_newer_or_equal(a, b): """ Is the version string ``a`` newer than or equal to ``b``? If one of the version is `master`, `head`, or formatted like a git commit sha, it is considered more recent than the other version. :raises TankError: Raised if the two versions are different git commit shas, as they can't be compared. :returns: ``True`` if ``a`` is newer than or equal to ``b``, ``False`` otherwise. """ return is_version_older(a, b) is False
[docs]def is_version_older_or_equal(a, b): """ Is the version string ``a`` older than or equal to ``b``? If one of the version is `master`, `head`, or formatted like a git commit sha, it is considered more recent than the other version. :raises TankError: Raised if the two versions are different git commit shas, as they can't be compared. :returns: ``True`` if ``a`` is older than or equal to ``b``, ``False`` otherwise. """ return is_version_newer(a, b) is False
def is_version_number(version): r""" Tests whether the given string is a properly formed version number (ex: v1.2.3). The test is made using the pattern r"v\d+.\d+.\d+$" :param str version: The version string to test. :rtype: bool """ match = re.match(r"v\d+.\d+.\d+$", version) if match: return True else: return False @contextlib.contextmanager def suppress_known_deprecation(): """ Imported function from setuptools.distutils module """ with warnings.catch_warnings(record=True) as ctx: warnings.filterwarnings( action="default", category=DeprecationWarning, message="distutils Version classes are deprecated.", ) yield ctx def _compare_versions(a, b): """ Tests if version a is newer than version b. :param str a: The first version string to compare. :param str b: The second version string to compare. :rtype: bool """ if b in [None, "Undefined"]: # a is always newer than None or `Undefined` return True if _is_git_commit(a) and not _is_git_commit(b): return True elif _is_git_commit(b) and not _is_git_commit(a): return False elif _is_git_commit(a) and _is_git_commit(b): if a.lower() == b.lower(): return False else: raise TankError("Can't compare two git commits lexicographically.") if is_version_head(a): # our version is latest return True if is_version_head(b): # comparing against HEAD - our version is always old return False if a.startswith("v"): a = a[1:] if b.startswith("v"): b = b[1:] # In Python 3, LooseVersion comparisons between versions where a non-numeric # version component is compared to a numeric one fail. We'll work around this # as follows: # First, try to use LooseVersion for comparison. This should work in # most cases. try: with suppress_known_deprecation(): # Supress `distutils Version classes are deprecated.` for Python 3.10 version_a = LooseVersion(a).version version_b = LooseVersion(b).version version_num_a = [] version_num_b = [] # taking only the integers of the version to make comparison for version in version_a: if isinstance(version, (int)): version_num_a.append(version) elif version == "-": break for version in version_b: if isinstance(version, (int)): version_num_b.append(version) elif version == "-": break # Comparing equal number versions with with one of them with '-' appended, if a version # has '-' appended it's older than the same version with '-' at the end if version_num_a == version_num_b: if "-" in a and "-" not in b: return False # False, version a is older than b elif "-" in b and "-" not in a: return True # True, version a is older than b else: return LooseVersion(a) > LooseVersion( b ) # If both has '-' compare '-rcx' versions else: return LooseVersion(a) > LooseVersion( b ) # If they are different numeric versions except TypeError: # To mimick the behavior in Python 2.7 as closely as possible, we will # If LooseVersion comparison didn't work, try to extract a numeric # version from both versions for comparison version_expr = re.compile(r"^((?:\d+)(?:\.\d+)*)(.+)$") match_a = version_expr.match(a) match_b = version_expr.match(b) if match_a and match_b: # If we could get two numeric versions, generate LooseVersions for # them. ver_a = LooseVersion(match_a.group(1)) ver_b = LooseVersion(match_b.group(1)) if ver_a != ver_b: # If they're not identical, return based on this comparison return ver_a > ver_b else: # If the numeric versions do match, do a string comparsion for # the rest. return match_a.group(2) > match_b.group(2) elif match_a or match_b: # If only one had a numeric version, treat that as the newer version. return bool(match_a) # In the case that both versions are non-numeric, do a string comparison. return a > b