diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:08:06 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:08:06 +0000 |
commit | ba6d96469df143b52295f8e79da648bf8a597407 (patch) | |
tree | 5ea0c3374f5c53209ad02008dcdddfc8ccae92e5 /dhpython/version.py | |
parent | Initial commit. (diff) | |
download | dh-python-upstream.tar.xz dh-python-upstream.zip |
Adding upstream version 5.20230130+deb12u1.upstream/5.20230130+deb12u1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dhpython/version.py')
-rw-r--r-- | dhpython/version.py | 457 |
1 files changed, 457 insertions, 0 deletions
diff --git a/dhpython/version.py b/dhpython/version.py new file mode 100644 index 0000000..98c16b7 --- /dev/null +++ b/dhpython/version.py @@ -0,0 +1,457 @@ +# Copyright © 2010-2013 Piotr Ożarowski <piotr@debian.org> +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import logging +import re +from os.path import exists + +from dhpython import _defaults + +RANGE_PATTERN = r'(-)?(\d\.\d+)(?:(-)(\d\.\d+)?)?' +RANGE_RE = re.compile(RANGE_PATTERN) +VERSION_RE = re.compile(r''' + (?P<major>\d+)\.? + (?P<minor>\d+)?\.? + (?P<micro>\d+)?[.\s]? + (?P<releaselevel>alpha|beta|candidate|final)?[.\s]? + (?P<serial>\d+)?''', re.VERBOSE) + +log = logging.getLogger('dhpython') +Interpreter = None + + +class Version: + # TODO: Upgrade to PEP-440 + def __init__(self, value=None, major=None, minor=None, micro=None, + releaselevel=None, serial=None): + """Construct a new instance. + + >>> Version(major=0, minor=0, micro=0, releaselevel=0, serial=0) + Version('0.0') + >>> Version('0.0') + Version('0.0') + """ + if isinstance(value, (tuple, list)): + value = '.'.join(str(i) for i in value) + if isinstance(value, Version): + for name in ('major', 'minor', 'micro', 'releaselevel', 'serial'): + setattr(self, name, getattr(value, name)) + return + comp = locals() + del comp['self'] + del comp['value'] + if value: + match = VERSION_RE.match(value) + for name, value in match.groupdict().items() if match else []: + if value is not None and comp[name] is None: + comp[name] = value + for name, value in comp.items(): + if name != 'releaselevel' and value is not None: + value = int(value) + setattr(self, name, value) + if self.major is None: + raise ValueError('major component is required') + + def __str__(self): + """Return major.minor or major string. + + >>> str(Version(major=3, minor=2, micro=1, releaselevel='final', serial=4)) + '3.2' + >>> str(Version(major=2)) + '2' + """ + result = str(self.major) + if self.minor is not None: + result += '.{}'.format(self.minor) + return result + + def __hash__(self): + return hash(repr(self)) + + def __repr__(self): + """Return full version string. + + >>> repr(Version(major=3, minor=2, micro=1, releaselevel='final', serial=4)) + "Version('3.2.1.final.4')" + >>> repr(Version(major=2)) + "Version('2')" + """ + result = "Version('{}".format(self) + for name in ('micro', 'releaselevel', 'serial'): + value = getattr(self, name) + if not value: + break + result += '.{}'.format(value) + return result + "')" + + def __add__(self, other): + """Return next version. + + >>> Version('3.1') + 1 + Version('3.2') + >>> Version('2') + '1' + Version('3') + """ + result = Version(self) + if self.minor is None: + result.major += int(other) + else: + result.minor += int(other) + return result + + def __sub__(self, other): + """Return previous version. + + >>> Version('3.1') - 1 + Version('3.0') + >>> Version('3') - '1' + Version('2') + """ + result = Version(self) + if self.minor is None: + result.major -= int(other) + new = result.major + else: + result.minor -= int(other) + new = result.minor + if new < 0: + raise ValueError('cannot decrease version further') + return result + + def __eq__(self, other): + try: + other = Version(other) + except Exception: + return False + return self.__cmp(other) == 0 + + def __lt__(self, other): + return self.__cmp(other) < 0 + + def __le__(self, other): + return self.__cmp(other) <= 0 + + def __gt__(self, other): + return self.__cmp(other) > 0 + + def __ge__(self, other): + return self.__cmp(other) >= 0 + + def __lshift__(self, other): + """Compare major.minor or major only (if minor is not set). + + >>> Version('2.6') << Version('2.7') + True + >>> Version('2.6') << Version('2.6.6') + False + >>> Version('3') << Version('2') + False + >>> Version('3.1') << Version('2') + False + >>> Version('2') << Version('3.2.1.alpha.3') + True + """ + if not isinstance(other, Version): + other = Version(other) + if self.minor is None or other.minor is None: + return self.__cmp(other, ignore='minor') < 0 + else: + return self.__cmp(other, ignore='micro') < 0 + + def __rshift__(self, other): + """Compare major.minor or major only (if minor is not set). + + >>> Version('2.6') >> Version('2.7') + False + >>> Version('2.6.7') >> Version('2.6.6') + False + >>> Version('3') >> Version('2') + True + >>> Version('3.1') >> Version('2') + True + >>> Version('2.1') >> Version('3.2.1.alpha.3') + False + """ + if not isinstance(other, Version): + other = Version(other) + if self.minor is None or other.minor is None: + return self.__cmp(other, ignore='minor') > 0 + else: + return self.__cmp(other, ignore='micro') > 0 + + def __cmp(self, other, ignore=None): + if not isinstance(other, Version): + other = Version(other) + for name in ('major', 'minor', 'micro', 'releaselevel', 'serial'): + if name == ignore: + break + value1 = getattr(self, name) or 0 + value2 = getattr(other, name) or 0 + if name == 'releaselevel': + rmap = {'alpha': -3, 'beta': -2, 'candidate': -1, 'final': 0} + value1 = rmap.get(value1, 0) + value2 = rmap.get(value2, 0) + if value1 == value2: + continue + return (value1 > value2) - (value1 < value2) + return 0 + + +class VersionRange: + def __init__(self, value=None, minver=None, maxver=None): + if minver: + self.minver = Version(minver) + else: + self.minver = None + if maxver: + self.maxver = Version(maxver) + else: + self.maxver = None + + if value: + minver, maxver = self.parse(value) + if minver and self.minver is None: + self.minver = minver + if maxver and self.maxver is None: + self.maxver = maxver + + def __bool__(self): + if self.minver is not None or self.maxver is not None: + return True + return False + + def __str__(self): + """Return version range string from given range. + + >>> str(VersionRange(minver='3.4')) + '3.4-' + >>> str(VersionRange(minver='3.4', maxver='3.6')) + '3.4-3.6' + >>> str(VersionRange(minver='3.4', maxver='4.0')) + '3.4-4.0' + >>> str(VersionRange(maxver='3.7')) + '-3.7' + >>> str(VersionRange(minver='3.5', maxver='3.5')) + '3.5' + >>> str(VersionRange()) + '-' + """ + if self.minver is None is self.maxver: + return '-' + if self.minver == self.maxver: + return str(self.minver) + elif self.minver is None: + return '-{}'.format(self.maxver) + elif self.maxver is None: + return '{}-'.format(self.minver) + else: + return '{}-{}'.format(self.minver, self.maxver) + + def __repr__(self): + """Return version range string. + + >>> repr(VersionRange('5.0-')) + "VersionRange(minver='5.0')" + >>> repr(VersionRange('3.0-3.5')) + "VersionRange(minver='3.0', maxver='3.5')" + """ + result = 'VersionRange(' + if self.minver is not None: + result += "minver='{}'".format(self.minver) + if self.maxver is not None: + result += ", maxver='{}'".format(self.maxver) + result = result.replace('(, ', '(') + return result + ")" + + @staticmethod + def parse(value): + """Return minimum and maximum Python version from given range. + + >>> VersionRange.parse('3.0-') + (Version('3.0'), None) + >>> VersionRange.parse('3.1-3.13') + (Version('3.1'), Version('3.13')) + >>> VersionRange.parse('3.2-4.0') + (Version('3.2'), Version('4.0')) + >>> VersionRange.parse('-3.7') + (None, Version('3.7')) + >>> VersionRange.parse('3.2') + (Version('3.2'), Version('3.2')) + >>> VersionRange.parse('') == VersionRange.parse('-') + True + >>> VersionRange.parse('>= 4.0') + (Version('4.0'), None) + """ + if value in ('', '-'): + return None, None + + match = RANGE_RE.match(value) + if not match: + try: + minv, maxv = VersionRange._parse_pycentral(value) + except Exception: + raise ValueError("version range is invalid: %s" % value) + else: + groups = match.groups() + + if list(groups).count(None) == 3: # only one version is allowed + minv = Version(groups[1]) + return minv, minv + + minv = maxv = None + if groups[0]: # maximum version only + maxv = groups[1] + else: + minv = groups[1] + maxv = groups[3] + + minv = Version(minv) if minv else None + maxv = Version(maxv) if maxv else None + + if maxv and minv and minv > maxv: + raise ValueError("version range is invalid: %s" % value) + + return minv, maxv + + @staticmethod + def _parse_pycentral(value): + """Parse X-Python3-Version. + + >>> VersionRange._parse_pycentral('>= 3.10') + (Version('3.10'), None) + >>> VersionRange._parse_pycentral('<< 4.0') + (None, Version('4.0')) + >>> VersionRange._parse_pycentral('3.1') + (Version('3.1'), Version('3.1')) + >>> VersionRange._parse_pycentral('3.1, 3.2') + (Version('3.1'), None) + """ + + minv = maxv = None + hardcoded = set() + + for item in value.split(','): + item = item.strip() + + match = re.match('>=\s*([\d\.]+)', item) + if match: + minv = match.group(1) + continue + match = re.match('<<\s*([\d\.]+)', item) + if match: + maxv = match.group(1) + continue + match = re.match('^[\d\.]+$', item) + if match: + hardcoded.add(match.group(0)) + + if len(hardcoded) == 1: + ver = hardcoded.pop() + return Version(ver), Version(ver) + + if not minv and hardcoded: + # yeah, no maxv! + minv = sorted(hardcoded)[0] + + return Version(minv) if minv else None, Version(maxv) if maxv else None + + +def default(impl): + """Return default interpreter version for given implementation.""" + if impl not in _defaults.DEFAULT: + raise ValueError("interpreter implementation not supported: %r" % impl) + ver = _defaults.DEFAULT[impl] + return Version(major=ver[0], minor=ver[1]) + + +def supported(impl): + """Return list of supported interpreter versions for given implementation.""" + if impl not in _defaults.SUPPORTED: + raise ValueError("interpreter implementation not supported: %r" % impl) + versions = _defaults.SUPPORTED[impl] + return [Version(major=v[0], minor=v[1]) for v in versions] + + +def get_requested_versions(impl, vrange=None, available=None): + """Return a set of requested and supported Python versions. + + :param impl: interpreter implementation + :param available: if set to `True`, return installed versions only, + if set to `False`, return requested versions that are not installed. + By default returns all requested versions. + :type available: bool + + >>> sorted(get_requested_versions('cpython3', '')) == sorted(supported('cpython3')) + True + >>> sorted(get_requested_versions('cpython3', '-')) == sorted(supported('cpython3')) + True + >>> get_requested_versions('cpython3', '>= 5.0') + set() + """ + if isinstance(vrange, str): + vrange = VersionRange(vrange) + + if not vrange: + versions = set(supported(impl)) + else: + minv = Version(major=0, minor=0) if vrange.minver is None else vrange.minver + maxv = Version(major=99, minor=99) if vrange.maxver is None else vrange.maxver + if minv == maxv: + versions = set([minv] if minv in supported(impl) else tuple()) + else: + versions = set(v for v in supported(impl) if minv <= v < maxv) + + if available is not None: + # to avoid circular imports + global Interpreter + if Interpreter is None: + from dhpython.interpreter import Interpreter + if available: + interpreter = Interpreter(impl=impl) + versions = set(v for v in versions + if exists(interpreter.binary(v))) + elif available is False: + interpreter = Interpreter(impl=impl) + versions = set(v for v in versions + if not exists(interpreter.binary(v))) + + return versions + + +def build_sorted(versions, impl='cpython3'): + """Return sorted list of versions in a build friendly order. + + i.e. default version, if among versions, is sorted last. + + >>> build_sorted([(2, 6), (3, 4), default('cpython3'), (3, 6), (2, 7)])[-1] == default('cpython3') + True + >>> build_sorted(('3.2', (3, 0), '3.1')) + [Version('3.0'), Version('3.1'), Version('3.2')] + """ + default_ver = default(impl) + + result = sorted(Version(v) for v in versions) + try: + result.remove(default_ver) + except ValueError: + pass + else: + result.append(default_ver) + return result |