gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-build-common] 02/02: add version range checks


From: gnunet
Subject: [taler-build-common] 02/02: add version range checks
Date: Mon, 30 Mar 2020 09:18:16 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository build-common.

commit b2604317980b9cb26f1445dfb6ff82c84f9cbe65
Author: Florian Dold <address@hidden>
AuthorDate: Mon Mar 30 12:46:22 2020 +0530

    add version range checks
---
 semver.py           | 1420 +++++++++++++++++++++++++++++++++++++++++++++++++++
 talerbuildconfig.py |   19 +-
 testconfigure.py    |    2 +-
 3 files changed, 1438 insertions(+), 3 deletions(-)

diff --git a/semver.py b/semver.py
new file mode 100644
index 0000000..7fd871e
--- /dev/null
+++ b/semver.py
@@ -0,0 +1,1420 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) The python-semanticversion project
+# This code is distributed under the two-clause BSD License.
+
+import functools
+import re
+import warnings
+
+
+def _has_leading_zero(value):
+    return (value
+            and value[0] == '0'
+            and value.isdigit()
+            and value != '0')
+
+
+class MaxIdentifier(object):
+    __slots__ = []
+
+    def __repr__(self):
+        return 'MaxIdentifier()'
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__)
+
+
+@functools.total_ordering
+class NumericIdentifier(object):
+    __slots__ = ['value']
+
+    def __init__(self, value):
+        self.value = int(value)
+
+    def __repr__(self):
+        return 'NumericIdentifier(%r)' % self.value
+
+    def __eq__(self, other):
+        if isinstance(other, NumericIdentifier):
+            return self.value == other.value
+        return NotImplemented
+
+    def __lt__(self, other):
+        if isinstance(other, MaxIdentifier):
+            return True
+        elif isinstance(other, AlphaIdentifier):
+            return True
+        elif isinstance(other, NumericIdentifier):
+            return self.value < other.value
+        else:
+            return NotImplemented
+
+
+@functools.total_ordering
+class AlphaIdentifier(object):
+    __slots__ = ['value']
+
+    def __init__(self, value):
+        self.value = value.encode('ascii')
+
+    def __repr__(self):
+        return 'AlphaIdentifier(%r)' % self.value
+
+    def __eq__(self, other):
+        if isinstance(other, AlphaIdentifier):
+            return self.value == other.value
+        return NotImplemented
+
+    def __lt__(self, other):
+        if isinstance(other, MaxIdentifier):
+            return True
+        elif isinstance(other, NumericIdentifier):
+            return False
+        elif isinstance(other, AlphaIdentifier):
+            return self.value < other.value
+        else:
+            return NotImplemented
+
+
+class Version(object):
+
+    version_re = 
re.compile(r'^(\d+)\.(\d+)\.(\d+)(?:-([0-9a-zA-Z.-]+))?(?:\+([0-9a-zA-Z.-]+))?$')
+    partial_version_re = 
re.compile(r'^(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:-([0-9a-zA-Z.-]*))?(?:\+([0-9a-zA-Z.-]*))?$')
+
+    def __init__(
+            self,
+            version_string=None,
+            major=None,
+            minor=None,
+            patch=None,
+            prerelease=None,
+            build=None,
+            partial=False):
+        if partial:
+            warnings.warn(
+                "Partial versions will be removed in 3.0; use 
SimpleSpec('1.x.x') instead.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+        has_text = version_string is not None
+        has_parts = not (major is minor is patch is prerelease is build is 
None)
+        if not has_text ^ has_parts:
+            raise ValueError("Call either Version('1.2.3') or Version(major=1, 
...).")
+
+        if has_text:
+            major, minor, patch, prerelease, build = 
self.parse(version_string, partial)
+        else:
+            # Convenience: allow to omit prerelease/build.
+            prerelease = tuple(prerelease or ())
+            if not partial:
+                build = tuple(build or ())
+            self._validate_kwargs(major, minor, patch, prerelease, build, 
partial)
+
+        self.major = major
+        self.minor = minor
+        self.patch = patch
+        self.prerelease = prerelease
+        self.build = build
+
+        self.partial = partial
+
+    @classmethod
+    def _coerce(cls, value, allow_none=False):
+        if value is None and allow_none:
+            return value
+        return int(value)
+
+    def next_major(self):
+        if self.prerelease and self.minor == self.patch == 0:
+            return Version(
+                major=self.major,
+                minor=0,
+                patch=0,
+                partial=self.partial,
+            )
+        else:
+            return Version(
+                major=self.major + 1,
+                minor=0,
+                patch=0,
+                partial=self.partial,
+            )
+
+    def next_minor(self):
+        if self.prerelease and self.patch == 0:
+            return Version(
+                major=self.major,
+                minor=self.minor,
+                patch=0,
+                partial=self.partial,
+            )
+        else:
+            return Version(
+                major=self.major,
+                minor=self.minor + 1,
+                patch=0,
+                partial=self.partial,
+            )
+
+    def next_patch(self):
+        if self.prerelease:
+            return Version(
+                major=self.major,
+                minor=self.minor,
+                patch=self.patch,
+                partial=self.partial,
+            )
+        else:
+            return Version(
+                major=self.major,
+                minor=self.minor,
+                patch=self.patch + 1,
+                partial=self.partial,
+            )
+
+    def truncate(self, level='patch'):
+        """Return a new Version object, truncated up to the selected level."""
+        if level == 'build':
+            return self
+        elif level == 'prerelease':
+            return Version(
+                major=self.major,
+                minor=self.minor,
+                patch=self.patch,
+                prerelease=self.prerelease,
+                partial=self.partial,
+            )
+        elif level == 'patch':
+            return Version(
+                major=self.major,
+                minor=self.minor,
+                patch=self.patch,
+                partial=self.partial,
+            )
+        elif level == 'minor':
+            return Version(
+                major=self.major,
+                minor=self.minor,
+                patch=None if self.partial else 0,
+                partial=self.partial,
+            )
+        elif level == 'major':
+            return Version(
+                major=self.major,
+                minor=None if self.partial else 0,
+                patch=None if self.partial else 0,
+                partial=self.partial,
+            )
+        else:
+            raise ValueError("Invalid truncation level `%s`." % level)
+
+    @classmethod
+    def coerce(cls, version_string, partial=False):
+        """Coerce an arbitrary version string into a semver-compatible one.
+
+        The rule is:
+        - If not enough components, fill minor/patch with zeroes; unless
+          partial=True
+        - If more than 3 dot-separated components, extra components are "build"
+          data. If some "build" data already appeared, append it to the
+          extra components
+
+        Examples:
+            >>> Version.coerce('0.1')
+            Version(0, 1, 0)
+            >>> Version.coerce('0.1.2.3')
+            Version(0, 1, 2, (), ('3',))
+            >>> Version.coerce('0.1.2.3+4')
+            Version(0, 1, 2, (), ('3', '4'))
+            >>> Version.coerce('0.1+2-3+4_5')
+            Version(0, 1, 0, (), ('2-3', '4-5'))
+        """
+        base_re = re.compile(r'^\d+(?:\.\d+(?:\.\d+)?)?')
+
+        match = base_re.match(version_string)
+        if not match:
+            raise ValueError(
+                "Version string lacks a numerical component: %r"
+                % version_string
+            )
+
+        version = version_string[:match.end()]
+        if not partial:
+            # We need a not-partial version.
+            while version.count('.') < 2:
+                version += '.0'
+
+        # Strip leading zeros in components
+        # Version is of the form nn, nn.pp or nn.pp.qq
+        version = '.'.join(
+            # If the part was '0', we end up with an empty string.
+            part.lstrip('0') or '0'
+            for part in version.split('.')
+        )
+
+        if match.end() == len(version_string):
+            return Version(version, partial=partial)
+
+        rest = version_string[match.end():]
+
+        # Cleanup the 'rest'
+        rest = re.sub(r'[^a-zA-Z0-9+.-]', '-', rest)
+
+        if rest[0] == '+':
+            # A 'build' component
+            prerelease = ''
+            build = rest[1:]
+        elif rest[0] == '.':
+            # An extra version component, probably 'build'
+            prerelease = ''
+            build = rest[1:]
+        elif rest[0] == '-':
+            rest = rest[1:]
+            if '+' in rest:
+                prerelease, build = rest.split('+', 1)
+            else:
+                prerelease, build = rest, ''
+        elif '+' in rest:
+            prerelease, build = rest.split('+', 1)
+        else:
+            prerelease, build = rest, ''
+
+        build = build.replace('+', '.')
+
+        if prerelease:
+            version = '%s-%s' % (version, prerelease)
+        if build:
+            version = '%s+%s' % (version, build)
+
+        return cls(version, partial=partial)
+
+    @classmethod
+    def parse(cls, version_string, partial=False, coerce=False):
+        """Parse a version string into a Version() object.
+
+        Args:
+            version_string (str), the version string to parse
+            partial (bool), whether to accept incomplete input
+            coerce (bool), whether to try to map the passed in string into a
+                valid Version.
+        """
+        if not version_string:
+            raise ValueError('Invalid empty version string: %r' % 
version_string)
+
+        if partial:
+            version_re = cls.partial_version_re
+        else:
+            version_re = cls.version_re
+
+        match = version_re.match(version_string)
+        if not match:
+            raise ValueError('Invalid version string: %r' % version_string)
+
+        major, minor, patch, prerelease, build = match.groups()
+
+        if _has_leading_zero(major):
+            raise ValueError("Invalid leading zero in major: %r" % 
version_string)
+        if _has_leading_zero(minor):
+            raise ValueError("Invalid leading zero in minor: %r" % 
version_string)
+        if _has_leading_zero(patch):
+            raise ValueError("Invalid leading zero in patch: %r" % 
version_string)
+
+        major = int(major)
+        minor = cls._coerce(minor, partial)
+        patch = cls._coerce(patch, partial)
+
+        if prerelease is None:
+            if partial and (build is None):
+                # No build info, strip here
+                return (major, minor, patch, None, None)
+            else:
+                prerelease = ()
+        elif prerelease == '':
+            prerelease = ()
+        else:
+            prerelease = tuple(prerelease.split('.'))
+            cls._validate_identifiers(prerelease, allow_leading_zeroes=False)
+
+        if build is None:
+            if partial:
+                build = None
+            else:
+                build = ()
+        elif build == '':
+            build = ()
+        else:
+            build = tuple(build.split('.'))
+            cls._validate_identifiers(build, allow_leading_zeroes=True)
+
+        return (major, minor, patch, prerelease, build)
+
+    @classmethod
+    def _validate_identifiers(cls, identifiers, allow_leading_zeroes=False):
+        for item in identifiers:
+            if not item:
+                raise ValueError(
+                    "Invalid empty identifier %r in %r"
+                    % (item, '.'.join(identifiers))
+                )
+
+            if item[0] == '0' and item.isdigit() and item != '0' and not 
allow_leading_zeroes:
+                raise ValueError("Invalid leading zero in identifier %r" % 
item)
+
+    @classmethod
+    def _validate_kwargs(cls, major, minor, patch, prerelease, build, partial):
+        if (
+                major != int(major)
+                or minor != cls._coerce(minor, partial)
+                or patch != cls._coerce(patch, partial)
+                or prerelease is None and not partial
+                or build is None and not partial
+        ):
+            raise ValueError(
+                "Invalid kwargs to Version(major=%r, minor=%r, patch=%r, "
+                "prerelease=%r, build=%r, partial=%r" % (
+                    major, minor, patch, prerelease, build, partial
+                ))
+        if prerelease is not None:
+            cls._validate_identifiers(prerelease, allow_leading_zeroes=False)
+        if build is not None:
+            cls._validate_identifiers(build, allow_leading_zeroes=True)
+
+    def __iter__(self):
+        return iter((self.major, self.minor, self.patch, self.prerelease, 
self.build))
+
+    def __str__(self):
+        version = '%d' % self.major
+        if self.minor is not None:
+            version = '%s.%d' % (version, self.minor)
+        if self.patch is not None:
+            version = '%s.%d' % (version, self.patch)
+
+        if self.prerelease or (self.partial and self.prerelease == () and 
self.build is None):
+            version = '%s-%s' % (version, '.'.join(self.prerelease))
+        if self.build or (self.partial and self.build == ()):
+            version = '%s+%s' % (version, '.'.join(self.build))
+        return version
+
+    def __repr__(self):
+        return '%s(%r%s)' % (
+            self.__class__.__name__,
+            str(self),
+            ', partial=True' if self.partial else '',
+        )
+
+    def __hash__(self):
+        # We don't include 'partial', since this is strictly equivalent to 
having
+        # at least a field being `None`.
+        return hash((self.major, self.minor, self.patch, self.prerelease, 
self.build))
+
+    @property
+    def precedence_key(self):
+        if self.prerelease:
+            prerelease_key = tuple(
+                NumericIdentifier(part) if re.match(r'^[0-9]+$', part) else 
AlphaIdentifier(part)
+                for part in self.prerelease
+            )
+        else:
+            prerelease_key = (
+                MaxIdentifier(),
+            )
+
+        return (
+            self.major,
+            self.minor,
+            self.patch,
+            prerelease_key,
+        )
+
+    def __cmp__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        if self < other:
+            return -1
+        elif self > other:
+            return 1
+        elif self == other:
+            return 0
+        else:
+            return NotImplemented
+
+    def __eq__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        return (
+            self.major == other.major
+            and self.minor == other.minor
+            and self.patch == other.patch
+            and (self.prerelease or ()) == (other.prerelease or ())
+            and (self.build or ()) == (other.build or ())
+        )
+
+    def __ne__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        return tuple(self) != tuple(other)
+
+    def __lt__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        return self.precedence_key < other.precedence_key
+
+    def __le__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        return self.precedence_key <= other.precedence_key
+
+    def __gt__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        return self.precedence_key > other.precedence_key
+
+    def __ge__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+        return self.precedence_key >= other.precedence_key
+
+
+class SpecItem(object):
+    """A requirement specification."""
+
+    KIND_ANY = '*'
+    KIND_LT = '<'
+    KIND_LTE = '<='
+    KIND_EQUAL = '=='
+    KIND_SHORTEQ = '='
+    KIND_EMPTY = ''
+    KIND_GTE = '>='
+    KIND_GT = '>'
+    KIND_NEQ = '!='
+    KIND_CARET = '^'
+    KIND_TILDE = '~'
+    KIND_COMPATIBLE = '~='
+
+    # Map a kind alias to its full version
+    KIND_ALIASES = {
+        KIND_SHORTEQ: KIND_EQUAL,
+        KIND_EMPTY: KIND_EQUAL,
+    }
+
+    re_spec = re.compile(r'^(<|<=||=|==|>=|>|!=|\^|~|~=)(\d.*)$')
+
+    def __init__(self, requirement_string, _warn=True):
+        if _warn:
+            warnings.warn(
+                "The `SpecItem` class will be removed in 3.0.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+        kind, spec = self.parse(requirement_string)
+        self.kind = kind
+        self.spec = spec
+        self._clause = Spec(requirement_string).clause
+
+    @classmethod
+    def parse(cls, requirement_string):
+        if not requirement_string:
+            raise ValueError("Invalid empty requirement specification: %r" % 
requirement_string)
+
+        # Special case: the 'any' version spec.
+        if requirement_string == '*':
+            return (cls.KIND_ANY, '')
+
+        match = cls.re_spec.match(requirement_string)
+        if not match:
+            raise ValueError("Invalid requirement specification: %r" % 
requirement_string)
+
+        kind, version = match.groups()
+        if kind in cls.KIND_ALIASES:
+            kind = cls.KIND_ALIASES[kind]
+
+        spec = Version(version, partial=True)
+        if spec.build is not None and kind not in (cls.KIND_EQUAL, 
cls.KIND_NEQ):
+            raise ValueError(
+                "Invalid requirement specification %r: build numbers have no 
ordering."
+                % requirement_string
+            )
+        return (kind, spec)
+
+    @classmethod
+    def from_matcher(cls, matcher):
+        if matcher == Always():
+            return cls('*', _warn=False)
+        elif matcher == Never():
+            return cls('<0.0.0-', _warn=False)
+        elif isinstance(matcher, Range):
+            return cls('%s%s' % (matcher.operator, matcher.target), 
_warn=False)
+
+    def match(self, version):
+        return self._clause.match(version)
+
+    def __str__(self):
+        return '%s%s' % (self.kind, self.spec)
+
+    def __repr__(self):
+        return '<SpecItem: %s %r>' % (self.kind, self.spec)
+
+    def __eq__(self, other):
+        if not isinstance(other, SpecItem):
+            return NotImplemented
+        return self.kind == other.kind and self.spec == other.spec
+
+    def __hash__(self):
+        return hash((self.kind, self.spec))
+
+
+def compare(v1, v2):
+    return Version(v1).__cmp__(Version(v2))
+
+
+def match(spec, version):
+    return Spec(spec).match(Version(version))
+
+
+def validate(version_string):
+    """Validates a version string againt the SemVer specification."""
+    try:
+        Version.parse(version_string)
+        return True
+    except ValueError:
+        return False
+
+
+DEFAULT_SYNTAX = 'simple'
+
+
+class BaseSpec(object):
+    """A specification of compatible versions.
+
+    Usage:
+    >>> Spec('>=1.0.0', syntax='npm')
+
+    A version matches a specification if it matches any
+    of the clauses of that specification.
+
+    Internally, a Spec is AnyOf(
+        AllOf(Matcher, Matcher, Matcher),
+        AllOf(...),
+    )
+    """
+    SYNTAXES = {}
+
+    @classmethod
+    def register_syntax(cls, subclass):
+        syntax = subclass.SYNTAX
+        if syntax is None:
+            raise ValueError("A Spec needs its SYNTAX field to be set.")
+        elif syntax in cls.SYNTAXES:
+            raise ValueError(
+                "Duplicate syntax for %s: %r, %r"
+                % (syntax, cls.SYNTAXES[syntax], subclass)
+            )
+        cls.SYNTAXES[syntax] = subclass
+        return subclass
+
+    def __init__(self, expression):
+        super(BaseSpec, self).__init__()
+        self.expression = expression
+        self.clause = self._parse_to_clause(expression)
+
+    @classmethod
+    def parse(cls, expression, syntax=DEFAULT_SYNTAX):
+        """Convert a syntax-specific expression into a BaseSpec instance."""
+        return cls.SYNTAXES[syntax](expression)
+
+    @classmethod
+    def _parse_to_clause(cls, expression):
+        """Converts an expression to a clause."""
+        raise NotImplementedError()
+
+    def filter(self, versions):
+        """Filter an iterable of versions satisfying the Spec."""
+        for version in versions:
+            if self.match(version):
+                yield version
+
+    def match(self, version):
+        """Check whether a Version satisfies the Spec."""
+        return self.clause.match(version)
+
+    def select(self, versions):
+        """Select the best compatible version among an iterable of options."""
+        options = list(self.filter(versions))
+        if options:
+            return max(options)
+        return None
+
+    def __contains__(self, version):
+        """Whether `version in self`."""
+        if isinstance(version, Version):
+            return self.match(version)
+        return False
+
+    def __eq__(self, other):
+        if not isinstance(other, self.__class__):
+            return NotImplemented
+
+        return self.clause == other.clause
+
+    def __hash__(self):
+        return hash(self.clause)
+
+    def __str__(self):
+        return self.expression
+
+    def __repr__(self):
+        return '<%s: %r>' % (self.__class__.__name__, self.expression)
+
+
+class Clause(object):
+    __slots__ = []
+
+    def match(self, version):
+        raise NotImplementedError()
+
+    def __and__(self, other):
+        raise NotImplementedError()
+
+    def __or__(self, other):
+        raise NotImplementedError()
+
+    def __eq__(self, other):
+        raise NotImplementedError()
+
+    def prettyprint(self, indent='\t'):
+        """Pretty-print the clause.
+        """
+        return '\n'.join(self._pretty()).replace('\t', indent)
+
+    def _pretty(self):
+        """Actual pretty-printing logic.
+
+        Yields:
+            A list of string. Indentation is performed with \t.
+        """
+        yield repr(self)
+
+    def __ne__(self, other):
+        return not self == other
+
+    def simplify(self):
+        return self
+
+
+class AnyOf(Clause):
+    __slots__ = ['clauses']
+
+    def __init__(self, *clauses):
+        super(AnyOf, self).__init__()
+        self.clauses = frozenset(clauses)
+
+    def match(self, version):
+        return any(c.match(version) for c in self.clauses)
+
+    def simplify(self):
+        subclauses = set()
+        for clause in self.clauses:
+            simplified = clause.simplify()
+            if isinstance(simplified, AnyOf):
+                subclauses |= simplified.clauses
+            elif simplified == Never():
+                continue
+            else:
+                subclauses.add(simplified)
+        if len(subclauses) == 1:
+            return subclauses.pop()
+        return AnyOf(*subclauses)
+
+    def __hash__(self):
+        return hash((AnyOf, self.clauses))
+
+    def __iter__(self):
+        return iter(self.clauses)
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self.clauses == 
other.clauses
+
+    def __and__(self, other):
+        if isinstance(other, AllOf):
+            return other & self
+        elif isinstance(other, Matcher) or isinstance(other, AnyOf):
+            return AllOf(self, other)
+        else:
+            return NotImplemented
+
+    def __or__(self, other):
+        if isinstance(other, AnyOf):
+            clauses = list(self.clauses | other.clauses)
+        elif isinstance(other, Matcher) or isinstance(other, AllOf):
+            clauses = list(self.clauses | set([other]))
+        else:
+            return NotImplemented
+        return AnyOf(*clauses)
+
+    def __repr__(self):
+        return 'AnyOf(%s)' % ', '.join(sorted(repr(c) for c in self.clauses))
+
+    def _pretty(self):
+        yield 'AnyOF('
+        for clause in self.clauses:
+            lines = list(clause._pretty())
+            for line in lines[:-1]:
+                yield '\t' + line
+            yield '\t' + lines[-1] + ','
+        yield ')'
+
+
+class AllOf(Clause):
+    __slots__ = ['clauses']
+
+    def __init__(self, *clauses):
+        super(AllOf, self).__init__()
+        self.clauses = frozenset(clauses)
+
+    def match(self, version):
+        return all(clause.match(version) for clause in self.clauses)
+
+    def simplify(self):
+        subclauses = set()
+        for clause in self.clauses:
+            simplified = clause.simplify()
+            if isinstance(simplified, AllOf):
+                subclauses |= simplified.clauses
+            elif simplified == Always():
+                continue
+            else:
+                subclauses.add(simplified)
+        if len(subclauses) == 1:
+            return subclauses.pop()
+        return AllOf(*subclauses)
+
+    def __hash__(self):
+        return hash((AllOf, self.clauses))
+
+    def __iter__(self):
+        return iter(self.clauses)
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self.clauses == 
other.clauses
+
+    def __and__(self, other):
+        if isinstance(other, Matcher) or isinstance(other, AnyOf):
+            clauses = list(self.clauses | set([other]))
+        elif isinstance(other, AllOf):
+            clauses = list(self.clauses | other.clauses)
+        else:
+            return NotImplemented
+        return AllOf(*clauses)
+
+    def __or__(self, other):
+        if isinstance(other, AnyOf):
+            return other | self
+        elif isinstance(other, Matcher):
+            return AnyOf(self, AllOf(other))
+        elif isinstance(other, AllOf):
+            return AnyOf(self, other)
+        else:
+            return NotImplemented
+
+    def __repr__(self):
+        return 'AllOf(%s)' % ', '.join(sorted(repr(c) for c in self.clauses))
+
+    def _pretty(self):
+        yield 'AllOF('
+        for clause in self.clauses:
+            lines = list(clause._pretty())
+            for line in lines[:-1]:
+                yield '\t' + line
+            yield '\t' + lines[-1] + ','
+        yield ')'
+
+
+class Matcher(Clause):
+    __slots__ = []
+
+    def __and__(self, other):
+        if isinstance(other, AllOf):
+            return other & self
+        elif isinstance(other, Matcher) or isinstance(other, AnyOf):
+            return AllOf(self, other)
+        else:
+            return NotImplemented
+
+    def __or__(self, other):
+        if isinstance(other, AnyOf):
+            return other | self
+        elif isinstance(other, Matcher) or isinstance(other, AllOf):
+            return AnyOf(self, other)
+        else:
+            return NotImplemented
+
+
+class Never(Matcher):
+    __slots__ = []
+
+    def match(self, version):
+        return False
+
+    def __hash__(self):
+        return hash((Never,))
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__)
+
+    def __and__(self, other):
+        return self
+
+    def __or__(self, other):
+        return other
+
+    def __repr__(self):
+        return 'Never()'
+
+
+class Always(Matcher):
+    __slots__ = []
+
+    def match(self, version):
+        return True
+
+    def __hash__(self):
+        return hash((Always,))
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__)
+
+    def __and__(self, other):
+        return other
+
+    def __or__(self, other):
+        return self
+
+    def __repr__(self):
+        return 'Always()'
+
+
+class Range(Matcher):
+    OP_EQ = '=='
+    OP_GT = '>'
+    OP_GTE = '>='
+    OP_LT = '<'
+    OP_LTE = '<='
+    OP_NEQ = '!='
+
+    # <1.2.3 matches 1.2.3-a1
+    PRERELEASE_ALWAYS = 'always'
+    # <1.2.3 does not match 1.2.3-a1
+    PRERELEASE_NATURAL = 'natural'
+    # 1.2.3-a1 is only considered if target == 1.2.3-xxx
+    PRERELEASE_SAMEPATCH = 'same-patch'
+
+    # 1.2.3 matches 1.2.3+*
+    BUILD_IMPLICIT = 'implicit'
+    # 1.2.3 matches only 1.2.3, not 1.2.3+4
+    BUILD_STRICT = 'strict'
+
+    __slots__ = ['operator', 'target', 'prerelease_policy', 'build_policy']
+
+    def __init__(self, operator, target, prerelease_policy=PRERELEASE_NATURAL, 
build_policy=BUILD_IMPLICIT):
+        super(Range, self).__init__()
+        if target.build and operator not in (self.OP_EQ, self.OP_NEQ):
+            raise ValueError(
+                "Invalid range %s%s: build numbers have no ordering."
+                % (operator, target))
+        self.operator = operator
+        self.target = target
+        self.prerelease_policy = prerelease_policy
+        self.build_policy = self.BUILD_STRICT if target.build else build_policy
+
+    def match(self, version):
+        if self.build_policy != self.BUILD_STRICT:
+            version = version.truncate('prerelease')
+
+        if version.prerelease:
+            same_patch = self.target.truncate() == version.truncate()
+
+            if self.prerelease_policy == self.PRERELEASE_SAMEPATCH and not 
same_patch:
+                return False
+
+        if self.operator == self.OP_EQ:
+            if self.build_policy == self.BUILD_STRICT:
+                return (
+                    self.target.truncate('prerelease') == 
version.truncate('prerelease')
+                    and version.build == self.target.build
+                )
+            return version == self.target
+        elif self.operator == self.OP_GT:
+            return version > self.target
+        elif self.operator == self.OP_GTE:
+            return version >= self.target
+        elif self.operator == self.OP_LT:
+            if (
+                version.prerelease
+                and self.prerelease_policy == self.PRERELEASE_NATURAL
+                and version.truncate() == self.target.truncate()
+                and not self.target.prerelease
+            ):
+                return False
+            return version < self.target
+        elif self.operator == self.OP_LTE:
+            return version <= self.target
+        else:
+            assert self.operator == self.OP_NEQ
+            if self.build_policy == self.BUILD_STRICT:
+                return not (
+                    self.target.truncate('prerelease') == 
version.truncate('prerelease')
+                    and version.build == self.target.build
+                )
+
+            if (
+                version.prerelease
+                and self.prerelease_policy == self.PRERELEASE_NATURAL
+                and version.truncate() == self.target.truncate()
+                and not self.target.prerelease
+            ):
+                return False
+            return version != self.target
+
+    def __hash__(self):
+        return hash((Range, self.operator, self.target, 
self.prerelease_policy))
+
+    def __eq__(self, other):
+        return (
+            isinstance(other, self.__class__)
+            and self.operator == other.operator
+            and self.target == other.target
+            and self.prerelease_policy == other.prerelease_policy
+        )
+
+    def __str__(self):
+        return '%s%s' % (self.operator, self.target)
+
+    def __repr__(self):
+        policy_part = (
+            '' if self.prerelease_policy == self.PRERELEASE_NATURAL
+            else ', prerelease_policy=%r' % self.prerelease_policy
+        ) + (
+            '' if self.build_policy == self.BUILD_IMPLICIT
+            else ', build_policy=%r' % self.build_policy
+        )
+        return 'Range(%r, %r%s)' % (
+            self.operator,
+            self.target,
+            policy_part,
+        )
+
+
+@BaseSpec.register_syntax
+class SimpleSpec(BaseSpec):
+
+    SYNTAX = 'simple'
+
+    @classmethod
+    def _parse_to_clause(cls, expression):
+        return cls.Parser.parse(expression)
+
+    class Parser:
+        NUMBER = r'\*|0|[1-9][0-9]*'
+        NAIVE_SPEC = re.compile(r"""^
+            (?P<op><|<=||=|==|>=|>|!=|\^|~|~=)
+            (?P<major>{nb})(?:\.(?P<minor>{nb})(?:\.(?P<patch>{nb}))?)?
+            (?:-(?P<prerel>[a-z0-9A-Z.-]*))?
+            (?:\+(?P<build>[a-z0-9A-Z.-]*))?
+            $
+            """.format(nb=NUMBER),
+            re.VERBOSE,
+        )
+
+        @classmethod
+        def parse(cls, expression):
+            blocks = expression.split(',')
+            clause = Always()
+            for block in blocks:
+                if not cls.NAIVE_SPEC.match(block):
+                    raise ValueError("Invalid simple block %r" % block)
+                clause &= cls.parse_block(block)
+
+            return clause
+
+        PREFIX_CARET = '^'
+        PREFIX_TILDE = '~'
+        PREFIX_COMPATIBLE = '~='
+        PREFIX_EQ = '=='
+        PREFIX_NEQ = '!='
+        PREFIX_GT = '>'
+        PREFIX_GTE = '>='
+        PREFIX_LT = '<'
+        PREFIX_LTE = '<='
+
+        PREFIX_ALIASES = {
+            '=': PREFIX_EQ,
+            '': PREFIX_EQ,
+        }
+
+        EMPTY_VALUES = ['*', 'x', 'X', None]
+
+        @classmethod
+        def parse_block(cls, expr):
+            if not cls.NAIVE_SPEC.match(expr):
+                raise ValueError("Invalid simple spec component: %r" % expr)
+            prefix, major_t, minor_t, patch_t, prerel, build = 
cls.NAIVE_SPEC.match(expr).groups()
+            prefix = cls.PREFIX_ALIASES.get(prefix, prefix)
+
+            major = None if major_t in cls.EMPTY_VALUES else int(major_t)
+            minor = None if minor_t in cls.EMPTY_VALUES else int(minor_t)
+            patch = None if patch_t in cls.EMPTY_VALUES else int(patch_t)
+
+            if major is None:  # '*'
+                target = Version(major=0, minor=0, patch=0)
+                if prefix not in (cls.PREFIX_EQ, cls.PREFIX_GTE):
+                    raise ValueError("Invalid simple spec: %r" % expr)
+            elif minor is None:
+                target = Version(major=major, minor=0, patch=0)
+            elif patch is None:
+                target = Version(major=major, minor=minor, patch=0)
+            else:
+                target = Version(
+                    major=major,
+                    minor=minor,
+                    patch=patch,
+                    prerelease=prerel.split('.') if prerel else (),
+                    build=build.split('.') if build else (),
+                )
+
+            if (major is None or minor is None or patch is None) and (prerel 
or build):
+                raise ValueError("Invalid simple spec: %r" % expr)
+
+            if build is not None and prefix not in (cls.PREFIX_EQ, 
cls.PREFIX_NEQ):
+                raise ValueError("Invalid simple spec: %r" % expr)
+
+            if prefix == cls.PREFIX_CARET:
+                # Accept anything with the same most-significant digit
+                if target.major:
+                    high = target.next_major()
+                elif target.minor:
+                    high = target.next_minor()
+                else:
+                    high = target.next_patch()
+                return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high)
+
+            elif prefix == cls.PREFIX_TILDE:
+                assert major is not None
+                # Accept any higher patch in the same minor
+                # Might go higher if the initial version was a partial
+                if minor is None:
+                    high = target.next_major()
+                else:
+                    high = target.next_minor()
+                return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high)
+
+            elif prefix == cls.PREFIX_COMPATIBLE:
+                assert major is not None
+                # ~1 is 1.0.0..2.0.0; ~=2.2 is 2.2.0..3.0.0; ~=1.4.5 is 
1.4.5..1.5.0
+                if minor is None or patch is None:
+                    # We got a partial version
+                    high = target.next_major()
+                else:
+                    high = target.next_minor()
+                return Range(Range.OP_GTE, target) & Range(Range.OP_LT, high)
+
+            elif prefix == cls.PREFIX_EQ:
+                if major is None:
+                    return Range(Range.OP_GTE, target)
+                elif minor is None:
+                    return Range(Range.OP_GTE, target) & Range(Range.OP_LT, 
target.next_major())
+                elif patch is None:
+                    return Range(Range.OP_GTE, target) & Range(Range.OP_LT, 
target.next_patch())
+                elif build == '':
+                    return Range(Range.OP_EQ, target, 
build_policy=Range.BUILD_STRICT)
+                else:
+                    return Range(Range.OP_EQ, target)
+
+            elif prefix == cls.PREFIX_NEQ:
+                assert major is not None
+                if minor is None:
+                    # !=1.x => <1.0.0 || >=2.0.0
+                    return Range(Range.OP_LT, target) | Range(Range.OP_GTE, 
target.next_major())
+                elif patch is None:
+                    # !=1.2.x => <1.2.0 || >=1.3.0
+                    return Range(Range.OP_LT, target) | Range(Range.OP_GTE, 
target.next_minor())
+                elif prerel == '':
+                    # !=1.2.3-
+                    return Range(Range.OP_NEQ, target, 
prerelease_policy=Range.PRERELEASE_ALWAYS)
+                elif build == '':
+                    # !=1.2.3+ or !=1.2.3-a2+
+                    return Range(Range.OP_NEQ, target, 
build_policy=Range.BUILD_STRICT)
+                else:
+                    return Range(Range.OP_NEQ, target)
+
+            elif prefix == cls.PREFIX_GT:
+                assert major is not None
+                if minor is None:
+                    # >1.x => >=2.0
+                    return Range(Range.OP_GTE, target.next_major())
+                elif patch is None:
+                    return Range(Range.OP_GTE, target.next_minor())
+                else:
+                    return Range(Range.OP_GT, target)
+
+            elif prefix == cls.PREFIX_GTE:
+                return Range(Range.OP_GTE, target)
+
+            elif prefix == cls.PREFIX_LT:
+                assert major is not None
+                if prerel == '':
+                    # <1.2.3-
+                    return Range(Range.OP_LT, target, 
prerelease_policy=Range.PRERELEASE_ALWAYS)
+                return Range(Range.OP_LT, target)
+
+            else:
+                assert prefix == cls.PREFIX_LTE
+                assert major is not None
+                if minor is None:
+                    # <=1.x => <2.0
+                    return Range(Range.OP_LT, target.next_major())
+                elif patch is None:
+                    return Range(Range.OP_LT, target.next_minor())
+                else:
+                    return Range(Range.OP_LTE, target)
+
+
+class LegacySpec(SimpleSpec):
+    def __init__(self, *expressions):
+        warnings.warn(
+            "The Spec() class will be removed in 3.1; use SimpleSpec() 
instead.",
+            PendingDeprecationWarning,
+            stacklevel=2,
+        )
+
+        if len(expressions) > 1:
+            warnings.warn(
+                "Passing 2+ arguments to SimpleSpec will be removed in 3.0; 
concatenate them with ',' instead.",
+                DeprecationWarning,
+                stacklevel=2,
+            )
+        expression = ','.join(expressions)
+        super(LegacySpec, self).__init__(expression)
+
+    @property
+    def specs(self):
+        return list(self)
+
+    def __iter__(self):
+        warnings.warn(
+            "Iterating over the components of a SimpleSpec object will be 
removed in 3.0.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        try:
+            clauses = list(self.clause)
+        except TypeError:  # Not an iterable
+            clauses = [self.clause]
+        for clause in clauses:
+            yield SpecItem.from_matcher(clause)
+
+
+Spec = LegacySpec
+
+
+@BaseSpec.register_syntax
+class NpmSpec(BaseSpec):
+    SYNTAX = 'npm'
+
+    @classmethod
+    def _parse_to_clause(cls, expression):
+        return cls.Parser.parse(expression)
+
+    class Parser:
+        JOINER = '||'
+        HYPHEN = ' - '
+
+        NUMBER = r'x|X|\*|0|[1-9][0-9]*'
+        PART = r'[a-zA-Z0-9.-]*'
+        NPM_SPEC_BLOCK = re.compile(r"""
+            ^(?:v)?                     # Strip optional initial v
+            (?P<op><|<=|>=|>|=|\^|~|)   # Operator, can be empty
+            (?P<major>{nb})(?:\.(?P<minor>{nb})(?:\.(?P<patch>{nb}))?)?
+            (?:-(?P<prerel>{part}))?    # Optional re-release
+            (?:\+(?P<build>{part}))?    # Optional build
+            $""".format(nb=NUMBER, part=PART),
+            re.VERBOSE,
+        )
+
+        @classmethod
+        def range(cls, operator, target):
+            return Range(operator, target, 
prerelease_policy=Range.PRERELEASE_SAMEPATCH)
+
+        @classmethod
+        def parse(cls, expression):
+            result = Never()
+            groups = expression.split(cls.JOINER)
+            for group in groups:
+                group = group.strip()
+                if not group:
+                    group = '>=0.0.0'
+
+                subclauses = []
+                if cls.HYPHEN in group:
+                    low, high = group.split(cls.HYPHEN, 2)
+                    subclauses = cls.parse_simple('>=' + low) + 
cls.parse_simple('<=' + high)
+
+                else:
+                    blocks = group.split(' ')
+                    for block in blocks:
+                        if not cls.NPM_SPEC_BLOCK.match(block):
+                            raise ValueError("Invalid NPM block in %r: %r" % 
(expression, block))
+
+                        subclauses.extend(cls.parse_simple(block))
+
+                prerelease_clauses = []
+                non_prerel_clauses = []
+                for clause in subclauses:
+                    if clause.target.prerelease:
+                        if clause.operator in (Range.OP_GT, Range.OP_GTE):
+                            prerelease_clauses.append(Range(
+                                operator=Range.OP_LT,
+                                target=Version(
+                                    major=clause.target.major,
+                                    minor=clause.target.minor,
+                                    patch=clause.target.patch + 1,
+                                ),
+                                prerelease_policy=Range.PRERELEASE_ALWAYS,
+                            ))
+                        elif clause.operator in (Range.OP_LT, Range.OP_LTE):
+                            prerelease_clauses.append(Range(
+                                operator=Range.OP_GTE,
+                                target=Version(
+                                    major=clause.target.major,
+                                    minor=clause.target.minor,
+                                    patch=0,
+                                    prerelease=(),
+                                ),
+                                prerelease_policy=Range.PRERELEASE_ALWAYS,
+                            ))
+                        prerelease_clauses.append(clause)
+                        non_prerel_clauses.append(cls.range(
+                            operator=clause.operator,
+                            target=clause.target.truncate(),
+                        ))
+                    else:
+                        non_prerel_clauses.append(clause)
+                if prerelease_clauses:
+                    result |= AllOf(*prerelease_clauses)
+                result |= AllOf(*non_prerel_clauses)
+
+            return result
+
+        PREFIX_CARET = '^'
+        PREFIX_TILDE = '~'
+        PREFIX_EQ = '='
+        PREFIX_GT = '>'
+        PREFIX_GTE = '>='
+        PREFIX_LT = '<'
+        PREFIX_LTE = '<='
+
+        PREFIX_ALIASES = {
+            '': PREFIX_EQ,
+        }
+
+        PREFIX_TO_OPERATOR = {
+            PREFIX_EQ: Range.OP_EQ,
+            PREFIX_LT: Range.OP_LT,
+            PREFIX_LTE: Range.OP_LTE,
+            PREFIX_GTE: Range.OP_GTE,
+            PREFIX_GT: Range.OP_GT,
+        }
+
+        EMPTY_VALUES = ['*', 'x', 'X', None]
+
+        @classmethod
+        def parse_simple(cls, simple):
+            match = cls.NPM_SPEC_BLOCK.match(simple)
+
+            prefix, major_t, minor_t, patch_t, prerel, build = match.groups()
+
+            prefix = cls.PREFIX_ALIASES.get(prefix, prefix)
+            major = None if major_t in cls.EMPTY_VALUES else int(major_t)
+            minor = None if minor_t in cls.EMPTY_VALUES else int(minor_t)
+            patch = None if patch_t in cls.EMPTY_VALUES else int(patch_t)
+
+            if build is not None and prefix not in [cls.PREFIX_EQ]:
+                # Ignore the 'build' part when not comparing to a specific 
part.
+                build = None
+
+            if major is None:  # '*', 'x', 'X'
+                target = Version(major=0, minor=0, patch=0)
+                if prefix not in [cls.PREFIX_EQ, cls.PREFIX_GTE]:
+                    raise ValueError("Invalid expression %r" % simple)
+                prefix = cls.PREFIX_GTE
+            elif minor is None:
+                target = Version(major=major, minor=0, patch=0)
+            elif patch is None:
+                target = Version(major=major, minor=minor, patch=0)
+            else:
+                target = Version(
+                    major=major,
+                    minor=minor,
+                    patch=patch,
+                    prerelease=prerel.split('.') if prerel else (),
+                    build=build.split('.') if build else (),
+                )
+
+            if (major is None or minor is None or patch is None) and (prerel 
or build):
+                raise ValueError("Invalid NPM spec: %r" % simple)
+
+            if prefix == cls.PREFIX_CARET:
+                if target.major:  # ^1.2.4 => >=1.2.4 <2.0.0 ; ^1.x => >=1.0.0 
<2.0.0
+                    high = target.truncate().next_major()
+                elif target.minor:  # ^0.1.2 => >=0.1.2 <0.2.0
+                    high = target.truncate().next_minor()
+                elif minor is None:  # ^0.x => >=0.0.0 <1.0.0
+                    high = target.truncate().next_major()
+                elif patch is None:  # ^0.2.x => >=0.2.0 <0.3.0
+                    high = target.truncate().next_minor()
+                else:  # ^0.0.1 => >=0.0.1 <0.0.2
+                    high = target.truncate().next_patch()
+                return [cls.range(Range.OP_GTE, target), 
cls.range(Range.OP_LT, high)]
+
+            elif prefix == cls.PREFIX_TILDE:
+                assert major is not None
+                if minor is None:  # ~1.x => >=1.0.0 <2.0.0
+                    high = target.next_major()
+                else:  # ~1.2.x => >=1.2.0 <1.3.0; ~1.2.3 => >=1.2.3 <1.3.0
+                    high = target.next_minor()
+                return [cls.range(Range.OP_GTE, target), 
cls.range(Range.OP_LT, high)]
+
+            elif prefix == cls.PREFIX_EQ:
+                if major is None:
+                    return [cls.range(Range.OP_GTE, target)]
+                elif minor is None:
+                    return [cls.range(Range.OP_GTE, target), 
cls.range(Range.OP_LT, target.next_major())]
+                elif patch is None:
+                    return [cls.range(Range.OP_GTE, target), 
cls.range(Range.OP_LT, target.next_minor())]
+                else:
+                    return [cls.range(Range.OP_EQ, target)]
+
+            elif prefix == cls.PREFIX_GT:
+                assert major is not None
+                if minor is None:  # >1.x
+                    return [cls.range(Range.OP_GTE, target.next_major())]
+                elif patch is None:  # >1.2.x => >=1.3.0
+                    return [cls.range(Range.OP_GTE, target.next_minor())]
+                else:
+                    return [cls.range(Range.OP_GT, target)]
+
+            elif prefix == cls.PREFIX_GTE:
+                return [cls.range(Range.OP_GTE, target)]
+
+            elif prefix == cls.PREFIX_LT:
+                assert major is not None
+                return [cls.range(Range.OP_LT, target)]
+
+            else:
+                assert prefix == cls.PREFIX_LTE
+                assert major is not None
+                if minor is None:  # <=1.x => <2.0.0
+                    return [cls.range(Range.OP_LT, target.next_major())]
+                elif patch is None:  # <=1.2.x => <1.3.0
+                    return [cls.range(Range.OP_LT, target.next_minor())]
+                else:
+                    return [cls.range(Range.OP_LTE, target)]
diff --git a/talerbuildconfig.py b/talerbuildconfig.py
index e243a19..f28e1c2 100644
--- a/talerbuildconfig.py
+++ b/talerbuildconfig.py
@@ -35,6 +35,7 @@ import logging
 from distutils.spawn import find_executable
 import subprocess
 from dataclasses import dataclass
+import semver
 
 """
 This module aims to replicate a small GNU Coding Standards
@@ -50,6 +51,9 @@ Makefile fragement, which is the processed by a Makefile 
(usually) in
 GNU Make format.
 """
 
+# Should be incremented each time we add some functionality
+serialversion = 2
+
 
 # TODO: We need a smallest version argument.
 
@@ -116,9 +120,17 @@ class BuildConfig:
         for tool in self.tools:
             res = tool.check(self)
             if not res:
-                print(f"Error: tool {tool.name} not available")
+                print(f"Error: tool '{tool.name}' not available")
                 if hasattr(tool, "hint"):
                     print(f"Hint: {tool.hint}")
+                sys.exit(1)
+            if hasattr(tool, "version_spec"):
+                sv = semver.SimpleSpec(tool.version_spec)
+                path, version = self.tool_results[tool.name]
+                if not sv.match(semver.Version(version)):
+                    print(f"Error: Tool '{tool.name}' has version '{version}', 
but we require '{tool.version_spec}'")
+                    sys.exit(1)
+
 
         for tool in self.tools:
             path, version = self.tool_results[tool.name]
@@ -413,6 +425,9 @@ class NodeJsTool(Tool):
     name = "node"
     hint = "If you are using Ubuntu Linux or Debian Linux, try installing 
the\nnode-legacy package or symlink node to nodejs."
 
+    def __init__(self, version_spec):
+        self.version_spec = version_spec
+
     def args(self, parser):
         pass
 
@@ -427,7 +442,7 @@ class NodeJsTool(Tool):
         ):
             buildconfig._warn("your node version is too old, use Node 4.x or 
newer")
             return False
-        node_version = tool_version("node --version")
+        node_version = tool_version("node --version").lstrip("v")
         buildconfig._set_tool("node", "node", version=node_version)
         return True
 
diff --git a/testconfigure.py b/testconfigure.py
index 29d82a1..3db0aa4 100644
--- a/testconfigure.py
+++ b/testconfigure.py
@@ -6,7 +6,7 @@ b.enable_configmk()
 b.add_tool(YarnTool())
 b.add_tool(BrowserTool())
 b.add_tool(PyBabelTool())
-b.add_tool(NodeJsTool())
+b.add_tool(NodeJsTool(version_spec=">=12.0.0"))
 b.add_tool(PythonTool())
 b.add_tool(PosixTool("find"))
 b.add_tool(PosixTool("xargs"))

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]