Compare commits

..

No commits in common. "c8" and "c9s" have entirely different histories.
c8 ... c9s

39 changed files with 3499 additions and 410 deletions

5
.gitignore vendored
View File

@ -0,0 +1,5 @@
/test-sources-2020-04-29.tar.gz
/__pycache__/
/tests/__pycache__/
/tests/data/scripts_pythondistdeps/usr/
/test-sources-2021-03-11.tar.gz

View File

@ -0,0 +1 @@
f9c244a5385fbf619fdd0718ed66ac4c9df914cf test-sources-2021-03-11.tar.gz

View File

@ -1,4 +0,0 @@
%__python_provides %{_rpmconfigdir}/pythondistdeps.py --provides --majorver-provides-versions @MAJORVER-PROVIDES-VERSIONS@
%__python_requires %{_rpmconfigdir}/pythondeps.sh --requires
%__python_path ^((/usr/lib(64)?/python[[:digit:]]+\\.[[:digit:]]+/site-packages/[^/]+\\.(dist-info|egg-info|egg-link))|(/usr/lib(64)?/python[[:digit:]]+\\.[[:digit:]]+/.*\\.(py[oc]?|so))|(%{_bindir}/python[[:digit:]]+\\.[[:digit:]]+))$
%__python_magic [Pp]ython.*(executable|byte-compiled)

View File

@ -1,32 +0,0 @@
#!/bin/bash
[ $# -ge 1 ] || {
cat > /dev/null
exit 0
}
case $1 in
-P|--provides)
shift
# Match buildroot/payload paths of the form
# /PATH/OF/BUILDROOT/usr/bin/pythonMAJOR.MINOR
# generating a line of the form
# python(abi) = MAJOR.MINOR
# (Don't match against -config tools e.g. /usr/bin/python2.6-config)
grep "/usr/bin/python.\..$" \
| sed -e "s|.*/usr/bin/python\(.\..\)|python(abi) = \1|"
;;
-R|--requires)
shift
# Match buildroot paths of the form
# /PATH/OF/BUILDROOT/usr/lib/pythonMAJOR.MINOR/ and
# /PATH/OF/BUILDROOT/usr/lib64/pythonMAJOR.MINOR/
# generating (uniqely) lines of the form:
# python(abi) = MAJOR.MINOR
grep -E "/usr/lib[^/]*/python[[:digit:]]+\.[[:digit:]]+/.*" \
| sed -Ee "s|.*/usr/lib[^/]*/python([[:digit:]]+\.[[:digit:]]+)/.*|python(abi) = \1|g" \
| sort | uniq
;;
esac
exit 0

View File

@ -1,256 +0,0 @@
#!/usr/libexec/platform-python
# -*- coding: utf-8 -*-
#
# Copyright 2010 Per Øyvind Karlsen <proyvind@moondrake.org>
# Copyright 2015 Neal Gompa <ngompa13@gmail.com>
#
# This program is free software. It may be redistributed and/or modified under
# the terms of the LGPL version 2.1 (or later).
#
# RPM python dependency generator, using .egg-info/.egg-link/.dist-info data
#
from __future__ import print_function
from getopt import getopt
from os.path import basename, dirname, isdir, sep
from sys import argv, stdin, version
from distutils.sysconfig import get_python_lib
from warnings import warn
opts, args = getopt(
argv[1:], 'hPRrCEMmLl:',
['help', 'provides', 'requires', 'recommends', 'conflicts', 'extras', 'majorver-provides', 'majorver-provides-versions=', 'majorver-only', 'legacy-provides' , 'legacy'])
Provides = False
Requires = False
Recommends = False
Conflicts = False
Extras = False
Provides_PyMajorVer_Variant = False
Provides_PyMajorVer_Versions = None
PyMajorVer_Deps = False
legacy_Provides = False
legacy = False
for o, a in opts:
if o in ('-h', '--help'):
print('-h, --help\tPrint help')
print('-P, --provides\tPrint Provides')
print('-R, --requires\tPrint Requires')
print('-r, --recommends\tPrint Recommends')
print('-C, --conflicts\tPrint Conflicts')
print('-E, --extras\tPrint Extras ')
print('-M, --majorver-provides\tPrint extra Provides with Python major version only for all Python versions')
print(' --majorver-provides-versions VERSIONS\n'
' \tPrint extra Provides with Python major version only for listed Python VERSIONS (comma separated, no spaces, e.g. 2.7,3.6)')
print('-m, --majorver-only\tPrint Provides/Requires with Python major version only')
print('-L, --legacy-provides\tPrint extra legacy pythonegg Provides')
print('-l, --legacy\tPrint legacy pythonegg Provides/Requires instead')
exit(1)
elif o in ('-P', '--provides'):
Provides = True
elif o in ('-R', '--requires'):
Requires = True
elif o in ('-r', '--recommends'):
Recommends = True
elif o in ('-C', '--conflicts'):
Conflicts = True
elif o in ('-E', '--extras'):
Extras = True
elif o in ('-M', '--majorver-provides'):
Provides_PyMajorVer_Variant = True
elif o in ('--majorver-provides-versions'):
Provides_PyMajorVer_Versions = a.split(",")
elif o in ('-m', '--majorver-only'):
PyMajorVer_Deps = True
elif o in ('-L', '--legacy-provides'):
legacy_Provides = True
elif o in ('-l', '--legacy'):
legacy = True
if Provides_PyMajorVer_Variant and Provides_PyMajorVer_Versions:
print("Error, options --majorver-provides and --majorver-provides-versions are mutually incompatible.")
exit(2)
if Requires:
py_abi = True
else:
py_abi = False
py_deps = {}
if args:
files = args
else:
files = stdin.readlines()
for f in files:
f = f.strip()
lower = f.lower()
name = 'python(abi)'
# add dependency based on path, versioned if within versioned python directory
if py_abi and (lower.endswith('.py') or lower.endswith('.pyc') or lower.endswith('.pyo')):
if name not in py_deps:
py_deps[name] = []
purelib = get_python_lib(standard_lib=0, plat_specific=0).split(version[:3])[0]
platlib = get_python_lib(standard_lib=0, plat_specific=1).split(version[:3])[0]
for lib in (purelib, platlib):
if lib in f:
spec = ('==', f.split(lib)[1].split(sep)[0])
if spec not in py_deps[name]:
py_deps[name].append(spec)
# XXX: hack to workaround RPM internal dependency generator not passing directories
lower_dir = dirname(lower)
if lower_dir.endswith('.egg') or \
lower_dir.endswith('.egg-info') or \
lower_dir.endswith('.dist-info'):
lower = lower_dir
f = dirname(f)
# Determine provide, requires, conflicts & recommends based on egg/dist metadata
if lower.endswith('.egg') or \
lower.endswith('.egg-info') or \
lower.endswith('.dist-info'):
# This import is very slow, so only do it if needed
from pkg_resources import Distribution, FileMetadata, PathMetadata
dist_name = basename(f)
if isdir(f):
path_item = dirname(f)
metadata = PathMetadata(path_item, f)
else:
path_item = f
metadata = FileMetadata(f)
dist = Distribution.from_location(path_item, dist_name, metadata)
# Check if py_version is defined in the metadata file/directory name
if not dist.py_version:
# Try to parse the Python version from the path the metadata
# resides at (e.g. /usr/lib/pythonX.Y/site-packages/...)
import re
res = re.search(r"/python(?P<pyver>\d+\.\d+)/", path_item)
if res:
dist.py_version = res.group('pyver')
else:
warn("Version for {!r} has not been found".format(dist), RuntimeWarning)
continue
# XXX: https://github.com/pypa/setuptools/pull/1275
import platform
platform.python_version = lambda: dist.py_version
if Provides_PyMajorVer_Variant or PyMajorVer_Deps or legacy_Provides or legacy or Provides_PyMajorVer_Versions:
# Get the Python major version
pyver_major = dist.py_version.split('.')[0]
if Provides:
# If egg/dist metadata says package name is python, we provide python(abi)
if dist.key == 'python':
name = 'python(abi)'
if name not in py_deps:
py_deps[name] = []
py_deps[name].append(('==', dist.py_version))
if not legacy or not PyMajorVer_Deps:
name = 'python{}dist({})'.format(dist.py_version, dist.key)
if name not in py_deps:
py_deps[name] = []
if Provides_PyMajorVer_Variant or PyMajorVer_Deps or \
(Provides_PyMajorVer_Versions and dist.py_version in Provides_PyMajorVer_Versions):
pymajor_name = 'python{}dist({})'.format(pyver_major, dist.key)
if pymajor_name not in py_deps:
py_deps[pymajor_name] = []
if legacy or legacy_Provides:
legacy_name = 'pythonegg({})({})'.format(pyver_major, dist.key)
if legacy_name not in py_deps:
py_deps[legacy_name] = []
if dist.version:
spec = ('==', dist.version)
if spec not in py_deps[name]:
if not legacy:
py_deps[name].append(spec)
if Provides_PyMajorVer_Variant or \
(Provides_PyMajorVer_Versions and dist.py_version in Provides_PyMajorVer_Versions):
py_deps[pymajor_name].append(spec)
if legacy or legacy_Provides:
py_deps[legacy_name].append(spec)
if Requires or (Recommends and dist.extras):
name = 'python(abi)'
# If egg/dist metadata says package name is python, we don't add dependency on python(abi)
if dist.key == 'python':
py_abi = False
if name in py_deps:
py_deps.pop(name)
elif py_abi and dist.py_version:
if name not in py_deps:
py_deps[name] = []
spec = ('==', dist.py_version)
if spec not in py_deps[name]:
py_deps[name].append(spec)
deps = dist.requires()
if Recommends:
depsextras = dist.requires(extras=dist.extras)
if not Requires:
for dep in reversed(depsextras):
if dep in deps:
depsextras.remove(dep)
deps = depsextras
# add requires/recommends based on egg/dist metadata
for dep in deps:
if legacy:
name = 'pythonegg({})({})'.format(pyver_major, dep.key)
else:
if PyMajorVer_Deps:
name = 'python{}dist({})'.format(pyver_major, dep.key)
else:
name = 'python{}dist({})'.format(dist.py_version, dep.key)
for spec in dep.specs:
if spec[0] != '!=':
if name not in py_deps:
py_deps[name] = []
if spec not in py_deps[name]:
py_deps[name].append(spec)
if not dep.specs:
py_deps[name] = []
# Unused, for automatic sub-package generation based on 'extras' from egg/dist metadata
# TODO: implement in rpm later, or...?
if Extras:
deps = dist.requires()
extras = dist.extras
print(extras)
for extra in extras:
print('%%package\textras-{}'.format(extra))
print('Summary:\t{} extra for {} python package'.format(extra, dist.key))
print('Group:\t\tDevelopment/Python')
depsextras = dist.requires(extras=[extra])
for dep in reversed(depsextras):
if dep in deps:
depsextras.remove(dep)
deps = depsextras
for dep in deps:
for spec in dep.specs:
if spec[0] == '!=':
print('Conflicts:\t{} {} {}'.format(dep.key, '==', spec[1]))
else:
print('Requires:\t{} {} {}'.format(dep.key, spec[0], spec[1]))
print('%%description\t{}'.format(extra))
print('{} extra for {} python package'.format(extra, dist.key))
print('%%files\t\textras-{}\n'.format(extra))
if Conflicts:
# Should we really add conflicts for extras?
# Creating a meta package per extra with recommends on, which has
# the requires/conflicts in stead might be a better solution...
for dep in dist.requires(extras=dist.extras):
name = dep.key
for spec in dep.specs:
if spec[0] == '!=':
if name not in py_deps:
py_deps[name] = []
spec = ('==', spec[1])
if spec not in py_deps[name]:
py_deps[name].append(spec)
names = list(py_deps.keys())
names.sort()
for name in names:
if py_deps[name]:
# Print out versioned provides, requires, recommends, conflicts
for spec in py_deps[name]:
print('{} {} {}'.format(name, spec[0], spec[1]))
else:
# Print out unversioned provides, requires, recommends, conflicts
print(name)

View File

