From 5400124dc6b10d4fc57e5b803e3a0f9786c348b3 Mon Sep 17 00:00:00 2001 From: Gordon Messmer Date: Sep 09 2020 04:47:51 +0000 Subject: This change introduces code from pyreq2rpm, a tested set of requirement conversion functions used in pyp2rpm and rpm's pythondistdeps. This adds support for the '~=' operator and wildcards. --- diff --git a/pyproject_buildrequires.py b/pyproject_buildrequires.py index fd05812..051aea9 100644 --- a/pyproject_buildrequires.py +++ b/pyproject_buildrequires.py @@ -10,6 +10,7 @@ import subprocess import re import tempfile import email.parser +from pyproject_convert import convert print_err = functools.partial(print, file=sys.stderr) @@ -95,20 +96,13 @@ class Requirements: f'Unknown character in version: {specifier.version}. ' + '(This is probably a bug in pyproject-rpm-macros.)', ) - if specifier.operator == '!=': - lower = python3dist(name, '<', version) - higher = python3dist(name, '>', f'{version}.0') - together.append( - f'({lower} or {higher})' - ) - else: - together.append(python3dist(name, specifier.operator, version)) + together.append(convert(python3dist(name), specifier.operator, version)) if len(together) == 0: print(python3dist(name)) elif len(together) == 1: print(together[0]) else: - print(f"({' and '.join(together)})") + print(f"({' with '.join(together)})") def check(self, *, source=None): """End current pass if any unsatisfied dependencies were output""" diff --git a/pyproject_buildrequires_testcases.yaml b/pyproject_buildrequires_testcases.yaml index 05ed361..4ccac58 100644 --- a/pyproject_buildrequires_testcases.yaml +++ b/pyproject_buildrequires_testcases.yaml @@ -87,12 +87,12 @@ Build system dependencies in pyproject.toml: ] expected: | python3dist(foo) - (python3dist(ne) < 1 or python3dist(ne) > 1.0) + (python3dist(ne) < 1 or python3dist(ne) > 1) python3dist(ge) >= 1.2 python3dist(le) <= 1.2.3 python3dist(lt) < 1.2.3.4 python3dist(gt) > 1.2.3.4.5 - ((python3dist(combo) < 3 or python3dist(combo) > 3.0) and python3dist(combo) < 5 and python3dist(combo) > 2) + ((python3dist(combo) < 3 or python3dist(combo) > 3) with python3dist(combo) < 5 with python3dist(combo) > 2) python3dist(py3) python3dist(pkg) python3dist(setuptools) >= 40.8 @@ -108,7 +108,7 @@ Default build system, build dependencies in setup.py: setup( name='test', version='0.1', - setup_requires=['foo', 'bar!=2'], + setup_requires=['foo', 'bar!=2', 'baz~=1.1.1'], install_requires=['inst'], ) expected: | @@ -116,7 +116,8 @@ Default build system, build dependencies in setup.py: python3dist(wheel) python3dist(wheel) python3dist(foo) - (python3dist(bar) < 2 or python3dist(bar) > 2.0) + (python3dist(bar) < 2 or python3dist(bar) > 2) + (python3dist(baz) >= 1.1.1 with python3dist(baz) < 1.2) result: 0 Default build system, run dependencies in setup.py: diff --git a/pyproject_convert.py b/pyproject_convert.py new file mode 100644 index 0000000..95e9498 --- /dev/null +++ b/pyproject_convert.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 + +# Copyright 2019 Gordon Messmer +# +# 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. + +from pkg_resources import Requirement, parse_version + +class RpmVersion(): + def __init__(self, version_id): + version = parse_version(version_id) + if isinstance(version._version, str): + self.version = version._version + else: + self.epoch = version._version.epoch + self.version = list(version._version.release) + self.pre = version._version.pre + self.dev = version._version.dev + self.post = version._version.post + + def increment(self): + self.version[-1] += 1 + self.pre = None + self.dev = None + self.post = None + return self + + def __str__(self): + if isinstance(self.version, str): + return self.version + if self.epoch: + rpm_epoch = str(self.epoch) + ':' + else: + rpm_epoch = '' + while len(self.version) > 1 and self.version[-1] == 0: + self.version.pop() + rpm_version = '.'.join(str(x) for x in self.version) + if self.pre: + rpm_suffix = '~{}'.format(''.join(str(x) for x in self.pre)) + elif self.dev: + rpm_suffix = '~~{}'.format(''.join(str(x) for x in self.dev)) + elif self.post: + rpm_suffix = '^post{}'.format(self.post[1]) + else: + rpm_suffix = '' + return '{}{}{}'.format(rpm_epoch, rpm_version, rpm_suffix) + +def convert_compatible(name, operator, version_id): + if version_id.endswith('.*'): + return 'Invalid version' + version = RpmVersion(version_id) + if len(version.version) == 1: + return 'Invalid version' + upper_version = RpmVersion(version_id) + upper_version.version.pop() + upper_version.increment() + return '({} >= {} with {} < {})'.format( + name, version, name, upper_version) + +def convert_equal(name, operator, version_id): + if version_id.endswith('.*'): + version_id = version_id[:-2] + '.0' + return convert_compatible(name, '~=', version_id) + version = RpmVersion(version_id) + return '{} = {}'.format(name, version) + +def convert_arbitrary_equal(name, operator, version_id): + if version_id.endswith('.*'): + return 'Invalid version' + version = RpmVersion(version_id) + return '{} = {}'.format(name, version) + +def convert_not_equal(name, operator, version_id): + if version_id.endswith('.*'): + version_id = version_id[:-2] + version = RpmVersion(version_id) + lower_version = RpmVersion(version_id).increment() + else: + version = RpmVersion(version_id) + lower_version = version + return '({} < {} or {} > {})'.format( + name, version, name, lower_version) + +def convert_ordered(name, operator, version_id): + if version_id.endswith('.*'): + # PEP 440 does not define semantics for prefix matching + # with ordered comparisons + version_id = version_id[:-2] + version = RpmVersion(version_id) + if operator == '>': + # distutils will allow a prefix match with '>' + operator = '>=' + if operator == '<=': + # distutils will not allow a prefix match with '<=' + operator = '<' + else: + version = RpmVersion(version_id) + return '{} {} {}'.format(name, operator, version) + +OPERATORS = {'~=': convert_compatible, + '==': convert_equal, + '===': convert_arbitrary_equal, + '!=': convert_not_equal, + '<=': convert_ordered, + '<': convert_ordered, + '>=': convert_ordered, + '>': convert_ordered} + +def convert(name, operator, version_id): + return OPERATORS[operator](name, operator, version_id) + +def convert_requirement(req): + parsed_req = Requirement.parse(req) + reqs = [] + for spec in parsed_req.specs: + reqs.append(convert(parsed_req.project_name, spec[0], spec[1])) + if len(reqs) == 0: + return parsed_req.project_name + if len(reqs) == 1: + return reqs[0] + else: + reqs.sort() + return '({})'.format(' with '.join(reqs))