Support more Python version specifiers in generated BuildRequires

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.
This commit is contained in:
Gordon Messmer 2020-09-08 21:47:51 -07:00 committed by Miro Hrončok
parent b2b33b8a0b
commit cb38f210d2
4 changed files with 165 additions and 21 deletions

View File

@ -6,7 +6,7 @@ License: MIT
# Keep the version at zero and increment only release
Version: 0
Release: 27%{?dist}
Release: 28%{?dist}
# Macro files
Source001: macros.pyproject
@ -14,6 +14,7 @@ Source001: macros.pyproject
# Implementation files
Source101: pyproject_buildrequires.py
Source102: pyproject_save_files.py
Source103: pyproject_convert.py
# Tests
Source201: test_pyproject_buildrequires.py
@ -70,6 +71,7 @@ mkdir -p %{buildroot}%{_rpmmacrodir}
mkdir -p %{buildroot}%{_rpmconfigdir}/redhat
install -m 644 macros.pyproject %{buildroot}%{_rpmmacrodir}/
install -m 644 pyproject_buildrequires.py %{buildroot}%{_rpmconfigdir}/redhat/
install -m 644 pyproject_convert.py %{buildroot}%{_rpmconfigdir}/redhat/
install -m 644 pyproject_save_files.py %{buildroot}%{_rpmconfigdir}/redhat/
%if %{with tests}
@ -82,12 +84,17 @@ export HOSTNAME="rpmbuild" # to speedup tox in network-less mock, see rhbz#1856
%files
%{_rpmmacrodir}/macros.pyproject
%{_rpmconfigdir}/redhat/pyproject_buildrequires.py
%{_rpmconfigdir}/redhat/pyproject_convert.py
%{_rpmconfigdir}/redhat/pyproject_save_files.py
%doc README.md
%license LICENSE
%changelog
* Fri Sep 08 2020 Gordon Messmer <gordon.messmer@gmail.com> - 0-28
- Support more Python version specifiers in generated BuildRequires
- This adds support for the '~=' operator and wildcards
* Fri Sep 4 2020 Miro Hrončok <miro@hroncok.cz> - 0-27
- Make code in $PWD importable from %%pyproject_buildrequires
- Only require toml for projects with pyproject.toml

View File

@ -34,6 +34,9 @@ except ImportError as e:
# already echoed by the %pyproject_buildrequires macro
sys.exit(0)
# uses packaging, needs to be imported after packaging is verified to be present
from pyproject_convert import convert
@contextlib.contextmanager
def hook_call():
@ -116,24 +119,15 @@ 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,
self.python3_pkgversion)
higher = python3dist(name, '>', f'{version}.0',
self.python3_pkgversion)
together.append(
f'({lower} or {higher})'
)
else:
together.append(python3dist(name, specifier.operator, version,
self.python3_pkgversion))
together.append(convert(python3dist(name, python3_pkgversion=self.python3_pkgversion),
specifier.operator, version))
if len(together) == 0:
print(python3dist(name,
python3_pkgversion=self.python3_pkgversion))
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"""

View File

@ -97,15 +97,15 @@ Build system dependencies in pyproject.toml with extras:
python3dist(foo)
python3dist(bar) > 5
python3dist(bar[baz]) > 5
(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(multi) == 6
python3dist(multi[extras1]) == 6
python3dist(multi[extras2]) == 6
((python3dist(combo) < 3 or python3dist(combo) > 3.0) and python3dist(combo) < 5 and python3dist(combo) > 2)
python3dist(multi) = 6
python3dist(multi[extras1]) = 6
python3dist(multi[extras2]) = 6
((python3dist(combo) < 3 or python3dist(combo) > 3) with python3dist(combo) < 5 with python3dist(combo) > 2)
python3dist(py3)
python3dist(setuptools) >= 40.8
python3dist(wheel)
@ -126,7 +126,7 @@ Build system dependencies in pyproject.toml without extras:
expected: |
python3dist(toml)
python3dist(bar) > 5
python3dist(multi) == 6
python3dist(multi) = 6
python3dist(setuptools) >= 40.8
python3dist(wheel)
result: 0
@ -140,7 +140,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: |
@ -148,7 +148,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:

142
pyproject_convert.py Normal file
View File

@ -0,0 +1,142 @@
# Copyright 2019 Gordon Messmer <gordon.messmer@gmail.com>
#
# Upstream: https://github.com/gordonmessmer/pyreq2rpm
#
# 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 packaging.requirements import Requirement
from packaging.version import parse as 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))