Source code for polyvers.vermath

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2015-2018 European Commission (JRC);
# Licensed under the EUPL 1.2+ (the 'Licence');
# You may not use this work except in compliance with the Licence.
# You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
#
"""Validate absolute versions or add relative ones on top of a base absolute."""

from typing import Union, Optional
import re

from packaging.version import InvalidVersion, Version, _parse_letter_version

import itertools as itt

from ._vendor.traitlets import traitlets as trt
from .cmdlet import cmdlets


VerLike = Union[str, Version]


[docs]class VersionError(cmdlets.CmdException): pass
def _packver(v: VerLike) -> Version: try: return v if isinstance(v, Version) else Version(str(v)) except InvalidVersion as ex: raise VersionError(str(ex))
[docs]class Pep440Version(trt.Instance): """A trait parsing text like python "slice" expression (ie ``-10::2``).""" klass = Version _cast_types = str # type: ignore
[docs] def cast(self, value): return _packver(value)
#: Possible to skip release-numbers, no local-part. #: Adapted from :data:`packaging.VERSION_PATTERN`. _relative_ver_regex = re.compile(r"""(?ix) ^\s* v? (?: (?P<op>[+^]) # relative operator (?P<fix>=?) # fix pre/post/dev parts (?:(?P<epoch>[0-9]+)!)? # epoch (?P<release>[0-9]+(?:\.[0-9]+)*)? # release segment (?P<pre> # pre-release (?P<fixpre>=?) # fix pre part [-_\.]? (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview)) [-_\.]? (?P<pre_n>[0-9]+)? )? (?P<post> # post release (?P<fixpost>=?) # fix post part (?: (?:-(?P<post_n1>[0-9]+)) | (?: [-_\.]? (?P<post_l>post|rev|r) [-_\.]? (?P<post_n2>[0-9]+)? ) ) )? (?P<dev> # dev release (?P<fixdev>=?) # fix dev part [-_\.]? (?P<dev_l>dev) [-_\.]? (?P<dev_n>[0-9]+)? )? ) (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version \s*$""") def is_version_id_relative(version_str: VerLike) -> bool: return _relative_ver_regex.match(str(version_str)) is not None def _add_pre(base_tuple, rel_label, rel_num): assert base_tuple is not None or rel_label is not None, ( base_tuple, rel_label, rel_num) if not rel_label: return base_tuple blabel, bnum = base_tuple or (None, 0) rlabel, rnum = _parse_letter_version(rel_label, rel_num) if blabel == rlabel: return blabel, bnum + rnum else: return rlabel, rnum def _add_versions(base_ver: VerLike, relative_ver): ## TODO: epoch vermath, and update README bver = _packver(base_ver) m = _relative_ver_regex.match(str(relative_ver)) if not m: raise VersionError("Invalid relative version: {}".format(relative_ver)) op = m.group('op') ver_nums = list(bver.release) rel_release = m.group('release') if rel_release: # ## Caret(^) makes a difference only for release-digits. rel_nums = [int(d) for d in rel_release.split('.')] if op == '^': ## Extend caret version from base-version's last digit. # ver_nums[-1] += rel_nums[0] ver_nums.extend(rel_nums[1:]) elif op == '+': ver_nums = [a + b for a, b in itt.zip_longest(ver_nums, rel_nums, fillvalue=0)] else: raise AssertionError(op) parts = ['.'.join(str(i) for i in ver_nums)] fix_parts = bool(m.group('fix')) """When `fix`, pre/post/dev parts are not reset if earlier parts have changed, and relative-number is added on top of existing base one.""" are_previous_parts_changed = bool(rel_release) """A rolling flag tracking if any earlier release/pre/post/dev part has changed. Used to decide whether to update/reset/clear the part-number .""" def is_part_in_new_version(rel_exist, base_exist, part_fix) -> bool: """ decide whether the new-version must have pre/post/dev part, base on previous parts that have been updated. :param rel_exists: if relative-version has a pre/post/dev part :param base_exists: if base-version has a pre/post/dev part :return: true when the part must be updated """ nonlocal are_previous_parts_changed must_update = bool(rel_exist or base_exist and (not are_previous_parts_changed or fix_parts or part_fix)) are_previous_parts_changed |= must_update return must_update def rebase_part(base_part: Optional[int]) -> int: "conditionally reset pre/post/dev part if earlier parts have changed" return (0 if are_previous_parts_changed and not fix_parts else base_part or 0) if is_part_in_new_version(m.group('pre'), bver.pre, m.group('fixpre')): bver_pre = bver.pre if bver_pre: bver_pre = (bver_pre[0], rebase_part(bver_pre[1])) parts.append('%s%s' % _add_pre(bver_pre, m.group('pre_l'), m.group('pre_n'))) if is_part_in_new_version(m.group('post'), bver.post is not None, m.group('fixpost')): rel_post = m.group('post_n1') or m.group('post_n2') or 0 new_post = rebase_part(bver.post) + int(rel_post) parts.append(".post%s" % new_post) if is_part_in_new_version(m.group('dev'), bver.dev is not None, m.group('fixdev')): new_dev = rebase_part(bver.dev) + int(m.group('dev_n') or 0) parts.append(".dev%s" % new_dev) if bver.local: parts.append('+' + bver.local) new_version = ''.join(parts) return _packver(new_version)
[docs]def add_versions(v1: VerLike, *rel_versions: VerLike) -> Version: """return the "sum" of the the given two versions.""" new_version = v1 for v2 in rel_versions: new_version = _add_versions(new_version, v2) v1 = _packver(v1) ## TODO: make backward bump forceable. if new_version < v1: raise VersionError("Backward bump is forbidden: %s -/-> %s" % (v1, new_version)) return new_version