@ -1,118 +0,0 @@
# Disable automatic bytecompilation. We install only one script and we will
# never "import" it.
%undefine py_auto_byte_compile
Name: python-rpm-generators
Summary: Dependency generators for Python RPMs
Version: 5
Release: 8%{?dist}
# Originally all those files were part of RPM, so license is kept here
License: GPLv2+
Url: https://src.fedoraproject.org/python-rpm-generators
# Commit is the last change in following files
Source0: https://raw.githubusercontent.com/rpm-software-management/rpm/102eab50b3d0d6546dfe082eac0ade21e6b3dbf1/COPYING
Source1: python.attr
Source2: pythondeps.sh
Source3: pythondistdeps.py
BuildArch: noarch
%description
%{summary}.
%package -n python3-rpm-generators
Summary: %{summary}
%if 0%{?rhel} && 0%{?rhel} >= 8
Requires: platform-python-setuptools
%else
Requires: python3-setuptools
%endif
# The point of split
Conflicts: rpm-build < 4.13.0.1-2
%description -n python3-rpm-generators
%{summary}.
%prep
%autosetup -c -T
cp -a %{sources} .
# Set which Python versions should have the major-version provides
# (pythonXdist...) generated
sed -i 's/@MAJORVER-PROVIDES-VERSIONS@/2.7,3.6/' python.attr
%install
install -Dpm0644 -t %{buildroot}%{_fileattrsdir} python.attr
install -Dpm0755 -t %{buildroot}%{_rpmconfigdir} pythondeps.sh pythondistdeps.py
%files -n python3-rpm-generators
%license COPYING
%{_fileattrsdir}/python.attr
%{_rpmconfigdir}/pythondeps.sh
%{_rpmconfigdir}/pythondistdeps.py
%changelog
* Mon Nov 14 2022 Charalampos Stratakis <cstratak@redhat.com> - 5-8
- Fix the pythondeps.sh and pythondistdeps.py scripts for multiple digits python versions
- Resolves: rhbz#2143990
* Tue Jun 15 2021 Tomas Orsava <torsava@redhat.com> - 5-7
- Do not parse nested dist/egg-info metadata
- Resolves: rhbz#1916172
* Thu Dec 12 2019 Tomas Orsava <torsava@redhat.com> - 5-6
- Enabled gating
- Related: rhbz#1776941
* Wed Nov 27 2019 Tomas Orsava <torsava@redhat.com> - 5-5
- Create major-version provides only on major Python versions (2.7, 3.6)
- Fix an extra parenthesis in python.attr
- Resolves: rhbz#1776941
* Fri Nov 16 2018 Lumír Balhar <lbalhar@redhat.com> - 5-4
- Require platform-python-setuptools instead of python3-setuptools
- Resolves: rhbz#1650544
* Sat Jul 28 2018 Miro Hrončok <mhroncok@redhat.com> - 5-3
- Use nonstandardlib for purelib definition (#1609492)
* Tue Jun 05 2018 Tomas Orsava <torsava@redhat.com> - 5-2
- Switch the pythondistdeps.py script to /usr/libexec/platform-python
* Sun Feb 11 2018 Igor Gnatenko <ignatenkobrain@fedoraproject.org> - 5-1
- Fork upstream generators
- "Fix" support of environment markers
* Fri Feb 09 2018 Fedora Release Engineering <releng@fedoraproject.org> - 4.14.0-2.1
- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild
* Tue Nov 28 2017 Tomas Orsava <torsava@redhat.com> - 4.14.0-2
- Switch bootsrapping macro to a bcond for modularity
* Fri Oct 20 2017 Tomas Orsava <torsava@redhat.com> - 4.14.0-1
- Rebase to rpm 4.14.0 final (http://rpm.org/wiki/Releases/4.14.0)
- Re-synchronize version/release macros with the rpm Fedora package
* Mon Sep 18 2017 Tomas Orsava <torsava@redhat.com> - 4.14.0-0.rc1.1
- Update to a new upstream version of RPM
- Drop upstreamed patches
- Renumber remaining patches
* Thu Aug 24 2017 Miro Hrončok <mhroncok@redhat.com> - 4.13.0.1-4
- Add patch 10: Do not provide pythonXdist for platform-python packages (rhbz#1484607)
* Tue Aug 08 2017 Tomas Orsava <torsava@redhat.com> - 4.13.0.1-3
- Add patch 9: Generate requires and provides for platform-python(abi)
(https://fedoraproject.org/wiki/Changes/Platform_Python_Stack)
* Thu Jul 27 2017 Fedora Release Engineering <releng@fedoraproject.org> - 4.13.0.1-2.1
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild
* Thu May 18 2017 Tomas Orsava <torsava@redhat.com> - 4.13.0.1-2
- Added a license file
- Added a dependency on rpm for the proper directory structure
- Properly owning the __pycache__ directory
* Tue May 02 2017 Tomas Orsava <torsava@redhat.com> - 4.13.0.1-1
- Splitting Python RPM generators from the `rpm` package to standalone one

6
gating.yaml Normal file
View File

@ -0,0 +1,6 @@
--- !Policy
product_versions:
- rhel-9
decision_context: osci_compose_gate
rules:
- !PassingTestCaseRule {test_case_name: baseos-ci.brew-build.tier1.functional}

224
python-rpm-generators.spec Normal file
View File

@ -0,0 +1,224 @@
Name: python-rpm-generators
Summary: Dependency generators for Python RPMs
Version: 12
Release: 9%{?dist}
# Originally all those files were part of RPM, so license is kept here
License: GPLv2+
Url: https://src.fedoraproject.org/python-rpm-generators
# Commit is the last change in following files
Source0: https://raw.githubusercontent.com/rpm-software-management/rpm/102eab50b3d0d6546dfe082eac0ade21e6b3dbf1/COPYING
Source1: python.attr
Source2: pythondist.attr
Source3: pythonname.attr
Source4: pythondistdeps.py
Source5: pythonbundles.py
BuildArch: noarch
%description
%{summary}.
%package -n python3-rpm-generators
Summary: %{summary}
Requires: python3-packaging
# We have parametric macro generators, we need RPM 4.16 (4.15.90+ is 4.16 alpha)
Requires: rpm > 4.15.90-0
# This contains the Lua functions we use:
Requires: python-srpm-macros >= 3.9-49
%description -n python3-rpm-generators
%{summary}.
%prep
%autosetup -c -T
cp -a %{sources} .
%install
install -Dpm0644 -t %{buildroot}%{_fileattrsdir} *.attr
install -Dpm0755 -t %{buildroot}%{_rpmconfigdir} *.py
%files -n python3-rpm-generators
%license COPYING
%{_fileattrsdir}/python.attr
%{_fileattrsdir}/pythondist.attr
%{_fileattrsdir}/pythonname.attr
%{_rpmconfigdir}/pythondistdeps.py
%{_rpmconfigdir}/pythonbundles.py
%changelog
* Fri Sep 22 2023 Tomáš Hrnčiar <thrnciar@redhat.com> - 12-9
- Avoid needless pkg_resources import in pythonbundles.py
- Resolves: RHEL-6110
* Wed Jan 26 2022 Tomas Orsava <torsava@redhat.com> - 12-8
- From `python3-foo` packages automatically generate `python3.X-foo` Obsoletes
tags on CentOS/RHEL
- Resolves: rhbz#1990421
* Tue Aug 10 2021 Mohan Boddu <mboddu@redhat.com> - 12-7
- Rebuilt for IMA sigs, glibc 2.34, aarch64 flags
Related: rhbz#1991688
* Mon Apr 19 2021 Miro Hrončok <mhroncok@redhat.com> - 12-6
- Get rid of distutils deprecation warning (by not using it)
- The distutils module is deprecated in Python 3.10+
- https://www.python.org/dev/peps/pep-0632/
* Fri Apr 16 2021 Miro Hrončok <mhroncok@redhat.com> - 12-5.1
- Do not generate setuptools requirement for console_scripts on Python 3.10+
- See https://fedoraproject.org/wiki/Changes/Reduce_dependencies_on_python3-setuptools
* Fri Apr 16 2021 Mohan Boddu <mboddu@redhat.com> - 12-5
- Rebuilt for RHEL 9 BETA on Apr 15th 2021. Related: rhbz#1947937
* Thu Mar 11 2021 Tomas Orsava <torsava@redhat.com> - 12-4
- scripts/pythondistdeps: Treat extras names case-insensitively and always
output them in lower case (#1936875)
* Mon Feb 22 2021 Tomas Orsava <torsava@redhat.com> - 12-3
- scripts/pythondistdeps: Fix for Python 3.10
* Wed Feb 17 2021 Tomas Orsava <torsava@redhat.com> - 12-2
- scripts/pythondistdeps: Switch from using pkg_resources to importlib.metadata
for reading the egg/dist-info metadata
- The script no longer requires setuptools but instead requires packaging
* Wed Feb 03 2021 Miro Hrončok <mhroncok@redhat.com> - 12-1
- Disable the dist generators for Python 2
- https://fedoraproject.org/wiki/Changes/Disable_Python_2_Dist_RPM_Generators_and_Freeze_Python_2_Macros
* Wed Jan 27 2021 Fedora Release Engineering <releng@fedoraproject.org> - 11-13
- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild
* Mon Oct 19 2020 Tomas Orsava <torsava@redhat.com> - 11-12
- Run scripts in an isolated Python environment (#1889080)
* Wed Jul 29 2020 Fedora Release Engineering <releng@fedoraproject.org> - 11-11
- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild
* Tue Jul 21 2020 Miro Hrončok <mhroncok@redhat.com> - 11-10
- pythondistdeps: Split Python Extras names after the rightmost plus sign
- pythondistdeps: Handle edge cases of version comparisons more closely to
upstream, despite irrationality
See: https://github.com/pypa/packaging/issues/320
* Fri Jul 10 2020 Tomas Orsava <torsava@redhat.com> - 11-9
- pythondistdeps: Implement provides/requires for extras packages
- Enable --require-extras-subpackages
- Adapt Python version marker workaround for setuptools 42+
* Fri Jun 26 2020 Miro Hrončok <mhroncok@redhat.com> - 11-8
- Fix python(abi) requires generator, it picked files from almost good directories
- Add a script to generate Python bundled provides
* Thu May 21 2020 Miro Hrončok <mhroncok@redhat.com> - 11-7
- Use PEP 503 names for requires
* Tue May 05 2020 Miro Hrončok <mhroncok@redhat.com> - 11-6
- Deduplicate automatically provided names trough Python RPM Lua macros
* Wed Apr 29 2020 Tomas Orsava <torsava@redhat.com> - 11-5
- Backporting proposed upstream changes
https://github.com/rpm-software-management/rpm/pull/1195
- Only provide python3dist(..) for the main Python versions (BZ#1812083)
- Preparation for the proper handling of normalized names (BZ#1791530)
- Add a test suite (and enable it in Fedora CI)
- Better error messages for unsupported package versions
- Fix sorting of dev versions
* Tue Apr 28 2020 Miro Hrončok <mhroncok@redhat.com> - 11-4
- Don't define global Lua variables from Python generator
* Mon Apr 20 2020 Gordon Messmer <gordon.messmer@gmail.com> - 11-3
- Handle all-zero versions without crashing
* Tue Apr 07 2020 Miro Hrončok <mhroncok@redhat.com> - 11-2
- Use dynamic %%_prefix value when matching files for python(abi) provides
- Sync with upstream RPM dist generator
* Wed Apr 01 2020 Miro Hrončok <mhroncok@redhat.com> - 11-1
- Rewrite python(abi) generators to Lua to make them faster
- RPM 4.16+ is needed
- Automatically call %%python_provide
* Thu Jan 30 2020 Fedora Release Engineering <releng@fedoraproject.org> - 10-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild
* Fri Jan 17 2020 Miro Hrončok <mhroncok@redhat.com> - 10-3
- Also provide pythonXdist() with PEP 503 normalized names (#1791530)
* Fri Jan 03 2020 Miro Hrončok <mhroncok@redhat.com> - 10-2
- Fix more complicated requirement expressions by adding parenthesis
* Wed Jan 01 2020 Miro Hrončok <mhroncok@redhat.com> - 10-1
- Handle version ending with ".*" (#1758141)
- Handle compatible-release operator "~=" (#1758141)
- Use rich deps for semantically versioned dependencies
- Match Python version if minor has multiple digits (e.g. 3.10, #1777382)
- Only add setuptools requirement for egg-info packages
* Fri Jul 26 2019 Fedora Release Engineering <releng@fedoraproject.org> - 9-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild
* Mon Jun 24 2019 Tomas Orsava <torsava@redhat.com> - 9-1
- Canonicalize Python versions and properly handle != spec
* Wed Apr 17 2019 Miro Hrončok <mhroncok@redhat.com> - 8-1
- console_scripts entry points to require setuptools
https://github.com/rpm-software-management/rpm/pull/666
* Sat Feb 02 2019 Fedora Release Engineering <releng@fedoraproject.org> - 7-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild
* Thu Dec 20 2018 Igor Gnatenko <ignatenkobrain@fedoraproject.org> - 7-1
- Enable requires generator
* Wed Oct 03 2018 Igor Gnatenko <ignatenkobrain@fedoraproject.org> - 6-1
- Tighten regex for depgen
* Sat Jul 28 2018 Miro Hrončok <mhroncok@redhat.com> - 5-4
- Use nonstandardlib for purelib definition (#1609492)
* Sat Jul 28 2018 Igor Gnatenko <ignatenkobrain@fedoraproject.org> - 5-3
- Add pythondist generator
* Sat Jul 14 2018 Fedora Release Engineering <releng@fedoraproject.org> - 5-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild
* Sun Feb 11 2018 Igor Gnatenko <ignatenkobrain@fedoraproject.org> - 5-1
- Fork upstream generators
- "Fix" support of environment markers
* Fri Feb 09 2018 Fedora Release Engineering <releng@fedoraproject.org> - 4.14.0-2.1
- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild
* Tue Nov 28 2017 Tomas Orsava <torsava@redhat.com> - 4.14.0-2
- Switch bootsrapping macro to a bcond for modularity
* Fri Oct 20 2017 Tomas Orsava <torsava@redhat.com> - 4.14.0-1
- Rebase to rpm 4.14.0 final (http://rpm.org/wiki/Releases/4.14.0)
- Re-synchronize version/release macros with the rpm Fedora package
* Mon Sep 18 2017 Tomas Orsava <torsava@redhat.com> - 4.14.0-0.rc1.1
- Update to a new upstream version of RPM
- Drop upstreamed patches
- Renumber remaining patches
* Thu Aug 24 2017 Miro Hrončok <mhroncok@redhat.com> - 4.13.0.1-4
- Add patch 10: Do not provide pythonXdist for platform-python packages (rhbz#1484607)
* Tue Aug 08 2017 Tomas Orsava <torsava@redhat.com> - 4.13.0.1-3
- Add patch 9: Generate requires and provides for platform-python(abi)
(https://fedoraproject.org/wiki/Changes/Platform_Python_Stack)
* Thu Jul 27 2017 Fedora Release Engineering <releng@fedoraproject.org> - 4.13.0.1-2.1
- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild
* Thu May 18 2017 Tomas Orsava <torsava@redhat.com> - 4.13.0.1-2
- Added a license file
- Added a dependency on rpm for the proper directory structure
- Properly owning the __pycache__ directory
* Tue May 02 2017 Tomas Orsava <torsava@redhat.com> - 4.13.0.1-1
- Splitting Python RPM generators from the `rpm` package to standalone one

27
python.attr Normal file
View File

@ -0,0 +1,27 @@
%__python_provides() %{lua:
-- Match buildroot/payload paths of the form
-- /PATH/OF/BUILDROOT/usr/bin/pythonMAJOR.MINOR
-- generating a line of the form
-- python(abi) = MAJOR.MINOR
-- (Don't match against -config tools e.g. /usr/bin/python2.6-config)
local path = rpm.expand('%1')
if path:match('/usr/bin/python%d+%.%d+$') then
local provides = path:gsub('.*/usr/bin/python(%d+%.%d+)', 'python(abi) = %1')
print(provides)
end
}
%__python_requires() %{lua:
-- Match buildroot paths of the form
-- /PATH/OF/BUILDROOT/usr/lib/pythonMAJOR.MINOR/ and
-- /PATH/OF/BUILDROOT/usr/lib64/pythonMAJOR.MINOR/
-- generating a line of the form:
-- python(abi) = MAJOR.MINOR
local path = rpm.expand('%1')
if path:match('/usr/lib%d*/python%d+%.%d+/.*') then
local requires = path:gsub('.*/usr/lib%d*/python(%d+%.%d+)/.*', 'python(abi) = %1')
print(requires)
end
}
%__python_path ^((%{_prefix}/lib(64)?/python[[:digit:]]+\\.[[:digit:]]+/.*\\.(py[oc]?|so))|(%{_bindir}/python[[:digit:]]+\\.[[:digit:]]+))$

85
pythonbundles.py Executable file
View File

@ -0,0 +1,85 @@
#!/usr/bin/python3 -sB
# (imports pythondistdeps from /usr/lib/rpm, hence -B)
#
# This program is free software.
#
# It is placed in the public domain or under the CC0-1.0-Universal license,
# whichever is more permissive.
#
# Alternatively, it may be redistributed and/or modified under the terms of
# the LGPL version 2.1 (or later) or GPL version 2 (or later).
#
# Use this script to generate bundled provides, e.g.:
# ./pythonbundles.py setuptools-47.1.1/pkg_resources/_vendor/vendored.txt
import pathlib
import sys
import pythondistdeps
def generate_bundled_provides(path, namespace):
provides = set()
for line in path.read_text().splitlines():
line, _, comment = line.partition('#')
if comment.startswith('egg='):
# not a real comment
# e.g. git+https://github.com/monty/spam.git@master#egg=spam&...
egg, *_ = comment.strip().partition(' ')
egg, *_ = egg.strip().partition('&')
name = pythondistdeps.normalize_name(egg[4:])
provides.add(f'Provides: bundled({namespace}({name}))')
continue
line = line.strip()
if line:
name, _, version = line.partition('==')
name = pythondistdeps.normalize_name(name)
bundled_name = f"bundled({namespace}({name}))"
python_provide = pythondistdeps.convert(bundled_name, '==', version)
provides.add(f'Provides: {python_provide}')
return provides
def compare(expected, given):
stripped = (l.strip() for l in given)
no_comments = set(l for l in stripped if not l.startswith('#'))
no_comments.discard('')
if expected == no_comments:
return True
extra_expected = expected - no_comments
extra_given = no_comments - expected
if extra_expected:
print('Missing expected provides:', file=sys.stderr)
for provide in sorted(extra_expected):
print(f' - {provide}', file=sys.stderr)
if extra_given:
print('Redundant unexpected provides:', file=sys.stderr)
for provide in sorted(extra_given):
print(f' + {provide}', file=sys.stderr)
return False
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(prog=sys.argv[0],
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('vendored', metavar='VENDORED.TXT',
help='Upstream information about vendored libraries')
parser.add_argument('-c', '--compare-with', action='store',
help='A string value to compare with and verify')
parser.add_argument('-n', '--namespace', action='store',
help='What namespace of provides will used', default='python3dist')
args = parser.parse_args()
provides = generate_bundled_provides(pathlib.Path(args.vendored), args.namespace)
if args.compare_with:
given = args.compare_with.splitlines()
same = compare(provides, given)
if not same:
sys.exit(1)
else:
for provide in sorted(provides):
print(provide)

3
pythondist.attr Normal file
View File

@ -0,0 +1,3 @@
%__pythondist_provides %{_rpmconfigdir}/pythondistdeps.py --provides --normalized-names-format pep503 --package-name %{name} --normalized-names-provide-both --majorver-provides-versions %{__default_python3_version}
%__pythondist_requires %{_rpmconfigdir}/pythondistdeps.py --requires --normalized-names-format pep503 --package-name %{name} %{?!_python_no_extras_requires:--require-extras-subpackages} --console-scripts-nodep-setuptools-since 3.10
%__pythondist_path ^/usr/lib(64)?/python[3-9]\\.[[:digit:]]+/site-packages/[^/]+\\.(dist-info|egg-info|egg-link)$

546
pythondistdeps.py Executable file
View File

@ -0,0 +1,546 @@
#!/usr/bin/python3 -s
# -*- coding: utf-8 -*-
#
# Copyright 2010 Per Øyvind Karlsen <proyvind@moondrake.org>
# Copyright 2015 Neal Gompa <ngompa13@gmail.com>
# Copyright 2020 SUSE LLC
#
# This program is free software. It may be redistributed and/or modified under
# the terms of the LGPL version 2.1 (or later).
#
# RPM python dependency generator, using .egg-info/.egg-link/.dist-info data
#
from __future__ import print_function
import argparse
from os.path import dirname, sep
import re
from sys import argv, stdin, stderr, version_info
from sysconfig import get_path
from warnings import warn
from packaging.requirements import Requirement as Requirement_
from packaging.version import parse
import packaging.markers
# Monkey patching packaging.markers to handle extras names in a
# case-insensitive manner:
# pip considers dnspython[DNSSEC] and dnspython[dnssec] to be equal, but
# packaging markers treat extras in a case-sensitive manner. To solve this
# issue, we introduce a comparison operator that compares case-insensitively
# if both sides of the comparison are strings. And then we inject this
# operator into packaging.markers to be used when comparing names of extras.
# Fedora BZ: https://bugzilla.redhat.com/show_bug.cgi?id=1936875
# Upstream issue: https://discuss.python.org/t/what-extras-names-are-treated-as-equal-and-why/7614
# - After it's established upstream what is the canonical form of an extras
# name, we plan to open an issue with packaging to hopefully solve this
# there without having to resort to monkeypatching.
def str_lower_eq(a, b):
if isinstance(a, str) and isinstance(b, str):
return a.lower() == b.lower()
else:
return a == b
packaging.markers._operators["=="] = str_lower_eq
try:
from importlib.metadata import PathDistribution
except ImportError:
from importlib_metadata import PathDistribution
try:
from pathlib import Path
except ImportError:
from pathlib2 import Path
def normalize_name(name):
"""https://www.python.org/dev/peps/pep-0503/#normalized-names"""
return re.sub(r'[-_.]+', '-', name).lower()
def legacy_normalize_name(name):
"""Like pkg_resources Distribution.key property"""
return re.sub(r'[-_]+', '-', name).lower()
class Requirement(Requirement_):
def __init__(self, requirement_string):
super(Requirement, self).__init__(requirement_string)
self.normalized_name = normalize_name(self.name)
self.legacy_normalized_name = legacy_normalize_name(self.name)
class Distribution(PathDistribution):
def __init__(self, path):
super(Distribution, self).__init__(Path(path))
self.normalized_name = normalize_name(self.name)
self.legacy_normalized_name = legacy_normalize_name(self.name)
self.requirements = [Requirement(r) for r in self.requires or []]
self.extras = [
v.lower() for k, v in self.metadata.items() if k == 'Provides-Extra']
self.py_version = self._parse_py_version(path)
# `name` is defined as a property exactly like this in Python 3.10 in the
# PathDistribution class. Due to that we can't redefine `name` as a normal
# attribute. So we copied the Python 3.10 definition here into the code so
# that it works also on previous Python/importlib_metadata versions.
@property
def name(self):
"""Return the 'Name' metadata for the distribution package."""
return self.metadata['Name']
def _parse_py_version(self, path):
# Try to parse the Python version from the path the metadata
# resides at (e.g. /usr/lib/pythonX.Y/site-packages/...)
res = re.search(r"/python(?P<pyver>\d+\.\d+)/", path)
if res:
return res.group('pyver')
# If that hasn't worked, attempt to parse it from the metadata
# directory name
res = re.search(r"-py(?P<pyver>\d+.\d+)[.-]egg-info$", path)
if res:
return res.group('pyver')
return None
def requirements_for_extra(self, extra):
extra_deps = []
for req in self.requirements:
if not req.marker:
continue
if req.marker.evaluate(get_marker_env(self, extra)):
extra_deps.append(req)
return extra_deps
def __repr__(self):
return '{} from {}'.format(self.name, self._path)
class RpmVersion():
def __init__(self, version_id):
version = parse(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('.*'):
print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***")
print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
exit(65) # os.EX_DATAERR
version = RpmVersion(version_id)
if len(version.version) == 1:
print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***")
print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
exit(65) # os.EX_DATAERR
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('.*'):
print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***")
print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr)
exit(65) # os.EX_DATAERR
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):
try:
return OPERATORS[operator](name, operator, version_id)
except Exception as exc:
raise RuntimeError("Cannot process Python package version `{}` for name `{}`".
format(version_id, name)) from exc
def get_marker_env(dist, extra):
# packaging uses a default environment using
# platform.python_version to evaluate if a dependency is relevant
# based on environment markers [1],
# e.g. requirement `argparse;python_version<"2.7"`
#
# Since we're running this script on one Python version while
# possibly evaluating packages for different versions, we
# set up an environment with the version we want to evaluate.
#
# [1] https://www.python.org/dev/peps/pep-0508/#environment-markers
return {"python_full_version": dist.py_version,
"python_version": dist.py_version,
"extra": extra}
if __name__ == "__main__":
"""To allow this script to be importable (and its classes/functions
reused), actions are performed only when run as a main script."""
parser = argparse.ArgumentParser(prog=argv[0])
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-P', '--provides', action='store_true', help='Print Provides')
group.add_argument('-R', '--requires', action='store_true', help='Print Requires')
group.add_argument('-r', '--recommends', action='store_true', help='Print Recommends')
group.add_argument('-C', '--conflicts', action='store_true', help='Print Conflicts')
group.add_argument('-E', '--extras', action='store_true', help='[Unused] Generate spec file snippets for extras subpackages')
group_majorver = parser.add_mutually_exclusive_group()
group_majorver.add_argument('-M', '--majorver-provides', action='store_true', help='Print extra Provides with Python major version only')
group_majorver.add_argument('--majorver-provides-versions', action='append',
help='Print extra Provides with Python major version only for listed '
'Python VERSIONS (appended or comma separated without spaces, e.g. 2.7,3.9)')
parser.add_argument('-m', '--majorver-only', action='store_true', help='Print Provides/Requires with Python major version only')
parser.add_argument('-n', '--normalized-names-format', action='store',
default="legacy-dots", choices=["pep503", "legacy-dots"],
help='Format of normalized names according to pep503 or legacy format that allows dots [default]')
parser.add_argument('--normalized-names-provide-both', action='store_true',
help='Provide both `pep503` and `legacy-dots` format of normalized names (useful for a transition period)')
parser.add_argument('-L', '--legacy-provides', action='store_true', help='Print extra legacy pythonegg Provides')
parser.add_argument('-l', '--legacy', action='store_true', help='Print legacy pythonegg Provides/Requires instead')
parser.add_argument('--console-scripts-nodep-setuptools-since', action='store',
help='An optional Python version (X.Y), at least 3.8. '
'For that version and any newer version, '
'a dependency on "setuptools" WILL NOT be generated for packages with console_scripts/gui_scripts entry points. '
'By setting this flag, you guarantee that setuptools >= 47.2.0 is used '
'during the build of packages for this and any newer Python version.')
parser.add_argument('--require-extras-subpackages', action='store_true',
help="If there is a dependency on a package with extras functionality, require the extras subpackage")
parser.add_argument('--package-name', action='store', help="Name of the RPM package that's being inspected. Required for extras requires/provides to work.")
parser.add_argument('files', nargs=argparse.REMAINDER, help="Files from the RPM package that are to be inspected, can also be supplied on stdin")
args = parser.parse_args()
py_abi = args.requires
py_deps = {}
if args.majorver_provides_versions:
# Go through the arguments (can be specified multiple times),
# and parse individual versions (can be comma-separated)
args.majorver_provides_versions = [v for vstring in args.majorver_provides_versions
for v in vstring.split(",")]
# If normalized_names_require_pep503 is True we require the pep503
# normalized name, if it is False we provide the legacy normalized name
normalized_names_require_pep503 = args.normalized_names_format == "pep503"
# If normalized_names_provide_pep503/legacy is True we provide the
# pep503/legacy normalized name, if it is False we don't
normalized_names_provide_pep503 = \
args.normalized_names_format == "pep503" or args.normalized_names_provide_both
normalized_names_provide_legacy = \
args.normalized_names_format == "legacy-dots" or args.normalized_names_provide_both
# At least one type of normalization must be provided
assert normalized_names_provide_pep503 or normalized_names_provide_legacy
if args.console_scripts_nodep_setuptools_since:
nodep_setuptools_pyversion = parse(args.console_scripts_nodep_setuptools_since)
if nodep_setuptools_pyversion < parse("3.8"):
print("Only version 3.8+ is supported in --console-scripts-nodep-setuptools-since", file=stderr)
print("*** PYTHON_EXTRAS_ARGUMENT_ERROR___SEE_STDERR ***")
exit(65) # os.EX_DATAERR
else:
nodep_setuptools_pyversion = None
# Is this script being run for an extras subpackage?
extras_subpackage = None
if args.package_name and '+' in args.package_name:
# The extras names are encoded in the package names after the + sign.
# We take the part after the rightmost +, ignoring when empty,
# this allows packages like nicotine+ or c++ to work fine.
# While packages with names like +spam or foo+bar would break,
# names started with the plus sign are not very common
# and pluses in the middle can be easily replaced with dashes.
# Python extras names don't contain pluses according to PEP 508.
package_name_parts = args.package_name.rpartition('+')
extras_subpackage = package_name_parts[2].lower() or None
for f in (args.files or stdin.readlines()):
f = f.strip()
lower = f.lower()
name = 'python(abi)'
# add dependency based on path, versioned if within versioned python directory
if py_abi and (lower.endswith('.py') or lower.endswith('.pyc') or lower.endswith('.pyo')):
if name not in py_deps:
py_deps[name] = []
running_python_version = '{}.{}'.format(*version_info[:2])
purelib = get_path('purelib').split(running_python_version)[0]
platlib = get_path('platlib').split(running_python_version)[0]
for lib in (purelib, platlib):
if lib in f:
spec = ('==', f.split(lib)[1].split(sep)[0])
if spec not in py_deps[name]:
py_deps[name].append(spec)
# XXX: hack to workaround RPM internal dependency generator not passing directories
lower_dir = dirname(lower)
if lower_dir.endswith('.egg') or \
lower_dir.endswith('.egg-info') or \
lower_dir.endswith('.dist-info'):
lower = lower_dir
f = dirname(f)
# Determine provide, requires, conflicts & recommends based on egg/dist metadata
if lower.endswith('.egg') or \
lower.endswith('.egg-info') or \
lower.endswith('.dist-info'):
dist = Distribution(f)
if not dist.py_version:
warn("Version for {!r} has not been found".format(dist), RuntimeWarning)
continue
# If processing an extras subpackage:
# Check that the extras name is declared in the metadata, or
# that there are some dependencies associated with the extras
# name in the requires.txt (this is an outdated way to declare
# extras packages).
# - If there is an extras package declared only in requires.txt
# without any dependencies, this check will fail. In that case
# make sure to use updated metadata and declare the extras
# package there.
if extras_subpackage and extras_subpackage not in dist.extras and not dist.requirements_for_extra(extras_subpackage):
print("*** PYTHON_EXTRAS_NOT_FOUND_ERROR___SEE_STDERR ***")
print(f"\nError: The package name contains an extras name `{extras_subpackage}` that was not found in the metadata.\n"
"Check if the extras were removed from the project. If so, consider removing the subpackage and obsoleting it from another.\n", file=stderr)
exit(65) # os.EX_DATAERR
if args.majorver_provides or args.majorver_provides_versions or \
args.majorver_only or args.legacy_provides or args.legacy:
# Get the Python major version
pyver_major = dist.py_version.split('.')[0]
if args.provides:
extras_suffix = f"[{extras_subpackage}]" if extras_subpackage else ""
# If egg/dist metadata says package name is python, we provide python(abi)
if dist.normalized_name == 'python':
name = 'python(abi)'
if name not in py_deps:
py_deps[name] = []
py_deps[name].append(('==', dist.py_version))
if not args.legacy or not args.majorver_only:
if normalized_names_provide_legacy:
name = 'python{}dist({}{})'.format(dist.py_version, dist.legacy_normalized_name, extras_suffix)
if name not in py_deps:
py_deps[name] = []
if normalized_names_provide_pep503:
name_ = 'python{}dist({}{})'.format(dist.py_version, dist.normalized_name, extras_suffix)
if name_ not in py_deps:
py_deps[name_] = []
if args.majorver_provides or args.majorver_only or \
(args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions):
if normalized_names_provide_legacy:
pymajor_name = 'python{}dist({}{})'.format(pyver_major, dist.legacy_normalized_name, extras_suffix)
if pymajor_name not in py_deps:
py_deps[pymajor_name] = []
if normalized_names_provide_pep503:
pymajor_name_ = 'python{}dist({}{})'.format(pyver_major, dist.normalized_name, extras_suffix)
if pymajor_name_ not in py_deps:
py_deps[pymajor_name_] = []
if args.legacy or args.legacy_provides:
legacy_name = 'pythonegg({})({})'.format(pyver_major, dist.legacy_normalized_name)
if legacy_name not in py_deps:
py_deps[legacy_name] = []
if dist.version:
version = dist.version
spec = ('==', version)
if normalized_names_provide_legacy:
if spec not in py_deps[name]:
py_deps[name].append(spec)
if args.majorver_provides or \
(args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions):
py_deps[pymajor_name].append(spec)
if normalized_names_provide_pep503:
if spec not in py_deps[name_]:
py_deps[name_].append(spec)
if args.majorver_provides or \
(args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions):
py_deps[pymajor_name_].append(spec)
if args.legacy or args.legacy_provides:
if spec not in py_deps[legacy_name]:
py_deps[legacy_name].append(spec)
if args.requires or (args.recommends and dist.extras):
name = 'python(abi)'
# If egg/dist metadata says package name is python, we don't add dependency on python(abi)
if dist.normalized_name == 'python':
py_abi = False
if name in py_deps:
py_deps.pop(name)
elif py_abi and dist.py_version:
if name not in py_deps:
py_deps[name] = []
spec = ('==', dist.py_version)
if spec not in py_deps[name]:
py_deps[name].append(spec)
if extras_subpackage:
deps = [d for d in dist.requirements_for_extra(extras_subpackage)]
else:
deps = dist.requirements
# console_scripts/gui_scripts entry points needed pkg_resources from setuptools
# on new Python/setuptools versions, this is no longer required
if nodep_setuptools_pyversion is None or parse(dist.py_version) < nodep_setuptools_pyversion:
if (dist.entry_points and
(lower.endswith('.egg') or
lower.endswith('.egg-info'))):
groups = {ep.group for ep in dist.entry_points}
if {"console_scripts", "gui_scripts"} & groups:
# stick them first so any more specific requirement
# overrides it
deps.insert(0, Requirement('setuptools'))
# add requires/recommends based on egg/dist metadata
for dep in deps:
# Even if we're requiring `foo[bar]`, also require `foo`
# to be safe, and to make it discoverable through
# `repoquery --whatrequires`
extras_suffixes = [""]
if args.require_extras_subpackages and dep.extras:
# A dependency can have more than one extras,
# i.e. foo[bar,baz], so let's go through all of them
extras_suffixes += [f"[{e.lower()}]" for e in dep.extras]
for extras_suffix in extras_suffixes:
if normalized_names_require_pep503:
dep_normalized_name = dep.normalized_name
else:
dep_normalized_name = dep.legacy_normalized_name
if args.legacy:
name = 'pythonegg({})({})'.format(pyver_major, dep.legacy_normalized_name)
else:
if args.majorver_only:
name = 'python{}dist({}{})'.format(pyver_major, dep_normalized_name, extras_suffix)
else:
name = 'python{}dist({}{})'.format(dist.py_version, dep_normalized_name, extras_suffix)
if dep.marker and not args.recommends and not extras_subpackage:
if not dep.marker.evaluate(get_marker_env(dist, '')):
continue
if name not in py_deps:
py_deps[name] = []
for spec in dep.specifier:
if (spec.operator, spec.version) not in py_deps[name]:
py_deps[name].append((spec.operator, spec.version))
# Unused, for automatic sub-package generation based on 'extras' from egg/dist metadata
# TODO: implement in rpm later, or...?
if args.extras:
print(dist.extras)
for extra in dist.extras:
print('%%package\textras-{}'.format(extra))
print('Summary:\t{} extra for {} python package'.format(extra, dist.legacy_normalized_name))
print('Group:\t\tDevelopment/Python')
for dep in dist.requirements_for_extra(extra):
for spec in dep.specifier:
if spec.operator == '!=':
print('Conflicts:\t{} {} {}'.format(dep.legacy_normalized_name, '==', spec.version))
else:
print('Requires:\t{} {} {}'.format(dep.legacy_normalized_name, spec.operator, spec.version))
print('%%description\t{}'.format(extra))
print('{} extra for {} python package'.format(extra, dist.legacy_normalized_name))
print('%%files\t\textras-{}\n'.format(extra))
if args.conflicts:
# Should we really add conflicts for extras?
# Creating a meta package per extra with recommends on, which has
# the requires/conflicts in stead might be a better solution...
for dep in dist.requirements:
for spec in dep.specifier:
if spec.operator == '!=':
if dep.legacy_normalized_name not in py_deps:
py_deps[dep.legacy_normalized_name] = []
spec = ('==', spec.version)
if spec not in py_deps[dep.legacy_normalized_name]:
py_deps[dep.legacy_normalized_name].append(spec)
for name in sorted(py_deps):
if py_deps[name]:
# Print out versioned provides, requires, recommends, conflicts
spec_list = []
for spec in py_deps[name]:
spec_list.append(convert(name, spec[0], spec[1]))
if len(spec_list) == 1:
print(spec_list[0])
else:
# Sort spec_list so that the results can be tested easily
print('({})'.format(' with '.join(sorted(spec_list))))
else:
# Print out unversioned provides, requires, recommends, conflicts
print(name)

42
pythonname.attr Normal file
View File

@ -0,0 +1,42 @@
%__pythonname_provides() %{lua:
local python = require 'fedora.srpm.python'
-- this macro is called for each file in a package, the path being in %1
-- but we don't need to know the path, so we would get for each file: Macro %1 defined but not used within scope
-- in here, we expand %name conditionally on %1 to suppress the warning
local name = rpm.expand('%{?1:%{name}}')
local evr = rpm.expand('%{?epoch:%{epoch}:}%{version}-%{release}')
local provides = python.python_altprovides_once(name, evr)
-- provides is either an array/table or nil
-- nil means the function was already called with the same arguments:
-- either with another file in %1 or manually via %py_provides
if provides then
for i, provide in ipairs(provides) do
print(provide .. ' ')
end
end
}
%__pythonname_obsoletes() %{?rhel:%{lua:
-- On CentOS/RHEL we automatically generate Obsoletes tags in the form:
-- package python3-foo -> Obsoletes: python3.XY-foo
-- This provides a clean upgrade path between major versions of CentOS/RHEL.
-- In Fedora this is not needed as we don't ship ecosystem packages
-- for alternative Python interpreters.
local python = require 'fedora.srpm.python'
-- this macro is called for each file in a package, the path being in %1
-- but we don't need to know the path, so we would get for each file: Macro %1 defined but not used within scope
-- in here, we expand %name conditionally on %1 to suppress the warning
local name = rpm.expand('%{?1:%{name}}')
local evr = rpm.expand('%{?epoch:%{epoch}:}%{version}-%{release}')
local obsoletes = python.python_altobsoletes_once(name, evr)
-- obsoletes is either an array/table or nil
-- nil means the function was already called with the same arguments:
-- either with another file in %1 or manually via %py_provides
if obsoletes then
for i, obsolete in ipairs(obsoletes) do
print(obsolete .. ' ')
end
end
}}
%__pythonname_path ^/

7
rpminspect.yaml Normal file
View File

@ -0,0 +1,7 @@
# completely disabled inspections:
inspections:
# there is no upstream and the files are changed from time to time
addedfiles: off
changedfiles: off
filesize: off
upstream: off

1
sources Normal file
View File

@ -0,0 +1 @@
SHA512 (test-sources-2021-03-11.tar.gz) = 6f34c8151625be489a6a4d56d1fd3d39b7908bd31402c9703cb65918385320dfad35f35a32920c816fb85a829f44aaf04dc1d0654ccf512e26e87e95dbf87430

17
tests/console_script.sh Executable file
View File

@ -0,0 +1,17 @@
#!/usr/bin/bash -eux
RPMDIR=$(rpm --eval '%_topdir')/RPMS/noarch
RPMPKG="${RPMDIR}/isort-5.7.0-0.noarch.rpm"
mkdir -p $(rpm --eval '%_topdir')/SOURCES/
spectool -g -R isort.spec
for py_version in 3.6 3.7 3.8 3.9; do
rpmbuild -ba --define "python3_test_version ${py_version}" isort.spec
rpm -qp --requires ${RPMPKG} | grep "python${py_version}dist(setuptools)"
done
for py_version in 3.10 3.11; do
rpmbuild -ba --define "python3_test_version ${py_version}" isort.spec
rpm -qp --requires ${RPMPKG} | grep "python${py_version}dist(setuptools)" && exit 1 || true
done

View File

@ -0,0 +1,24 @@
appdirs==1.4.3
CacheControl==0.12.6
colorama==0.4.3
contextlib2==0.6.0.post1
distlib==0.3.0
distro==1.5.0
html5lib==1.0.1
ipaddress==1.0.23 # Only needed on 2.6 and 2.7
msgpack==1.0.0
packaging==20.3
pep517==0.8.2
progress==1.5
pyparsing==2.4.7
requests==2.23.0
certifi==2020.04.05.1
chardet==3.0.4
idna==2.9
urllib3==1.25.8
resolvelib==0.3.0
retrying==1.3.3
setuptools==44.0.0
six==1.14.0
toml==0.10.0
webencodings==0.5.1

View File

@ -0,0 +1,24 @@
Provides: bundled(python3dist(appdirs)) = 1.4.3
Provides: bundled(python3dist(cachecontrol)) = 0.12.6
Provides: bundled(python3dist(certifi)) = 2020.4.5.1
Provides: bundled(python3dist(chardet)) = 3.0.4
Provides: bundled(python3dist(colorama)) = 0.4.3
Provides: bundled(python3dist(contextlib2)) = 0.6^post1
Provides: bundled(python3dist(distlib)) = 0.3
Provides: bundled(python3dist(distro)) = 1.5
Provides: bundled(python3dist(html5lib)) = 1.0.1
Provides: bundled(python3dist(idna)) = 2.9
Provides: bundled(python3dist(ipaddress)) = 1.0.23
Provides: bundled(python3dist(msgpack)) = 1
Provides: bundled(python3dist(packaging)) = 20.3
Provides: bundled(python3dist(pep517)) = 0.8.2
Provides: bundled(python3dist(progress)) = 1.5
Provides: bundled(python3dist(pyparsing)) = 2.4.7
Provides: bundled(python3dist(requests)) = 2.23
Provides: bundled(python3dist(resolvelib)) = 0.3
Provides: bundled(python3dist(retrying)) = 1.3.3
Provides: bundled(python3dist(setuptools)) = 44
Provides: bundled(python3dist(six)) = 1.14
Provides: bundled(python3dist(toml)) = 0.10
Provides: bundled(python3dist(urllib3)) = 1.25.8
Provides: bundled(python3dist(webencodings)) = 0.5.1

View File

@ -0,0 +1,59 @@
appdirs==1.4.4
backports.shutil_get_terminal_size==1.0.0
backports.weakref==1.0.post1
click==7.1.2
click-completion==0.5.2
click-didyoumean==0.0.3
colorama==0.4.3
delegator.py==0.1.1
pexpect==4.8.0
ptyprocess==0.6.0
python-dotenv==0.10.3
first==2.0.1
iso8601==0.1.12
jinja2==2.11.2
markupsafe==1.1.1
parse==1.15.0
pathlib2==2.3.5
scandir==1.10
pipdeptree==0.13.2
pipreqs==0.4.10
docopt==0.6.2
yarg==0.1.9
pythonfinder==1.2.4
requests==2.23.0
chardet==3.0.4
idna==2.9
urllib3==1.25.9
certifi==2020.4.5.1
requirementslib==1.5.11
attrs==19.3.0
distlib==0.3.0
packaging==20.3
pyparsing==2.4.7
plette==0.2.3
tomlkit==0.5.11
shellingham==1.3.2
six==1.14.0
semver==2.9.0
toml==0.10.1
cached-property==1.5.1
vistir==0.5.2
pip-shims==0.5.2
contextlib2==0.6.0.post1
funcsigs==1.0.2
enum34==1.1.10
# yaspin==0.15.0
yaspin==0.14.3
cerberus==1.3.2
resolvelib==0.3.0
backports.functools_lru_cache==1.6.1
pep517==0.8.2
zipp==0.6.0
importlib_metadata==1.6.0
importlib-resources==1.5.0
more-itertools==5.0.0
git+https://github.com/sarugaku/passa.git@master#egg=passa
orderedmultidict==1.0.1
dparse==0.5.0
python-dateutil==2.8.1

View File

@ -0,0 +1,58 @@
Provides: bundled(python3dist(appdirs)) = 1.4.4
Provides: bundled(python3dist(attrs)) = 19.3
Provides: bundled(python3dist(backports-functools-lru-cache)) = 1.6.1
Provides: bundled(python3dist(backports-shutil-get-terminal-size)) = 1
Provides: bundled(python3dist(backports-weakref)) = 1^post1
Provides: bundled(python3dist(cached-property)) = 1.5.1
Provides: bundled(python3dist(cerberus)) = 1.3.2
Provides: bundled(python3dist(certifi)) = 2020.4.5.1
Provides: bundled(python3dist(chardet)) = 3.0.4
Provides: bundled(python3dist(click)) = 7.1.2
Provides: bundled(python3dist(click-completion)) = 0.5.2
Provides: bundled(python3dist(click-didyoumean)) = 0.0.3
Provides: bundled(python3dist(colorama)) = 0.4.3
Provides: bundled(python3dist(contextlib2)) = 0.6^post1
Provides: bundled(python3dist(delegator-py)) = 0.1.1
Provides: bundled(python3dist(distlib)) = 0.3
Provides: bundled(python3dist(docopt)) = 0.6.2
Provides: bundled(python3dist(dparse)) = 0.5
Provides: bundled(python3dist(enum34)) = 1.1.10
Provides: bundled(python3dist(first)) = 2.0.1
Provides: bundled(python3dist(funcsigs)) = 1.0.2
Provides: bundled(python3dist(idna)) = 2.9
Provides: bundled(python3dist(importlib-metadata)) = 1.6
Provides: bundled(python3dist(importlib-resources)) = 1.5
Provides: bundled(python3dist(iso8601)) = 0.1.12
Provides: bundled(python3dist(jinja2)) = 2.11.2
Provides: bundled(python3dist(markupsafe)) = 1.1.1
Provides: bundled(python3dist(more-itertools)) = 5
Provides: bundled(python3dist(orderedmultidict)) = 1.0.1
Provides: bundled(python3dist(packaging)) = 20.3
Provides: bundled(python3dist(parse)) = 1.15
Provides: bundled(python3dist(passa))
Provides: bundled(python3dist(pathlib2)) = 2.3.5
Provides: bundled(python3dist(pep517)) = 0.8.2
Provides: bundled(python3dist(pexpect)) = 4.8
Provides: bundled(python3dist(pip-shims)) = 0.5.2
Provides: bundled(python3dist(pipdeptree)) = 0.13.2
Provides: bundled(python3dist(pipreqs)) = 0.4.10
Provides: bundled(python3dist(plette)) = 0.2.3
Provides: bundled(python3dist(ptyprocess)) = 0.6
Provides: bundled(python3dist(pyparsing)) = 2.4.7
Provides: bundled(python3dist(python-dateutil)) = 2.8.1
Provides: bundled(python3dist(python-dotenv)) = 0.10.3
Provides: bundled(python3dist(pythonfinder)) = 1.2.4
Provides: bundled(python3dist(requests)) = 2.23
Provides: bundled(python3dist(requirementslib)) = 1.5.11
Provides: bundled(python3dist(resolvelib)) = 0.3
Provides: bundled(python3dist(scandir)) = 1.10
Provides: bundled(python3dist(semver)) = 2.9
Provides: bundled(python3dist(shellingham)) = 1.3.2
Provides: bundled(python3dist(six)) = 1.14
Provides: bundled(python3dist(toml)) = 0.10.1
Provides: bundled(python3dist(tomlkit)) = 0.5.11
Provides: bundled(python3dist(urllib3)) = 1.25.9
Provides: bundled(python3dist(vistir)) = 0.5.2
Provides: bundled(python3dist(yarg)) = 0.1.9
Provides: bundled(python3dist(yaspin)) = 0.14.3
Provides: bundled(python3dist(zipp)) = 0.6

View File

@ -0,0 +1,4 @@
packaging==16.8
pyparsing==2.2.1
six==1.10.0
appdirs==1.4.3

View File

@ -0,0 +1,4 @@
Provides: bundled(python3dist(appdirs)) = 1.4.3
Provides: bundled(python3dist(packaging)) = 16.8
Provides: bundled(python3dist(pyparsing)) = 2.2.1
Provides: bundled(python3dist(six)) = 1.10

View File

@ -0,0 +1,21 @@
Metadata-Version: 2.1
Name: pyreq2rpm.tests
Version: 2020.04.07.024dab0
Summary: Test package to verify conversion of dependencies from pip/python to rpm format, data taken from pyreq2rpm
Author: Tomas Orsava (author of this metapackage)
Home-page: https://github.com/gordonmessmer/pyreq2rpm
License: MIT
Description: This dist-info is mock metadata for a fictional package pyreq2rpm.tests
The important part of its contents is the requires.txt that contains
different formats of Python requirements taken from
https://github.com/gordonmessmer/pyreq2rpm, that are numbered as to be
unique. The metadata is then processed through
scripts/pythondistdeps.py and the resulting RPM requires compared to
expected results.
The version of the package contains the date when I converted the test
data from upstream to this metapackage, as well as the short hash of
the last git commit.
From the requirements I have omitted those that are incorrect, as they
crash the pythondistdeps.py script.

View File

@ -0,0 +1,104 @@
foobar0~=2.4.8
foobar1~=2.4.8.0
foobar2~=2.4.8.1
foobar4~=2.0
foobar7~=2.4.8b5
foobar8~=2.0.0b5
foobar9~=2.4.8.post1
foobar10~=2.0.post1
foobar11==2.4.8
foobar12==2.4.8.0
foobar13==2.4.8.1
foobar14==2.4.8.*
foobar15==2.0
foobar16==2
foobar17==2.*
foobar18==2.4.8b5
foobar19==2.0.0b5
foobar20==2.4.8.post1
foobar21==2.0.post1
foobar22===2.4.8
foobar23===2.4.8.0
foobar24===2.4.8.1
foobar26===2.0
foobar27===2
foobar29===2.4.8b5
foobar30===2.0.0b5
foobar31===2.4.8.post1
foobar32===2.0.post1
foobar33!=2.4.8
foobar34!=2.4.8.0
foobar35!=2.4.8.1
foobar36!=2.4.8.*
foobar37!=2.0
foobar38!=2
foobar39!=2.*
foobar40!=2.4.8b5
foobar41!=2.0.0b5
foobar42!=2.4.8.post1
foobar43!=2.0.post1
foobar44<=2.4.8
foobar45<=2.4.8.0
foobar46<=2.4.8.1
foobar47<=2.4.8.*
foobar48<=2.0
foobar49<=2
foobar50<=2.*
foobar51<=2.4.8b5
foobar52<=2.0.0b5
foobar53<=2.4.8.post1
foobar54<=2.0.post1
foobar55<2.4.8
foobar56<2.4.8.0
foobar57<2.4.8.1
foobar58<2.4.8.*
foobar59<2.0
foobar60<2
foobar61<2.*
foobar62<2.4.8b5
foobar63<2.0.0b5
foobar64<2.4.8.post1
foobar65<2.0.post1
foobar66>=2.4.8
foobar67>=2.4.8.0
foobar68>=2.4.8.1
foobar69>=2.4.8.*
foobar70>=2.0
foobar71>=2
foobar72>=2.*
foobar73>=2.4.8b5
foobar74>=2.0.0b5
foobar75>=2.4.8.post1
foobar76>=2.0.post1
foobar77>2.4.8
foobar78>2.4.8.0
foobar79>2.4.8.1
foobar80>2.4.8.*
foobar81>2.0
foobar82>2
foobar83>2.*
foobar84>2.4.8b5
foobar85>2.0.0b5
foobar86>2.4.8.post1
foobar87>2.0.post1
pyparsing0
pyparsing1>=2.0.1,!=2.0.4,!=2.1.2,!=2.1.6
babel>=1.3,!=2.0
fedora-python-nb2plots==0+unknown
hugo1==1.0.0.dev7
hugo2<=8a4
hugo3!=11.1.1b14
hugo4>11rc0
hugo5===11.1.0.post3
test___multiple__underscores==1
test_underscores==1
dnspython[DNSSEC]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,102 @@
setuptools:
wheel:
'41.6.0': ['2.7', '3.7', '3.9']
sdist:
'41.6.0': ['2.7', '3.7', '3.9', '3.10']
pip:
wheel:
'19.1.1': ['2.7', '3.7']
'20.0.2': ['3.9']
sdist:
'20.0.2': ['3.8', '3.11']
packaging:
wheel:
'19.0': ['2.7', '3.7']
'20.1': ['3.9']
attrs:
sdist:
'19.1.0': ['2.7', '3.9']
pyparsing:
wheel:
'2.4.0': ['2.7', '3.7', '3.9']
six:
wheel:
'1.12.0': ['2.7', '3.7', '3.9']
tox:
wheel:
'3.14.0': ['2.7', '3.7', '3.9']
urllib3:
sdist:
'1.25.7': ['2.7', '3.9']
zope.component:
sdist:
'4.3.0': ['2.7', '3.9']
zope.event:
wheel:
'4.2.0': ['2.7', '3.9']
zope.schema:
sdist:
'4.4.2': ['2.7', '3.9']
zope.interface:
sdist:
'5.1.0': ['3.9']
wheel:
'4.6.0': ['2.7']
lxml:
lib: lib64
wheel:
'4.4.0': ['2.7', '3.7']
scipy:
lib: lib64
wheel:
'1.2.1': ['2.7', '3.7']
numpy:
lib: lib64
wheel:
'1.16.4': ['2.7']
'1.17.4': ['3.7']
numpy-stl:
lib: lib64
sdist:
'2.11.2': ['2.7', '3.7', '3.9']
PyQt5_sip:
lib: lib64
wheel:
'4.19.19': ['3.7']
PyQtWebEngine:
lib: lib64
wheel:
'5.12.1': ['3.7', '3.9']
MarkupSafe:
lib: lib64
wheel:
'1.1.1': ['2.7', '3.7']
simplejson:
lib: lib64
sdist:
'3.16.0': ['2.7', '3.7', '3.9']
backports.range:
lib: lib64
sdist:
'3.7.2': ['2.7', '3.7', '3.9']
mistune:
sdist:
'0.8.4': ['2.7', '3.9']
astroid:
wheel:
'2.3.3': ['3.7', '3.9']
kubernetes:
wheel:
'11.0.0b2': ['2.7']
'11.0.0': ['3.9']
fsleyes:
wheel:
'0.32.3': ['3.9']
taskotron-python-versions:
wheel:
'0.1.dev6': ['3.9']
dnspython:
sdist:
'2.1.0': ['3.9']
wheel:
'2.1.0': ['3.9']

View File

@ -0,0 +1 @@
../../../update-test-sources.sh

36
tests/isort.spec Normal file
View File

@ -0,0 +1,36 @@
Name: isort
Version: 5.7.0
Release: 0
Summary: A Python package with a console_scripts entrypoint
License: MIT
Source0: %{pypi_source}
BuildArch: noarch
BuildRequires: python3-devel
BuildRequires: python3-setuptools
# Turn off Python bytecode compilation because the build would fail without Python %%{python3_test_version}
%define __brp_python_bytecompile %{nil}
%description
...
%prep
%autosetup
%build
%py3_build
%install
%py3_install
# A fake installation by a different Python version:
%if "%{python3_version}" != "%{python3_test_version}"
mv %{buildroot}%{_prefix}/lib/python%{python3_version} \
%{buildroot}%{_prefix}/lib/python%{python3_test_version}
mv %{buildroot}%{_prefix}/lib/python%{python3_test_version}/site-packages/%{name}-%{version}-py%{python3_version}.egg-info \
%{buildroot}%{_prefix}/lib/python%{python3_test_version}/site-packages/%{name}-%{version}-py%{python3_test_version}.egg-info
%endif
%files
%{_bindir}/%{name}*
%{_prefix}/lib/python%{python3_test_version}/site-packages/%{name}*

19
tests/pythonabi.sh Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/bash -eux
rpmbuild -ba pythonabi.spec
PYVER=$(rpm --eval '%python3_version')
RPMDIR=$(rpm --eval '%_topdir')/RPMS
ARCH=$(rpm --eval '%_arch')
ABI='^python(abi) = '${PYVER}'$'
rpm -qp --provides ${RPMDIR}/${ARCH}/python-interpreter-0-0.${ARCH}.rpm | grep "${ABI}"
rpm -qp --requires ${RPMDIR}/${ARCH}/python-interpreter-0-0.${ARCH}.rpm | grep "${ABI}" && exit 1 || true
rpm -qp --requires ${RPMDIR}/${ARCH}/python-arched-0-0.${ARCH}.rpm | grep "${ABI}"
rpm -qp --provides ${RPMDIR}/${ARCH}/python-arched-0-0.${ARCH}.rpm | grep "${ABI}" && exit 1 || true
rpm -qp --requires ${RPMDIR}/noarch/python-noarch-0-0.noarch.rpm | grep "${ABI}"
rpm -qp --provides ${RPMDIR}/noarch/python-noarch-0-0.noarch.rpm | grep "${ABI}" && exit 1 || true
rpm -qp --provides ${RPMDIR}/${ARCH}/python-misplaced-interpreter-0-0.${ARCH}.rpm | grep "${ABI}" && exit 1 || true
rpm -qp --requires ${RPMDIR}/noarch/python-misplaced-library-0-0.noarch.rpm | grep "${ABI}" && exit 1 || true

66
tests/pythonabi.spec Normal file
View File

@ -0,0 +1,66 @@
Name: pythonabi
Version: 0
Release: 0
Summary: ...
License: MIT
BuildRequires: python3-devel
%description
...
%install
mkdir -p %{buildroot}%{python3_sitelib}
mkdir -p %{buildroot}/opt%{python3_sitelib}
mkdir -p %{buildroot}%{python3_sitearch}
mkdir -p %{buildroot}%{_bindir}
mkdir -p %{buildroot}/opt%{_bindir}
echo "print()" > %{buildroot}%{python3_sitelib}/file.py
echo "print()" > %{buildroot}/opt%{python3_sitelib}/file.py
cp %{python3_sitearch}/../lib-dynload/cmath.*.so %{buildroot}%{python3_sitearch}/file.so
cp %{_bindir}/python%{python3_version} %{buildroot}%{_bindir}/python%{python3_version}
cp %{_bindir}/python%{python3_version} %{buildroot}/opt%{_bindir}/python%{python3_version}
%package -n python-noarch
Summary: ...
BuildArch: noarch
%description -n python-noarch
...
%files -n python-noarch
%pycached %{python3_sitelib}/file.py
%package -n python-arched
Summary: ...
%description -n python-arched
...
%files -n python-arched
%{python3_sitearch}/file.so
%package -n python-interpreter
Summary: ...
%description -n python-interpreter
...
%files -n python-interpreter
%{_bindir}/python%{python3_version}
%package -n python-misplaced-library
Summary: ...
BuildArch: noarch
%description -n python-misplaced-library
...
%files -n python-misplaced-library
%pycached /opt%{python3_sitelib}/file.py
%package -n python-misplaced-interpreter
Summary: ...
%description -n python-misplaced-interpreter
...
%files -n python-misplaced-interpreter
/opt%{_bindir}/python%{python3_version}

35
tests/pythondist.sh Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/bash -eux
X_Y=$(rpm --eval '%python3_version')
RPMDIR=$(rpm --eval '%_topdir')/RPMS/noarch
mkdir -p $(rpm --eval '%_topdir')/SOURCES/
spectool -g -R pythondist.spec
rpmbuild -ba pythondist.spec
rpm -qp --provides ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python3dist(zope\.component)'
rpm -qp --provides ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python3dist(zope-component)'
rpm -qp --provides ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python'$X_Y'dist(zope\.component)'
rpm -qp --provides ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python'$X_Y'dist(zope-component)'
rpm -qp --requires ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python'$X_Y'dist(zope-event)'
rpm -qp --requires ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python'$X_Y'dist(zope-interface)'
rpm -qp --provides ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3dist(zope\.component)' && exit 1 || true
rpm -qp --provides ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3dist(zope-component)' && exit 1 || true
rpm -qp --provides ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.7dist(zope\.component)'
rpm -qp --provides ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.7dist(zope-component)'
rpm -qp --requires ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.7dist(zope-event)'
rpm -qp --requires ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.7dist(zope-interface)'
rpm -qp --provides ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3dist(zope\.component)' && exit 1 || true
rpm -qp --provides ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3dist(zope-component)' && exit 1 || true
rpm -qp --provides ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.10dist(zope\.component)'
rpm -qp --provides ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.10dist(zope-component)'
rpm -qp --requires ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.10dist(zope-event)'
rpm -qp --requires ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.10dist(zope-interface)'

59
tests/pythondist.spec Normal file
View File

@ -0,0 +1,59 @@
Name: pythondist
Version: 4.3.0
Release: 0
Summary: ...
License: ZPLv2.1
Source0: %{pypi_source zope.component}
BuildArch: noarch
BuildRequires: python3-devel
BuildRequires: python3-setuptools
# Turn off Python bytecode compilation because the build would fail without Python 3.7/3.10
%define __brp_python_bytecompile %{nil}
%description
...
%package -n python3-zope-component
Summary: ...
%description -n python3-zope-component
...
%package -n python3.7-zope-component
Summary: ...
%description -n python3.7-zope-component
...
%package -n python3.10-zope-component
Summary: ...
%description -n python3.10-zope-component
...
%prep
%autosetup -n zope.component-%{version}
%build
%py3_build
%install
%py3_install
mkdir -p %{buildroot}/usr/lib/python3.7/site-packages
cp -a %{buildroot}%{python3_sitelib}/zope.component-%{version}-py%{python3_version}.egg-info \
%{buildroot}/usr/lib/python3.7/site-packages/zope.component-%{version}-py3.7.egg-info
mkdir -p %{buildroot}/usr/lib/python3.10/site-packages
cp -a %{buildroot}%{python3_sitelib}/zope.component-%{version}-py%{python3_version}.egg-info \
%{buildroot}/usr/lib/python3.10/site-packages/zope.component-%{version}-py3.10.egg-info
%files -n python3-zope-component
%license LICENSE.txt
%{python3_sitelib}/*
%files -n python3.7-zope-component
%license LICENSE.txt
/usr/lib/python3.7/site-packages/zope.component-%{version}-py3.7.egg-info/
%files -n python3.10-zope-component
%license LICENSE.txt
/usr/lib/python3.10/site-packages/zope.component-%{version}-py3.10.egg-info/

74
tests/pythonname.sh Executable file
View File

@ -0,0 +1,74 @@
#!/usr/bin/bash -eux
rpmbuild -ba pythonname.spec
X_Y=$(rpm --eval '%python3_version')
RPMDIR=$(rpm --eval '%_topdir')/RPMS/noarch
echo "Provides for python${X_Y}-foo"
rpm -qp --provides ${RPMDIR}/python${X_Y}-foo-0-0.noarch.rpm
rpm -qp --provides ${RPMDIR}/python${X_Y}-foo-0-0.noarch.rpm | grep -q '^python-foo = 0-0$'
rpm -qp --provides ${RPMDIR}/python${X_Y}-foo-0-0.noarch.rpm | grep -q '^python3-foo = 0-0$'
echo "Provides for python3-foo"
rpm -qp --provides ${RPMDIR}/python3-foo-0-0.noarch.rpm
rpm -qp --provides ${RPMDIR}/python3-foo-0-0.noarch.rpm | grep -q '^python-foo = 0-0$'
rpm -qp --provides ${RPMDIR}/python3-foo-0-0.noarch.rpm | grep -q '^python'${X_Y}'-foo = 0-0$'
echo "Provides for python2-foo"
rpm -qp --provides ${RPMDIR}/python2-foo-0-0.noarch.rpm
rpm -qp --provides ${RPMDIR}/python2-foo-0-0.noarch.rpm | grep -q '^python-foo = 0-0$' && exit 1 || true
echo "Provides for python-foo"
rpm -qp --provides ${RPMDIR}/python-foo-0-0.noarch.rpm
rpm -qp --provides ${RPMDIR}/python-foo-0-0.noarch.rpm | grep -q '^python2-foo = 0-0$' && exit 1 || true
echo "Provides for python3.5-foo"
rpm -qp --provides ${RPMDIR}/python3.5-foo-0-0.noarch.rpm
rpm -qp --provides ${RPMDIR}/python3.5-foo-0-0.noarch.rpm | grep -q '^python-foo = 0-0$' && exit 1 || true
rpm -qp --provides ${RPMDIR}/python3.5-foo-0-0.noarch.rpm | grep -q '^python3-foo = 0-0$' && exit 1 || true
echo "Provides for python3-python_provide"
rpm -qp --provides ${RPMDIR}/python3-python_provide-0-0.noarch.rpm
test $(rpm -qp --provides ${RPMDIR}/python3-python_provide-0-0.noarch.rpm | grep python-python_provide | wc -l) -eq 1
echo "Provides for python3-py_provides"
rpm -qp --provides ${RPMDIR}/python3-py_provides-0-0.noarch.rpm
test $(rpm -qp --provides ${RPMDIR}/python3-py_provides-0-0.noarch.rpm | grep python-py_provides | wc -l) -eq 1
echo "Obsoletes for python${X_Y}-foo"
rpm -qp --obsoletes ${RPMDIR}/python${X_Y}-foo-0-0.noarch.rpm
test $(rpm -qp --obsoletes ${RPMDIR}/python${X_Y}-foo-0-0.noarch.rpm | wc -l) -eq 0
echo "Obsoletes for python3-foo"
rpm -qp --obsoletes ${RPMDIR}/python3-foo-0-0.noarch.rpm
rpm -qp --obsoletes ${RPMDIR}/python3-foo-0-0.noarch.rpm | grep -q '^python39-foo < 0-0$'
test $(rpm -qp --obsoletes ${RPMDIR}/python3-foo-0-0.noarch.rpm | wc -l) -eq 1
echo "Obsoletes for python2-foo"
rpm -qp --obsoletes ${RPMDIR}/python2-foo-0-0.noarch.rpm
test $(rpm -qp --obsoletes ${RPMDIR}/python2-foo-0-0.noarch.rpm | wc -l) -eq 0
echo "Obsoletes for python-foo"
rpm -qp --obsoletes ${RPMDIR}/python-foo-0-0.noarch.rpm
test $(rpm -qp --obsoletes ${RPMDIR}/python-foo-0-0.noarch.rpm | wc -l) -eq 0
echo "Obsoletes for python3.5-foo"
rpm -qp --obsoletes ${RPMDIR}/python3.5-foo-0-0.noarch.rpm
test $(rpm -qp --obsoletes ${RPMDIR}/python3.5-foo-0-0.noarch.rpm | wc -l) -eq 0
echo "Obsoletes for python3-python_provide"
rpm -qp --obsoletes ${RPMDIR}/python3-python_provide-0-0.noarch.rpm
rpm -qp --obsoletes ${RPMDIR}/python3-python_provide-0-0.noarch.rpm | grep -q '^python-python_provide < 0-0$'
rpm -qp --obsoletes ${RPMDIR}/python3-python_provide-0-0.noarch.rpm | grep -q '^python39-python_provide < 0-0$'
test $(rpm -qp --obsoletes ${RPMDIR}/python3-python_provide-0-0.noarch.rpm | grep python-python_provide | wc -l) -eq 1
test $(rpm -qp --obsoletes ${RPMDIR}/python3-python_provide-0-0.noarch.rpm | grep python39-python_provide | wc -l) -eq 1
test $(rpm -qp --obsoletes ${RPMDIR}/python3-python_provide-0-0.noarch.rpm | wc -l) -eq 2
echo "Obsoletes for python3-py_provides"
rpm -qp --obsoletes ${RPMDIR}/python3-py_provides-0-0.noarch.rpm
rpm -qp --obsoletes ${RPMDIR}/python3-py_provides-0-0.noarch.rpm | grep -q '^python-py_provides < 0-0$' && exit 1 || true
rpm -qp --obsoletes ${RPMDIR}/python3-py_provides-0-0.noarch.rpm | grep -q '^python39-py_provides < 0-0$'
test $(rpm -qp --obsoletes ${RPMDIR}/python3-py_provides-0-0.noarch.rpm | grep python39-py_provides | wc -l) -eq 1
test $(rpm -qp --obsoletes ${RPMDIR}/python3-py_provides-0-0.noarch.rpm | wc -l) -eq 1

82
tests/pythonname.spec Normal file
View File

@ -0,0 +1,82 @@
Name: pythonname
Version: 0
Release: 0
Summary: ...
License: MIT
BuildArch: noarch
%description
...
%install
touch %{buildroot}/something
touch %{buildroot}/something_else
touch %{buildroot}/something_completely_different
%package -n python-foo
Summary: ...
%description -n python-foo
...
%files -n python-foo
/*
%package -n python2-foo
Summary: ...
%description -n python2-foo
...
%files -n python2-foo
/*
%package -n python3-foo
Summary: ...
%description -n python3-foo
...
%files -n python3-foo
/*
%package -n python%{python3_version}-foo
Summary: ...
%description -n python%{python3_version}-foo
...
%files -n python%{python3_version}-foo
/*
%package -n python3.5-foo
Summary: ...
%description -n python3.5-foo
...
%files -n python3.5-foo
/*
%package -n ruby-foo
Summary: ...
%description -n ruby-foo
...
%files -n ruby-foo
/*
%package -n python3-python_provide
Summary: ...
%{?python_provide:%python_provide python3-python_provide}
%description -n python3-python_provide
...
%files -n python3-python_provide
/*
%package -n python3-py_provides
Summary: ...
%py_provides python3-py_provides
%description -n python3-py_provides
...
%files -n python3-py_provides
/*

View File

@ -0,0 +1,99 @@
# Run tests using pytest, e.g. from the root directory
# $ python3 -m pytest --ignore tests/testing/ -vvv
#
# Requirements for this script:
# - Python >= 3.6
# - pytest
import pathlib
import pytest
import random
import sys
import subprocess
PYTHONBUNDLES = pathlib.Path(__file__).parent / '..' / 'pythonbundles.py'
TEST_DATA = pathlib.Path(__file__).parent / 'data' / 'scripts_pythonbundles'
def run_pythonbundles(*args, success=True):
"""
Runs pythonbundles.py with given command line arguments
Arguments:
*args: Shell arguments passed to the script
success:
- true-ish: assert return code is 0 (default)
- false-ish (excluding None): assert return code is not 0
- None: don't assert return code value
"""
cp = subprocess.run((sys.executable, PYTHONBUNDLES, *args), encoding='utf-8',
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if success:
assert cp.returncode == 0, cp.stderr
elif success is not None:
assert cp.returncode != 0, cp.stdout
return cp
projects = pytest.mark.parametrize('project', ('pkg_resources', 'pip', 'pipenv'))
@projects
def test_output_consistency(project):
cp = run_pythonbundles(TEST_DATA / f'{project}.in')
expected = (TEST_DATA / f'{project}.out').read_text()
assert cp.stdout == expected, cp.stdout
assert cp.stderr == '', cp.stderr
@pytest.mark.parametrize('namespace', ('python2dist', 'python3.11dist', 'pypy2.7dist'))
@projects
def test_namespace(project, namespace):
cp = run_pythonbundles(TEST_DATA / f'{project}.in', f'--namespace={namespace}')
expected = (TEST_DATA / f'{project}.out').read_text().replace('python3dist', namespace)
assert cp.stdout == expected, cp.stdout
assert cp.stderr == '', cp.stderr
@projects
def test_compare_with_identical(project):
expected = (TEST_DATA / f'{project}.out').read_text()
cp = run_pythonbundles(TEST_DATA / f'{project}.in', '--compare-with', expected)
assert cp.stdout == '', cp.stdout
assert cp.stderr == '', cp.stderr
@projects
def test_compare_with_shuffled(project):
expected = (TEST_DATA / f'{project}.out').read_text()
lines = expected.splitlines()
# some extra whitespace and comments
lines[0] = f' {lines[0]} '
lines.extend([''] * 3)
lines.append('# this is a comment on a single line')
random.shuffle(lines)
shuffled = '\n'.join(lines)
cp = run_pythonbundles(TEST_DATA / f'{project}.in', '--compare-with', shuffled)
assert cp.stdout == '', cp.stdout
assert cp.stderr == '', cp.stderr
@projects
def test_compare_with_missing(project):
expected = (TEST_DATA / f'{project}.out').read_text()
lines = expected.splitlines()
missing = lines[0]
del lines[0]
shorter = '\n'.join(lines)
cp = run_pythonbundles(TEST_DATA / f'{project}.in', '--compare-with', shorter, success=False)
assert cp.stdout == '', cp.stdout
assert cp.stderr == f'Missing expected provides:\n - {missing}\n', cp.stderr
@projects
def test_compare_with_unexpected(project):
expected = (TEST_DATA / f'{project}.out').read_text()
unexpected = 'Provides: bundled(python3dist(brainfuck)) = 6.6.6'
longer = f'{expected}{unexpected}\n'
cp = run_pythonbundles(TEST_DATA / f'{project}.in', '--compare-with', longer, success=False)
assert cp.stdout == '', cp.stdout
assert cp.stderr == f'Redundant unexpected provides:\n + {unexpected}\n', cp.stderr

View File

@ -0,0 +1,257 @@
# Run tests using pytest, e.g. from the root directory
# $ python3 -m pytest --ignore tests/testing/ -s -vvv
#
# If there are any breakags, the best way to see differences is using a diff:
# $ diff tests/data/scripts_pythondistdeps/test-data.yaml <(python3 tests/test_scripts_pythondistdeps.py)
#
# - Test cases and expected results are saved in test-data.yaml inside
# TEST_DATA_PATH (currently ./data/scripts_pythondistdeps/)
# - To regenerate test-data.yaml file with the current results of
# pythondistdeps.py for each test configuration, execute this test file
# directly and results will be on stdout
# $ python3 test_scripts_pythondistdeps.py
#
# To add new test-data, add them to the test-requires.yaml: they will be
# downloaded automatically. And then add the resulting dist-info/egg-info paths
# into test-data.yaml under whichever requires/provides configurations you want
# to test
# - To find all dist-info/egg-info directories in the test-data directory,
# run inside test-data:
# $ find . -type d -regex ".*\(dist-info\|egg-info\)" | sort
#
# Requirements for this script:
# - Python >= 3.6
# - pip >= 20.0.1
# - setuptools
# - pytest
# - pyyaml
# - wheel
from pathlib import Path
import pytest
import shlex
import shutil
import subprocess
import sys
import tempfile
import yaml
PYTHONDISTDEPS_PATH = Path(__file__).parent / '..' / 'pythondistdeps.py'
TEST_DATA_PATH = Path(__file__).parent / 'data' / 'scripts_pythondistdeps'
def run_pythondistdeps(provides_params, requires_params, dist_egg_info_path, expect_failure=False):
"""Runs pythondistdeps.py on `dits_egg_info_path` with given
provides and requires parameters and returns a dict with generated provides and requires"""
info_path = TEST_DATA_PATH / dist_egg_info_path
files = '\n'.join(map(str, info_path.iterdir()))
provides = subprocess.run((sys.executable, PYTHONDISTDEPS_PATH, *shlex.split(provides_params)),
input=files, capture_output=True, check=False, encoding="utf-8")
requires = subprocess.run((sys.executable, PYTHONDISTDEPS_PATH, *shlex.split(requires_params)),
input=files, capture_output=True, check=False, encoding="utf-8")
if expect_failure:
if provides.returncode == 0 or requires.returncode == 0:
raise RuntimeError(f"pythondistdeps.py did not exit with a non-zero code as expected.\n"
f"Used parameters: ({provides_params}, {requires_params}, {dist_egg_info_path})")
stdout = {"provides": provides.stdout.strip(), "requires": requires.stdout.strip()}
stderr = {"provides": provides.stderr.strip(), "requires": requires.stderr.strip()}
return {"stderr": stderr, "stdout": stdout}
else:
if provides.returncode != 0 or requires.returncode != 0:
raise RuntimeError(f"pythondistdeps.py unexpectedly exited with a non-zero code.\n"
f"Used parameters: ({provides_params}, {requires_params}, {dist_egg_info_path})")
return {"provides": provides.stdout.strip(), "requires": requires.stdout.strip()}
def load_test_data():
"""Reads the test-data.yaml and loads the test data into a dict."""
with TEST_DATA_PATH.joinpath('test-data.yaml').open() as file:
return yaml.safe_load(file)
def generate_test_cases(test_data):
"""Goes through the test data dict and yields test cases.
Test case is a tuple of 4 elements:
- provides parameters
- requires parameters
- path to the dist-info/egg-info directory inside test-data
- dict with expected results ("requires" and "provides")"""
for requires_params in test_data:
for provides_params in test_data[requires_params]:
for dist_egg_info_path in test_data[requires_params][provides_params]:
expected = test_data[requires_params][provides_params][dist_egg_info_path]
yield (provides_params, requires_params, dist_egg_info_path, expected)
def check_and_install_test_data():
"""Checks if the appropriate metadata are present in TEST_DATA_PATH, and if
not, downloads them through pip from PyPI."""
with TEST_DATA_PATH.joinpath('test-requires.yaml').open() as file:
test_requires = yaml.safe_load(file)
downloaded_anything = False
for package in test_requires:
# To be as close to the real environment, we want some packages saved in /usr/lib64 instead of /usr/lib,
# for these we explicitly set lib64 as a parameter, and by default we use /usr/lib.
lib = test_requires[package].pop("lib", "lib")
# type is either `wheel` or `sdist`
for type in test_requires[package]:
for pkg_version in test_requires[package][type]:
for py_version in test_requires[package][type][pkg_version]:
py_version_nodots = py_version.replace(".", "")
package_underscores = package.replace("-", "_")
suffix = ".egg-info" if type == "sdist" else ".dist-info"
pre_suffix = f"-py{py_version}" if type == "sdist" else ""
install_path = TEST_DATA_PATH / "usr" / lib / f"python{py_version}" \
/ "site-packages" / f"{package_underscores}-{pkg_version}{pre_suffix}{suffix}"
if install_path.exists():
continue
# If this is the first package we're downloading,
# display what's happening
if not downloaded_anything:
print("=====================")
print("Downloading test data")
print("=====================\n")
downloaded_anything = True
# We use a temporary directory to unpack/install the
# package to, and then we move only the metadata to the
# final location
with tempfile.TemporaryDirectory() as temp_dir:
import runpy
backup_argv = sys.argv[:]
if type == "wheel":
from pkg_resources import parse_version
abi = f"cp{py_version_nodots}"
# The "m" was removed from the abi flag in Python version 3.8
if parse_version(py_version) < parse_version('3.8'):
abi += "m"
# Install = download and unpack wheel into our
# temporary directory
sys.argv[1:] = ["install", "--no-deps",
"--only-binary", ":all:",
"--platform", "manylinux1_x86_64",
"--python-version", py_version,
"--implementation", "cp",
"--abi", abi,
"--target", temp_dir,
"--no-build-isolation",
f"{package}=={pkg_version}"]
else:
# Download sdist that we'll unpack later
sys.argv[1:] = ["download", "--no-deps",
"--no-binary", ":all:",
"--dest", temp_dir,
"--no-build-isolation",
f"{package}=={pkg_version}"]
try:
# run_module() alters sys.modules and sys.argv, but restores them at exit
runpy.run_module("pip", run_name="__main__", alter_sys=True)
except SystemExit as exc:
pass
finally:
sys.argv[:] = backup_argv
temp_path = Path(temp_dir)
if type == "sdist":
# Wheel were already unpacked by pip, sdists we
# have to unpack ourselves
sdist_path = next(temp_path.glob(f"{package}-{pkg_version}.*"))
if sdist_path.suffix == ".zip":
import zipfile
archive = zipfile.ZipFile(sdist_path)
else:
import tarfile
archive = tarfile.open(sdist_path)
archive.extractall(temp_path)
try:
info_path = next(temp_path.glob(f"**/*{suffix}"))
# Let's check the wheel metadata has the
# expected directory name. We don't check for
# egg-info metadata, because we're pulling them
# from sdists where they don't have the proper
# directory name
if type == "wheel":
if info_path.name != install_path.name:
print("\nWarning: wheel metadata have unexpected directory name.\n"
f"Expected: {install_path.name}\n"
f"Actual: {info_path.name}\n"
f"Info: package '{package}', version '{pkg_version}'"
f" for Python {py_version}\n"
f"Possible resolution: Specify the package version with"
f" trailing zeros in test-requires.yaml", file=sys.stderr)
shutil.move(info_path, install_path)
relative_path = install_path.relative_to(TEST_DATA_PATH)
print(f"\nDownloaded metadata to '{relative_path}'" \
f" inside test-data directory.\n")
except StopIteration:
# temp_path.glob() did not find any file and
# thus there's been some problem
sys.exit(f"Problem occured while getting dist-info/egg-info"
f" for package '{package}', version '{pkg_version}'"
f" for Python {py_version}")
if downloaded_anything:
print("\n==============================")
print("Finished downloading test data")
print("==============================")
@pytest.fixture(scope="session", autouse=True)
def fixture_check_and_install_test_data():
"""Wrapper fixture, because a fixture can't be called as a function."""
check_and_install_test_data()
@pytest.mark.parametrize("provides_params, requires_params, dist_egg_info_path, expected",
generate_test_cases(load_test_data()))
def test_pythondistdeps(provides_params, requires_params, dist_egg_info_path, expected):
"""Runs pythondistdeps with the given parameters and dist-info/egg-info
path, compares the results with the expected results"""
expect_failure = "stderr" in expected
assert expected == run_pythondistdeps(provides_params, requires_params, dist_egg_info_path, expect_failure)
if __name__ == "__main__":
"""If the script is called directly, we check and install test data if needed,
we look up all the test configurations in test-data.yaml, run
pythondistdeps for each, save the results and print the resulting YAML file
with the updated results."""
check_and_install_test_data()
# Set YAML dump style to block style
def str_presenter(dumper, data):
if len(data.splitlines()) > 1: # check for multiline string
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
return dumper.represent_scalar('tag:yaml.org,2002:str', data)
yaml.add_representer(str, str_presenter)
# Run pythondistdeps for each test configuration
test_data = load_test_data()
for provides_params, requires_params, dist_egg_info_path, expected in generate_test_cases(test_data):
# Print a dot to stderr for each test run to keep user informed about progress
print(".", end="", flush=True, file=sys.stderr)
expect_failure = "stderr" in test_data[requires_params][provides_params][dist_egg_info_path]
test_data[requires_params][provides_params][dist_egg_info_path] = \
run_pythondistdeps(provides_params, requires_params, dist_egg_info_path, expect_failure)
print(yaml.dump(test_data, indent=4))

48
tests/tests.yml Normal file
View File

@ -0,0 +1,48 @@
---
- hosts: localhost
tags:
- classic
tasks:
- dnf:
name: "*"
state: latest
- hosts: localhost
tags:
- classic
pre_tasks:
- import_role:
name: standard-test-source
vars:
fetch_only: True
roles:
- role: standard-test-basic
tests:
- pythonabi:
dir: .
run: ./pythonabi.sh
- pythonname:
dir: .
run: ./pythonname.sh
- pythondist:
dir: .
run: ./pythondist.sh
- console_script:
dir: .
run: ./console_script.sh
- prepare-test-data:
dir: .
run: tar -xvf test-sources-*.tar.gz -C ./tests/data/scripts_pythondistdeps/
- pytest:
dir: ./tests
# Use update-test-sources.sh to update the test data
run: python3 -m pytest --capture=no -vvv
required_packages:
- rpm-build
- rpmdevtools
- python3-devel
- python3-pip
- python3-pytest
- python3-pyyaml
- python3-setuptools
- python3-wheel

20
update-test-sources.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
#
# Requirements:
# - pip >= 20.0.1
# - poetry # Due to bug: https://github.com/pypa/pip/issues/9701
#
# First prune old test data
rm -rf ./tests/data/scripts_pythondistdeps/usr
# First run the test suite, it will download the test-data again
python3 -m pytest --capture=no -vvv
# Archive the test data into a file with today's date
archive=test-sources-$(date +%Y-%m-%d).tar.gz
tar -zcvf ${archive} -C ./tests/data/scripts_pythondistdeps/ usr
# Now manually run:
# $ fedpkg new-sources ${archive}