"""pysemver: Semantic Version comparing for Python.

Provides comparing of semantic versions by using SemVer objects using rich comperations plus the
possibility to match a selector string against versions. Interesting for version dependencies.
Versions look like: "1.7.12+b.133"
Selectors look like: ">1.7.0 || 1.6.9+b.111 - 1.6.9+b.113"

Example usages:
    >>> SemVer(1, 2, 3, build=13)
    SemVer("1.2.3+13")
    >>> SemVer.valid("1.2.3.4")
    False
    >>> SemVer.clean("this is unimportant text 1.2.3-2 and will be stripped")
    "1.2.3-2"
    >>> SemVer("1.7.12+b.133").satisfies(">1.7.0 || 1.6.9+b.111 - 1.6.9+b.113")
    True
    >>> SemSel(">1.7.0 || 1.6.9+b.111 - 1.6.9+b.113").matches(SemVer("1.7.12+b.133"),
    ... SemVer("1.6.9+b.112"), SemVer("1.6.10"))
    [SemVer("1.7.12+b.133"), SemVer("1.6.9+b.112")]
    >>> min(_)
    SemVer("1.6.9+b.112")
    >>> _.patch
    9

Exported classes:
    * SemVer(collections.namedtuple())
        Parses semantic versions and defines methods for them. Supports rich comparisons.
    * SemSel(tuple)
        Parses semantic version selector strings and defines methods for them.
    * SelParseError(Exception)
        An error among others raised when parsing a semantic version selector failed.

Other classes:
    * SemComparator(object)
    * SemSelAndChunk(list)
    * SemSelOrChunk(list)

Functions/Variables/Constants:
    none


Copyright (c) 2013 Zachary King, FichteFoll

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 re
import sys
from collections import namedtuple  # Python >=2.6


__all__ = ('SemVer', 'SemSel', 'SelParseError')


if sys.version_info[0] == 3:
    basestring = str

    def cmp(a, b):
        return (a > b) - (a < b)


# @functools.total_ordering would be nice here but was added in 2.7, __cmp__ is not Py3
class SemVer(namedtuple("_SemVer", 'major, minor, patch, prerelease, build')):

    """Semantic Version, consists of 3 to 5 components defining the version's adicity.

    See http://semver.org/ (2.0.0-rc.1) for the standard mainly used for this implementation, few
    changes have been made.

    Information on this particular class and their instances:
        - Immutable and hashable.
        - Subclasses `collections.namedtuple`.
        - Always `True` in boolean context.
        - len() returns an int between 3 and 5; 4 when a pre-release is set and 5 when a build is
          set. Note: Still returns 5 when build is set but not pre-release.
        - Parts of the semantic version can be accessed by integer indexing, key (string) indexing,
          slicing and getting an attribute. Returned slices are tuple. Leading '-' and '+' of
          optional components are not stripped. Supported keys/attributes:
          major, minor, patch, prerelease, build.

          Examples:
            s = SemVer("1.2.3-4.5+6")
            s[2] == 3
            s[:3] == (1, 2, 3)
            s['build'] == '-4.5'
            s.major == 1

    Short information on semantic version structure:

    Semantic versions consist of:
        * a major component (numeric)
        * a minor component (numeric)
        * a patch component (numeric)
        * a pre-release component [optional]
        * a build component [optional]

    The pre-release component is indicated by a hyphen '-' and followed by alphanumeric[1] sequences
    separated by dots '.'. Sequences are compared numerically if applicable (both sequences of two
    versions are numeric) or lexicographically. May also include hyphens. The existence of a
    pre-release component lowers the actual version; the shorter pre-release component is considered
    lower. An 'empty' pre-release component is considered to be the least version for this
    major-minor-patch combination (e.g. "1.0.0-").

    The build component may follow the optional pre-release component and is indicated by a plus '+'
    followed by sequences, just as the pre-release component. Comparing works similarly. However the
    existence of a build component raises the actual version and may also raise a pre-release. An
    'empty' build component is considered to be the highest version for this
    major-minor-patch-prerelease combination (e.g. "1.2.3+").


    [1]: Regexp for a sequence: r'[0-9A-Za-z-]+'.
    """

    # Static class variables
    _base_regex = r'''(?x)
        (?P<major>[0-9]+)
        \.(?P<minor>[0-9]+)
        \.(?P<patch>[0-9]+)
        (?:\-(?P<prerelease>(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?))?
        (?:\+(?P<build>(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?))?'''
    _search_regex = re.compile(_base_regex)
    _match_regex = re.compile('^%s$' % _base_regex)  # required because of $ anchor

    # "Constructor"
    def __new__(cls, *args, **kwargs):
        """There are two different constructor styles that are allowed:
        - Option 1 allows specification of a semantic version as a string and the option to "clean"
          the string before parsing it.
        - Option 2 allows specification of each component separately as one parameter.

        Note that all the parameters specified in the following sections can be passed either as
        positional or as named parameters while considering the usual Python rules for this. As
        such, `SemVer(1, 2, minor=1)` will result in an exception and not in `SemVer("1.1.2")`.

        Option 1:
            Constructor examples:
                SemVer("1.0.1")
                SemVer("this version 1.0.1-pre.1 here", True)
                SemVer(ver="0.0.9-pre-alpha+34", clean=False)

            Parameters:
                * ver (str)
                    The string containing the version.
                * clean = `False` (bool; optional)
                    If this is true in boolean context, `SemVer.clean(ver)` is called before
                    parsing.

        Option 2:
            Constructor examples:
                SemVer(1, 0, 1)
                SemVer(1, '0', prerelease='pre-alpha', patch=1, build=34)
                SemVer(**dict(minor=2, major=1, patch=3))

            Parameters:
                * major (int, str, float ...)
                * minor (...)
                * patch (...)
                    Major to patch components must be an integer or convertable to an int (e.g. a
                    string or another number type).

                * prerelease = `None` (str, int, float ...; optional)
                * build = `None` (...; optional)
                    Pre-release and build components should be a string (or number) type.
                    Will be passed to `str()` if not already a string but the final string must
                    match '^[0-9A-Za-z.-]*$'

        Raises:
            * TypeError
                Invalid parameter type(s) or combination (e.g. option 1 and 2).
            * ValueError
                Invalid semantic version or option 2 parameters unconvertable.
        """
        ver, clean, comps = None, False, None
        kw, length = kwargs.copy(), len(args) + len(kwargs)

        def inv():
            raise TypeError("Invalid parameter combination: args=%s; kwargs=%s" % (args, kwargs))

        # Do validation and parse the parameters
        if length == 0 or length > 5:
            raise TypeError("SemVer accepts at least 1 and at most 5 arguments (%d given)" % length)

        elif length < 3:
            if len(args) == 2:
                ver, clean = args
            else:
                ver = args[0] if args else kw.pop('ver', None)
                clean = kw.pop('clean', clean)
                if kw:
                    inv()

        else:
            comps = list(args) + [kw.pop(cls._fields[k], None) for k in range(len(args), 5)]
            if kw or any(comps[i] is None for i in range(3)):
                inv()

            typecheck = (int,) * 3 + (basestring,) * 2
            for i, (v, t) in enumerate(zip(comps, typecheck)):
                if v is None:
                    continue
                elif not isinstance(v, t):
                    try:
                        if i < 3:
                            v = typecheck[i](v)
                        else:  # The real `basestring` can not be instatiated (Py2)
                            v = str(v)
                    except ValueError as e:
                        # Modify the exception message. I can't believe this actually works
                        e.args = ("Parameter #%d must be of type %s or convertable"
                                  % (i, t.__name__),)
                        raise
                    else:
                        comps[i] = v
                if t is basestring and not re.match(r"^[0-9A-Za-z.-]*$", v):
                    raise ValueError("Build and pre-release strings must match '^[0-9A-Za-z.-]*$'")

        # Final adjustments
        if not comps:
            if ver is None or clean is None:
                inv()
            ver = clean and cls.clean(ver) or ver
            comps = cls._parse(ver)

        # Create the obj
        return super(SemVer, cls).__new__(cls, *comps)

    # Magic methods
    def __str__(self):
        return ('.'.join(map(str, self[:3]))
                + ('-' + self.prerelease if self.prerelease is not None else '')
                + ('+' + self.build if self.build is not None else ''))

    def __repr__(self):
        # Use the shortest representation - what would you prefer?
        return 'SemVer("%s")' % str(self)
        # return 'SemVer(%s)' % ', '.join('%s=%r' % (k, getattr(self, k)) for k in self._fields)

    def __len__(self):
        return 3 + (self.build is not None and 2 or self.prerelease is not None)

    # Magic rich comparing methods
    def __gt__(self, other):
        return self._compare(other) == 1 if isinstance(other, SemVer) else NotImplemented

    def __eq__(self, other):
        return self._compare(other) == 0 if isinstance(other, SemVer) else NotImplemented

    def __lt__(self, other):
        return not (self > other or self == other)

    def __ge__(self, other):
        return not (self < other)

    def __le__(self, other):
        return not (self > other)

    def __ne__(self, other):
        return not (self == other)

    # Utility (class-)methods
    def satisfies(self, sel):
        """Alias for `bool(sel.matches(self))` or `bool(SemSel(sel).matches(self))`.

        See `SemSel.__init__()` and `SemSel.matches(*vers)` for possible exceptions.

        Returns:
            * bool: `True` if the version matches the passed selector, `False` otherwise.
        """
        if not isinstance(sel, SemSel):
            sel = SemSel(sel)  # just "re-raise" exceptions

        return bool(sel.matches(self))

    @classmethod
    def valid(cls, ver):
        """Check if `ver` is a valid semantic version. Classmethod.

        Parameters:
            * ver (str)
                The string that should be stripped.

        Raises:
            * TypeError
                Invalid parameter type.

        Returns:
            * bool: `True` if it is valid, `False` otherwise.
        """
        if not isinstance(ver, basestring):
            raise TypeError("%r is not a string" % ver)

        if cls._match_regex.match(ver):
            return True
        else:
            return False

    @classmethod
    def clean(cls, vers):
        """Remove everything before and after a valid version string. Classmethod.

        Parameters:
            * vers (str)
                The string that should be stripped.

        Raises:
            * TypeError
                Invalid parameter type.

        Returns:
            * str:  The stripped version string. Only the first version is matched.
            * None: No version found in the string.
        """
        if not isinstance(vers, basestring):
            raise TypeError("%r is not a string" % vers)
        m = cls._search_regex.search(vers)
        if m:
            return vers[m.start():m.end()]
        else:
            return None

    # Private (class-)methods
    @classmethod
    def _parse(cls, ver):
        """Private. Do not touch. Classmethod.
        """
        if not isinstance(ver, basestring):
            raise TypeError("%r is not a string" % ver)

        match = cls._match_regex.match(ver)

        if match is None:
            raise ValueError("'%s' is not a valid SemVer string" % ver)

        g = list(match.groups())
        for i in range(3):
            g[i] = int(g[i])

        return g  # Will be passed as namedtuple(...)(*g)

    def _compare(self, other):
        """Private. Do not touch.
        self > other: 1
        self = other: 0
        self < other: -1
        """
        # Shorthand lambdas
        def cp_len(t, i=0):
            return cmp(len(t[i]), len(t[not i]))

        for i, (x1, x2) in enumerate(zip(self, other)):
            if i > 2:
                if x1 is None and x2 is None:
                    continue

                # self is greater when other has a prerelease but self doesn't
                # self is less    when other has a build      but self doesn't
                if x1 is None or x2 is None:
                    return int(2 * (i - 3.5)) * (1 - 2 * (x1 is None))

                # self is less when other's build is empty
                if i == 4 and (not x1 or not x2) and x1 != x2:
                    return 1 - 2 * bool(x1)

                # Split by '.' and use numeric comp or lexicographical order
                t2 = [x1.split('.'), x2.split('.')]
                for y1, y2 in zip(*t2):
                    if y1.isdigit() and y2.isdigit():
                        y1 = int(y1)
                        y2 = int(y2)
                    if y1 > y2:
                        return 1
                    elif y1 < y2:
                        return -1

                # The "longer" sub-version is greater
                d = cp_len(t2)
                if d:
                    return d
            else:
                if x1 > x2:
                    return 1
                elif x1 < x2:
                    return -1

        # The versions equal
        return 0


class SemComparator(object):

    """Holds a SemVer object and a comparing operator and can match these against a given version.

    Constructor: SemComparator('<=', SemVer("1.2.3"))

    Methods:
        * matches(ver)
    """
    # Private properties
    _ops = {
        '>=': '__ge__',
        '<=': '__le__',
        '>':  '__gt__',
        '<':  '__lt__',
        '=':  '__eq__',
        '!=': '__ne__'
    }
    _ops_satisfy = ('~', '!')

    # Constructor
    def __init__(self, op, ver):
        """Constructor examples:
        SemComparator('<=', SemVer("1.2.3"))
        SemComparator('!=', SemVer("2.3.4"))

        Parameters:
            * op (str, False, None)
                One of [>=, <=, >, <, =, !=, !, ~] or evaluates to `False` which defaults to '~'.
                '~' means a "satisfy" operation where pre-releases and builds are ignored.
                '!' is a negative "~".
            * ver (SemVer)
                Holds the version to compare with.

        Raises:
            * ValueError
                Invalid `op` parameter.
            * TypeError
                Invalid `ver` parameter.
        """
        super(SemComparator, self).__init__()

        if op and op not in self._ops_satisfy and op not in self._ops:
            raise ValueError("Invalid value for `op` parameter.")
        if not isinstance(ver, SemVer):
            raise TypeError("`ver` parameter is not instance of SemVer.")

        # Default to '~' for versions with no build or pre-release
        op = op or '~'
        # Fallback to '=' and '!=' if len > 3
        if len(ver) != 3:
            if op == '~':
                op = '='
            if op == '!':
                op = '!='

        self.op = op
        self.ver = ver

    # Magic methods
    def __str__(self):
        return (self.op or "") + str(self.ver)

    # Utility methods
    def matches(self, ver):
        """Match the internal version (constructor) against `ver`.

        Parameters:
            * ver (SemVer)

        Raises:
            * TypeError
                Could not compare `ver` against the version passed in the constructor with the
                passed operator.

        Returns:
            * bool
                `True` if the version matched the specified operator and internal version, `False`
                otherwise.
        """
        if self.op in self._ops_satisfy:
            # Compare only the first three parts (which are tuples) and directly
            return bool((self.ver[:3] == ver[:3]) + (self.op == '!') * -1)
        ret = getattr(ver, self._ops[self.op])(self.ver)
        if ret == NotImplemented:
            raise TypeError("Unable to compare %r with operator '%s'" % (ver, self.op))
        return ret


class SemSelAndChunk(list):

    """Extends list and defines a few methods used for matching versions.

    New elements should be added by calling `.add_child(op, ver)` which creates a SemComparator
    instance and adds that to itself.

    Methods:
        * matches(ver)
        * add_child(op, ver)
    """

    # Magic methods

    def __str__(self):
        return ' '.join(map(str, self))

    # Utitlity methods

    def matches(self, ver):
        """Match all of the added children against `ver`.

        Parameters:
            * ver (SemVer)

        Raises:
            * TypeError
                Invalid `ver` parameter.

        Returns:
            * bool:
                `True` if *all* of the SemComparator children match `ver`, `False` otherwise.
        """
        if not isinstance(ver, SemVer):
            raise TypeError("`ver` parameter is not instance of SemVer.")
        return all(cp.matches(ver) for cp in self)

    def add_child(self, op, ver):
        """Create a SemComparator instance with the given parameters and appends that to self.

        Parameters:
            * op (str)
            * ver (SemVer)
        Both parameters are forwarded to `SemComparator.__init__`, see there for a more detailed
        description.

        Raises:
            Exceptions raised by `SemComparator.__init__`.
        """
        self.append(SemComparator(op, SemVer(ver)))


class SemSelOrChunk(list):

    """Extends list and defines a few methods used for matching versions.

    New elements should be added by calling `.new_child()` which returns a SemSelAndChunk
    instance.

    Methods:
        * matches(ver)
        * new_child()
    """

    # Magic methods

    def __str__(self):
        return ' || '.join(map(str, self))

    # Utility methods

    def matches(self, ver):
        """Match all of the added children against `ver`.

        Parameters:
            * ver (SemVer)

        Raises:
            * TypeError
                Invalid `ver` parameter.

        Returns:
            * bool
                `True` if *any* of the SemSelAndChunk children matches `ver`.
                `False` otherwise.
        """
        if not isinstance(ver, SemVer):
            raise TypeError("`ver` parameter is not instance of SemVer.")
        return any(ch.matches(ver) for ch in self)

    def new_child(self):
        """Creates a new SemSelAndChunk instance, appends it to self and returns it.

        Returns:
            * SemSelAndChunk: An empty instance.
        """
        ch = SemSelAndChunk()
        self.append(ch)
        return ch


class SelParseError(Exception):

    """An Exception raised when parsing a semantic selector failed.
    """
    pass


# Subclass `tuple` because this is a somewhat simple method to make this immutable
class SemSel(tuple):

    """A Semantic Version Selector, holds a selector and can match it against semantic versions.

    Information on this particular class and their instances:
        - Immutable but not hashable because the content within might have changed.
        - Subclasses `tuple` but does not behave like one.
        - Always `True` in boolean context.
        - len() returns the number of containing *and chunks* (see below).
        - Iterable, iterates over containing *and chunks*.

    When talking about "versions" it refers to a semantic version (SemVer). For information on how
    versions compare to one another, see SemVer's doc string.

    List for **comparators**:
        "1.0.0"            matches the version 1.0.0 and all its pre-release and build variants
        "!1.0.0"           matches any version that is not 1.0.0 or any of its variants
        "=1.0.0"           matches only the version 1.0.0
        "!=1.0.0"          matches any version that is not 1.0.0
        ">=1.0.0"          matches versions greater than or equal 1.0.0
        "<1.0.0"           matches versions smaller than 1.0.0
        "1.0.0 - 1.0.3"    matches versions greater than or equal 1.0.0 thru 1.0.3
        "~1.0"             matches versions greater than or equal 1.0.0 thru 1.0.9999 (and more)
        "~1", "1.x", "1.*" match versions greater than or equal 1.0.0 thru 1.9999.9999 (and more)
        "~1.1.2"           matches versions greater than or equal 1.1.2 thru 1.1.9999 (and more)
        "~1.1.2+any"       matches versions greater than or equal 1.1.2+any thru 1.1.9999 (and more)
        "*", "~", "~x"     match any version

    Multiple comparators can be combined by using ' ' spaces and every comparator must match to make
    the **and chunk** match a version.
    Multiple and chunks can be combined to **or chunks** using ' || ' and match if any of the and
    chunks split by these matches.

    A complete example would look like:
        ~1 || 0.0.3 || <0.0.2 >0.0.1+b.1337 || 2.0.x || 2.1.0 - 2.1.0+b.12 !=2.1.0+b.9

    Methods:
        * matches(*vers)
    """
    # Private properties
    _fuzzy_regex = re.compile(r'''(?x)^
        (?P<op>[<>]=?|~>?=?)?
        (?:(?P<major>\d+)
         (?:\.(?P<minor>\d+)
          (?:\.(?P<patch>\d+)
           (?P<other>[-+][a-zA-Z0-9-+.]*)?
          )?
         )?
        )?$''')
    _xrange_regex = re.compile(r'''(?x)^
        (?P<op>[<>]=?|~>?=?)?
        (?:(?P<major>\d+|[xX*])
         (?:\.(?P<minor>\d+|[xX*])
          (?:\.(?P<patch>\d+|[xX*]))?
         )?
        )
        (?P<other>.*)$''')
    _split_op_regex = re.compile(r'^(?P<op>=|[<>!]=?)?(?P<ver>.*)$')

    # "Constructor"
    def __new__(cls, sel):
        """Constructor examples:
            SemSel(">1.0.0")
            SemSel("~1.2.9 !=1.2.12")

        Parameters:
            * sel (str)
                A version selector string.

        Raises:
            * TypeError
                `sel` parameter is not a string.
            * ValueError
                A version in the selector could not be matched as a SemVer.
            * SemParseError
                The version selector's syntax is unparsable; invalid ranges (fuzzy, xrange or
                explicit range) or invalid '||'
        """
        chunk = cls._parse(sel)
        return super(SemSel, cls).__new__(cls, (chunk,))

    # Magic methods
    def __str__(self):
        return str(self._chunk)

    def __repr__(self):
        return 'SemSel("%s")' % self._chunk

    def __len__(self):
        # What would you expect?
        return len(self._chunk)

    def __iter__(self):
        return iter(self._chunk)

    # Read-only (private) attributes
    @property
    def _chunk(self):
        return self[0]

    # Utility methods
    def matches(self, *vers):
        """Match the selector against a selection of versions.

        Parameters:
            * *vers (str, SemVer)
                Versions can be passed as strings and SemVer objects will be created with them.
                May also be a mixed list.

        Raises:
            * TypeError
                A version is not an instance of str (basestring) or SemVer.
            * ValueError
                A string version could not be parsed as a SemVer.

        Returns:
            * list
                A list with all the versions that matched, may be empty. Use `max()` to determine
                the highest matching version, or `min()` for the lowest.
        """
        ret = []
        for v in vers:
            if isinstance(v, str):
                t = self._chunk.matches(SemVer(v))
            elif isinstance(v, SemVer):
                t = self._chunk.matches(v)
            else:
                raise TypeError("Invalid parameter type '%s': %s" % (v, type(v)))
            if t:
                ret.append(v)

        return ret

    # Private methods
    @classmethod
    def _parse(cls, sel):
        """Private. Do not touch.

        1. split by whitespace into tokens
            a. start new and_chunk on ' || '
            b. parse " - " ranges
            c. replace "xX*" ranges with "~" equivalent
            d. parse "~" ranges
            e. parse unmatched token as comparator
            ~. append to current and_chunk
        2. return SemSelOrChunk

        Raises TypeError, ValueError or SelParseError.
        """
        if not isinstance(sel, basestring):
            raise TypeError("Selector must be a string")
        if not sel:
            raise ValueError("String must not be empty")

        # Split selector by spaces and crawl the tokens
        tokens = sel.split()
        i = -1
        or_chunk = SemSelOrChunk()
        and_chunk = or_chunk.new_child()

        while i + 1 < len(tokens):
            i += 1
            t = tokens[i]

            # Replace x ranges with ~ selector
            m = cls._xrange_regex.match(t)
            m = m and m.groups('')
            if m and any(not x.isdigit() for x in m[1:4]) and not m[0].startswith('>'):
                # (do not match '>1.0' or '>*')
                if m[4]:
                    raise SelParseError("XRanges do not allow pre-release or build components")

                # Only use digit parts and fail if digit found after non-digit
                mm, xran = [], False
                for x in m[1:4]:
                    if x.isdigit():
                        if xran:
                            raise SelParseError("Invalid fuzzy range or XRange '%s'" % tokens[i])
                        mm.append(x)
                    else:
                        xran = True
                t = m[0] + '.'.join(mm)  # x for x in m[1:4] if x.isdigit())
                # Append "~" if not already present
                if not t.startswith('~'):
                    t = '~' + t

            # switch t:
            if t == '||':
                if i == 0 or tokens[i - 1] == '||' or i + 1 == len(tokens):
                    raise SelParseError("OR range must not be empty")
                # Start a new and_chunk
                and_chunk = or_chunk.new_child()

            elif t == '-':
                # ' - ' range
                i += 1
                invalid = False
                try:
                    # If these result in exceptions, you know you're doing it wrong
                    t = tokens[i]
                    c = and_chunk[-1]
                except (Exception):
                    raise SelParseError("Invalid ' - ' range position")

                # If there is an op in front of one of the bound versions
                invalid = (c.op not in ('=', '~')
                           or cls._split_op_regex.match(t).group(1) not in (None, '='))
                if invalid:
                    raise SelParseError("Invalid ' - ' range '%s - %s'"
                                        % (tokens[i - 2], tokens[i]))

                c.op = ">="
                and_chunk.add_child('<=', t)

            elif t == '':
                # Multiple spaces
                pass

            elif t.startswith('~'):
                m = cls._fuzzy_regex.match(t)
                if not m:
                    raise SelParseError("Invalid fuzzy range or XRange '%s'" % tokens[i])

                mm, m = m.groups('')[1:4], m.groupdict('')  # mm: major to patch

                # Minimum requirement
                min_ver = ('.'.join(x or '0' for x in mm) + '-'
                           if not m['other']
                           else cls._split_op_regex(t[1:]).group('ver'))
                and_chunk.add_child('>=', min_ver)

                if m['major']:
                    # Increase version before none (or second to last if '~1.2.3')
                    e = [0, 0, 0]
                    for j, d in enumerate(mm):
                        if not d or j == len(mm) - 1:
                            e[j - 1] = e[j - 1] + 1
                            break
                        e[j] = int(d)

                    and_chunk.add_child('<', '.'.join(str(x) for x in e) + '-')

                # else: just plain '~' or '*', or '~>X' which are already handled

            else:
                # A normal comparator
                m = cls._split_op_regex.match(t).groupdict()  # this regex can't fail
                and_chunk.add_child(**m)

        # Finally return the or_chunk
        return or_chunk
