cb38f21
# Copyright 2019 Gordon Messmer <gordon.messmer@gmail.com>
cb38f21
#
cb38f21
# Upstream: https://github.com/gordonmessmer/pyreq2rpm
cb38f21
#
cb38f21
# Permission is hereby granted, free of charge, to any person
cb38f21
# obtaining a copy of this software and associated documentation files
cb38f21
# (the "Software"), to deal in the Software without restriction,
cb38f21
# including without limitation the rights to use, copy, modify, merge,
cb38f21
# publish, distribute, sublicense, and/or sell copies of the Software,
cb38f21
# and to permit persons to whom the Software is furnished to do so,
cb38f21
# subject to the following conditions:
cb38f21
#
cb38f21
# The above copyright notice and this permission notice shall be
cb38f21
# included in all copies or substantial portions of the Software.
cb38f21
#
cb38f21
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
cb38f21
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
cb38f21
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
cb38f21
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
cb38f21
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
cb38f21
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
cb38f21
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
cb38f21
# SOFTWARE.
cb38f21
cb38f21
from packaging.requirements import Requirement
cb38f21
from packaging.version import parse as parse_version
cb38f21
cb38f21
class RpmVersion():
cb38f21
    def __init__(self, version_id):
cb38f21
        version = parse_version(version_id)
cb38f21
        if isinstance(version._version, str):
cb38f21
            self.version = version._version
cb38f21
        else:
cb38f21
            self.epoch = version._version.epoch
cb38f21
            self.version = list(version._version.release)
cb38f21
            self.pre = version._version.pre
cb38f21
            self.dev = version._version.dev
cb38f21
            self.post = version._version.post
92802d7
            # version.local is ignored as it is not expected to appear
92802d7
            # in public releases
92802d7
            # https://www.python.org/dev/peps/pep-0440/#local-version-identifiers
cb38f21
ec5fc7a
    def is_legacy(self):
ec5fc7a
        return isinstance(self.version, str)
ec5fc7a
cb38f21
    def increment(self):
cb38f21
        self.version[-1] += 1
cb38f21
        self.pre = None
cb38f21
        self.dev = None
cb38f21
        self.post = None
cb38f21
        return self
cb38f21
cb38f21
    def __str__(self):
ec5fc7a
        if self.is_legacy():
cb38f21
            return self.version
cb38f21
        if self.epoch:
cb38f21
            rpm_epoch = str(self.epoch) + ':'
cb38f21
        else:
cb38f21
            rpm_epoch = ''
cb38f21
        while len(self.version) > 1 and self.version[-1] == 0:
cb38f21
            self.version.pop()
cb38f21
        rpm_version = '.'.join(str(x) for x in self.version)
cb38f21
        if self.pre:
cb38f21
            rpm_suffix = '~{}'.format(''.join(str(x) for x in self.pre))
cb38f21
        elif self.dev:
cb38f21
            rpm_suffix = '~~{}'.format(''.join(str(x) for x in self.dev))
cb38f21
        elif self.post:
cb38f21
            rpm_suffix = '^post{}'.format(self.post[1])
cb38f21
        else:
cb38f21
            rpm_suffix = ''
cb38f21
        return '{}{}{}'.format(rpm_epoch, rpm_version, rpm_suffix)
cb38f21
cb38f21
def convert_compatible(name, operator, version_id):
cb38f21
    if version_id.endswith('.*'):
cb38f21
        return 'Invalid version'
cb38f21
    version = RpmVersion(version_id)
ec5fc7a
    if version.is_legacy():
ec5fc7a
        # LegacyVersions are not supported in this context
ec5fc7a
        return 'Invalid version'
cb38f21
    if len(version.version) == 1:
cb38f21
        return 'Invalid version'
cb38f21
    upper_version = RpmVersion(version_id)
cb38f21
    upper_version.version.pop()
cb38f21
    upper_version.increment()
cb38f21
    return '({} >= {} with {} < {})'.format(
cb38f21
        name, version, name, upper_version)
cb38f21
cb38f21
def convert_equal(name, operator, version_id):
cb38f21
    if version_id.endswith('.*'):
cb38f21
        version_id = version_id[:-2] + '.0'
cb38f21
        return convert_compatible(name, '~=', version_id)
cb38f21
    version = RpmVersion(version_id)
cb38f21
    return '{} = {}'.format(name, version)
cb38f21
cb38f21
def convert_arbitrary_equal(name, operator, version_id):
cb38f21
    if version_id.endswith('.*'):
cb38f21
        return 'Invalid version'
cb38f21
    version = RpmVersion(version_id)
cb38f21
    return '{} = {}'.format(name, version)
cb38f21
cb38f21
def convert_not_equal(name, operator, version_id):
cb38f21
    if version_id.endswith('.*'):
cb38f21
        version_id = version_id[:-2]
cb38f21
        version = RpmVersion(version_id)
ec5fc7a
        if version.is_legacy():
ec5fc7a
            # LegacyVersions are not supported in this context
ec5fc7a
            return 'Invalid version'
92802d7
        version_gt = RpmVersion(version_id).increment()
92802d7
        version_gt_operator = '>='
92802d7
        # Prevent dev and pre-releases from satisfying a < requirement
92802d7
        version = '{}~~'.format(version)
cb38f21
    else:
cb38f21
        version = RpmVersion(version_id)
92802d7
        version_gt = version
92802d7
        version_gt_operator = '>'
92802d7
    return '({} < {} or {} {} {})'.format(
92802d7
        name, version, name, version_gt_operator, version_gt)
cb38f21
cb38f21
def convert_ordered(name, operator, version_id):
cb38f21
    if version_id.endswith('.*'):
cb38f21
        # PEP 440 does not define semantics for prefix matching
cb38f21
        # with ordered comparisons
92802d7
        # see: https://github.com/pypa/packaging/issues/320
92802d7
        # and: https://github.com/pypa/packaging/issues/321
92802d7
        # This style of specifier is officially "unsupported",
92802d7
        # even though it is processed.  Support may be removed
92802d7
        # in version 21.0.
cb38f21
        version_id = version_id[:-2]
cb38f21
        version = RpmVersion(version_id)
cb38f21
        if operator == '>':
cb38f21
            # distutils will allow a prefix match with '>'
cb38f21
            operator = '>='
cb38f21
        if operator == '<=':
cb38f21
            # distutils will not allow a prefix match with '<='
cb38f21
            operator = '<'
cb38f21
    else:
cb38f21
        version = RpmVersion(version_id)
ec5fc7a
    # For backwards compatibility, fallback to previous behavior with LegacyVersions
ec5fc7a
    if not version.is_legacy():
ec5fc7a
        # Prevent dev and pre-releases from satisfying a < requirement
ec5fc7a
        if operator == '<' and not version.pre and not version.dev and not version.post:
ec5fc7a
            version = '{}~~'.format(version)
ec5fc7a
        # Prevent post-releases from satisfying a > requirement
ec5fc7a
        if operator == '>' and not version.pre and not version.dev and not version.post:
ec5fc7a
            version = '{}.0'.format(version)
cb38f21
    return '{} {} {}'.format(name, operator, version)
cb38f21
cb38f21
OPERATORS = {'~=': convert_compatible,
cb38f21
             '==': convert_equal,
cb38f21
             '===': convert_arbitrary_equal,
cb38f21
             '!=': convert_not_equal,
cb38f21
             '<=': convert_ordered,
cb38f21
             '<':  convert_ordered,
cb38f21
             '>=': convert_ordered,
cb38f21
             '>':  convert_ordered}
cb38f21
cb38f21
def convert(name, operator, version_id):
cb38f21
    return OPERATORS[operator](name, operator, version_id)
cb38f21
cb38f21
def convert_requirement(req):
cb38f21
    parsed_req = Requirement.parse(req)
cb38f21
    reqs = []
cb38f21
    for spec in parsed_req.specs:
cb38f21
        reqs.append(convert(parsed_req.project_name, spec[0], spec[1]))
cb38f21
    if len(reqs) == 0:
cb38f21
        return parsed_req.project_name
cb38f21
    if len(reqs) == 1:
cb38f21
        return reqs[0]
cb38f21
    else:
cb38f21
        reqs.sort()
cb38f21
        return '({})'.format(' with '.join(reqs))