RHEL 9.0.0 Alpha bootstrap

The content of this branch was automatically imported from Fedora ELN
with the following as its source:
https://src.fedoraproject.org/rpms/pyproject-rpm-macros#6a44fe2d7a819cdcb55159d19c6878419871b41f
This commit is contained in:
Troy Dawson 2020-10-14 16:43:13 -07:00
parent 50a250ddcf
commit 89b6866906
38 changed files with 10879 additions and 0 deletions

1
.gitignore vendored
View File

@ -0,0 +1 @@
__pycache__/

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
Copyright 2019 pyproject-rpm-macros contributors
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.

296
README.md Normal file
View File

@ -0,0 +1,296 @@
pyproject RPM macros
====================
These macros allow projects that follow the Python [packaging specifications]
to be packaged as RPMs.
They are still *provisional*: we can make non-backwards-compatible changes to
the API.
Please subscribe to Fedora's [python-devel list] if you use the macros.
They work for:
* traditional Setuptools-based projects that use the `setup.py` file,
* newer Setuptools-based projects that have a `setup.cfg` file,
* general Python projects that use the [PEP 517] `pyproject.toml` file (which allows using any build system, such as setuptools, flit or poetry).
These macros replace `%py3_build` and `%py3_install`, which only work with `setup.py`.
[packaging specifications]: https://packaging.python.org/specifications/
[python-devel list]: https://lists.fedoraproject.org/archives/list/python-devel@lists.fedoraproject.org/
Usage
-----
To use these macros, first BuildRequire them:
BuildRequires: pyproject-rpm-macros
Also BuildRequire the devel package for the Python you are building against.
In Fedora, that's `python3-devel`.
(In the future, we plan to make `python3-devel` itself require
`pyproject-rpm-macros`.)
Next, you need to generate more build dependencies (of your projects and
the macros themselves) by running `%pyproject_buildrequires` in the
`%generate_buildrequires` section:
%generate_buildrequires
%pyproject_buildrequires
This will add build dependencies according to [PEP 517] and [PEP 518].
To also add run-time and test-time dependencies, see the section below.
If you need more dependencies, such as non-Python libraries, BuildRequire
them manually.
Note that `%generate_buildrequires` may produce error messages `(exit 11)` in
the build log. This is expected behavior of BuildRequires generators; see
[the Fedora change] for details.
[the Fedora change]: https://fedoraproject.org/wiki/Changes/DynamicBuildRequires
Then, build a wheel in `%build` with `%pyproject_wheel`:
%build
%pyproject_wheel
And install the wheel in `%install` with `%pyproject_install`:
%install
%pyproject_install
`%pyproject_install` installs all wheels in `$PWD/pyproject-wheeldir/`.
Adding run-time and test-time dependencies
------------------------------------------
To run tests in the `%check` section, the package's runtime dependencies
often need to also be included as build requirements.
This can be done using the `-r` flag:
%generate_buildrequires
%pyproject_buildrequires -r
For this to work, the project's build system must support the
[`prepare-metadata-for-build-wheel` hook](https://www.python.org/dev/peps/pep-0517/#prepare-metadata-for-build-wheel).
The popular buildsystems (setuptools, flit, poetry) do support it.
For projects that specify test requirements using an [`extra`
provide](https://packaging.python.org/specifications/core-metadata/#provides-extra-multiple-use),
these can be added using the `-x` flag.
Multiple extras can be supplied by repeating the flag or as a comma separated list.
For example, if upstream suggests installing test dependencies with
`pip install mypackage[testing]`, the test deps would be generated by:
%generate_buildrequires
%pyproject_buildrequires -x testing
For projects that specify test requirements in their [tox] configuration,
these can be added using the `-t` flag (default tox environment)
or the `-e` flag followed by the tox environment.
The default tox environment (such as `py37` assuming the Fedora's Python version is 3.7)
is available in the `%{toxenv}` macro.
For example, if upstream suggests running the tests on Python 3.7 with `tox -e py37`,
the test deps would be generated by:
%generate_buildrequires
%pyproject_buildrequires -t
If upstream uses a custom derived environment, such as `py37-unit`, use:
%pyproject_buildrequires -e %{toxenv}-unit
Or specify more environments if needed:
%pyproject_buildrequires -e %{toxenv}-unit,%{toxenv}-integration
The `-e` option redefines `%{toxenv}` for further reuse.
Use `%{default_toxenv}` to get the default value.
The `-t`/`-e` option uses [tox-current-env]'s `--print-deps-to-file` behind the scenes.
Note that both `-x` and `-t` imply `-r`,
because runtime dependencies are always required for testing.
[tox]: https://tox.readthedocs.io/
[tox-current-env]: https://github.com/fedora-python/tox-current-env/
Running tox based tests
-----------------------
In case you want to run the tests as specified in [tox] configuration,
you must use `%pyproject_buildrequires` with `-t` or `-e` as explained above.
Then, use the `%tox` macro in `%check`:
%check
%tox
The macro:
- Always prepends `$PATH` with `%{buildroot}%{_bindir}`
- If not defined, sets `$PYTHONPATH` to `%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}`
- If not defined, sets `$TOX_TESTENV_PASSENV` to `*`
- Runs `tox` with `-q` (quiet), `--recreate` and `--current-env` (from [tox-current-env]) flags
- Implicitly uses the tox environment name stored in `%{toxenv}` - as overridden by `%pyproject_buildrequires -e`
By using the `-e` flag, you can use a different tox environment(s):
%check
%tox
%if %{with integration_test}
%tox -e %{default_toxenv}-integration
%endif
If you wish to provide custom `tox` flags or arguments, add them after `--`:
%tox -- --flag-for-tox
If you wish to pass custom `posargs` to tox, use another `--`:
%tox -- --flag-for-tox -- --flag-for-posargs
Or (note the two sequential `--`s):
%tox -- -- --flag-for-posargs
Generating the %files section
-----------------------------
To generate the list of files in the `%files` section, you can use `%pyproject_save_files` after the `%pyproject_install` macro.
It takes toplevel module names (i.e. the names used with `import` in Python) and stores paths for those modules and metadata for the package (dist-info directory) to a file stored at `%{pyproject_files}`.
For example, if a package provides the modules `requests` and `_requests`, write:
%install
%pyproject_install
%pyproject_save_files requests _requests
To add listed files to the `%files` section, use `%files -f %{pyproject_files}`.
Note that you still need to add any documentation and license manually (for now).
%files -n python3-requests -f %{pyproject_files}
%doc README.rst
%license LICENSE
You can use globs in the module names if listing them explicitly would be too tedious:
%install
%pyproject_install
%pyproject_save_files '*requests'
In fully automated environments, you can use the `*` glob to include all modules (put it in single quotes to prevent Shell from expanding it). In Fedora however, you should always use a more specific glob to avoid accidentally packaging unwanted files (for example, a top level module named `test`).
Speaking about automated environments, some files cannot be classified with `%pyproject_save_files`, but it is possible to list all unclassified files by adding a special `+auto` argument.
%install
%pyproject_install
%pyproject_save_files '*' +auto
%files -n python3-requests -f %{pyproject_files}
However, in Fedora packages, always list executables explicitly to avoid unintended collisions with other packages or accidental missing executables:
%install
%pyproject_install
%pyproject_save_files requests _requests
%files -n python3-requests -f %{pyproject_files}
%doc README.rst
%license LICENSE
%{_bindir}/downloader
`%pyproject_save_files` also automatically recognizes language (`*.mo`) files and marks them with `%lang` macro and appropriate language code.
Note that RPM might warn about such files listed twice:
warning: File listed twice: /usr/lib/python3.9/site-packages/django/conf/locale/af/LC_MESSAGES/django.mo
The warning is harmless.
Generating Extras subpackages
-----------------------------
The `%pyproject_extras_subpkg` macro generates simple subpackage(s)
for Python extras.
The macro should be placed after the base package's `%description` to avoid
issues in building the SRPM.
For example, if the `requests` project's metadata defines the extras
`security` and `socks`, the following invocation will generate the subpackage
`python3-requests+security` that provides `python3dist(requests[security])`,
and a similar one for `socks`.
%pyproject_extras_subpkg -n python3-requests security socks
The macro works like `%python_extras_subpkg`,
except the `-i`/`-f`/`-F` arguments are optional and discouraged.
A filelist written by `%pyproject_install` is used by default.
For more information on `%python_extras_subpkg`, see the [Fedora change].
[Fedora change]: https://fedoraproject.org/wiki/Changes/PythonExtras
These arguments are still required:
* -n: name of the “base” package (e.g. python3-requests)
* Positional arguments: the extra name(s).
Multiple subpackages are generated when multiple names are provided.
The macro does nothing on Fedora 32 and lower, as automation around
extras was only added in f33.
Limitations
-----------
`%pyproject_install` changes shebang lines of every Python script in `%{buildroot}%{_bindir}` to `#!%{__python3} %{py3_shbang_opt}` (`#!/usr/bin/python3 -s`).
Existing Python flags in shebangs are preserved.
For example `#!/usr/bin/python3 -Ru` will be updated to `#!/usr/bin/python3 -sRu`.
Sometimes, this can interfere with tests that run such scripts directly by name,
because in tests we usually rely on `PYTHONPATH` (and `-s` ignores that).
Would this behavior be undesired for any reason,
undefine `%{py3_shbang_opt}` to turn it off.
Some valid Python version specifiers are not supported.
[PEP 517]: https://www.python.org/dev/peps/pep-0517/
[PEP 518]: https://www.python.org/dev/peps/pep-0518/
Testing the macros
------------------
This repository has two kinds of tests.
First, there is RPM `%check` section, run when building the `python-rpm-macros`
package.
Then there are CI tests.
There is currently [no way to run Fedora CI tests locally][ci-rfe],
but you can do what the tests do manually using mock.
For each `$PKG.spec` in `tests/`:
- clean your mock environment:
mock -r fedora-rawhide-x86_64 clean
- install the version of `python-rpm-macros` you're testing, e.g.:
mock -r fedora-rawhide-x86_64 install .../python-rpm-macros-*.noarch.rpm
- download the sources:
spectool -g -R $PKG.spec
- build a SRPM:
rpmbuild -bs $PKG.spec
- build in mock, using the path from the command above as `$SRPM`:
mock -r fedora-rawhide-x86_64 -n -N $SRPM
[ci-rfe]: https://pagure.io/fedora-ci/general/issue/4

112
macros.pyproject Normal file
View File

@ -0,0 +1,112 @@
# This is a directory where wheels are stored and installed from, relative to PWD
%_pyproject_wheeldir pyproject-wheeldir
# This is a directory used as TMPDIR, where pip copies sources to and builds from, relative to PWD
# For proper debugsource packages, we create TMPDIR within PWD
# See https://github.com/pypa/pip/issues/7555#issuecomment-595180864
#
# This will be used in debugsource package paths (applies to extension modules only)
# NB: pytest collects tests from here if not hidden
# https://docs.pytest.org/en/latest/reference.html#confval-norecursedirs
%_pyproject_builddir .pyproject-builddir
%pyproject_files %{_builddir}/pyproject-files
%pyproject_ghost_distinfo %{_builddir}/pyproject-ghost-distinfo
%pyproject_record %{_builddir}/pyproject-record
%pyproject_wheel() %{expand:\\\
export TMPDIR="${PWD}/%{_pyproject_builddir}"
mkdir -p "${TMPDIR}"
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}" \\\
%{__python3} -m pip wheel --wheel-dir %{_pyproject_wheeldir} --no-deps --use-pep517 --no-build-isolation --disable-pip-version-check --no-clean --progress-bar off --verbose .
}
%pyproject_install() %{expand:\\\
specifier=$(ls %{_pyproject_wheeldir}/*.whl | xargs basename --multiple | sed -E 's/([^-]+)-([^-]+)-.+\\\.whl/\\\1==\\\2/')
export TMPDIR="${PWD}/%{_pyproject_builddir}"
%{__python3} -m pip install --root %{buildroot} --no-deps --disable-pip-version-check --progress-bar off --verbose --ignore-installed --no-warn-script-location --no-index --no-cache-dir --find-links %{_pyproject_wheeldir} $specifier
if [ -d %{buildroot}%{_bindir} ]; then
%py3_shebang_fix %{buildroot}%{_bindir}/*
rm -rfv %{buildroot}%{_bindir}/__pycache__
fi
rm -f %{pyproject_ghost_distinfo}
site_dirs=()
# Process %%{python3_sitelib} if exists
if [ -d %{buildroot}%{python3_sitelib} ]; then
site_dirs+=( "%{python3_sitelib}" )
fi
# Process %%{python3_sitearch} if exists and does not equal to %%{python3_sitelib}
if [ %{buildroot}%{python3_sitearch} != %{buildroot}%{python3_sitelib} ] && [ -d %{buildroot}%{python3_sitearch} ]; then
site_dirs+=( "%{python3_sitearch}" )
fi
# Process all *.dist-info dirs in sitelib/sitearch
for site_dir in ${site_dirs[@]}; do
for distinfo in %{buildroot}$site_dir/*.dist-info; do
echo "%ghost ${distinfo#%{buildroot}}" >> %{pyproject_ghost_distinfo}
sed -i 's/pip/rpm/' ${distinfo}/INSTALLER
PYTHONPATH=%{_rpmconfigdir}/redhat \\
%{__python3} -B %{_rpmconfigdir}/redhat/pyproject_preprocess_record.py \\
--buildroot %{buildroot} --record ${distinfo}/RECORD --output %{pyproject_record}
rm -fv ${distinfo}/RECORD
rm -fv ${distinfo}/REQUESTED
done
done
lines=$(wc -l %{pyproject_ghost_distinfo} | cut -f1 -d" ")
if [ $lines -ne 1 ]; then
echo -e "\\n\\nWARNING: %%%%pyproject_extras_subpkg won't work without explicit -i or -F, found $lines dist-info directories.\\n\\n" >/dev/stderr
rm %{pyproject_ghost_distinfo} # any attempt to use this will fail
fi
}
# Note: the three times nested questionmarked -i -f -F pattern means: If none of those options was used -- in that case, we inject our own -f
%pyproject_extras_subpkg(n:i:f:F) %{expand:%{?python_extras_subpkg:%{python_extras_subpkg%{?!-i:%{?!-f:%{?!-F: -f %{pyproject_ghost_distinfo}}}} %**}}}
%pyproject_save_files() %{expand:\\\
%{__python3} %{_rpmconfigdir}/redhat/pyproject_save_files.py \\
--output "%{pyproject_files}" \\
--buildroot "%{buildroot}" \\
--sitelib "%{python3_sitelib}" \\
--sitearch "%{python3_sitearch}" \\
--python-version "%{python3_version}" \\
--pyproject-record "%{pyproject_record}" \\
%{*}
}
%default_toxenv py%{python3_version_nodots}
%toxenv %{default_toxenv}
%pyproject_buildrequires(rxte:) %{expand:\\\
%{-e:%{expand:%global toxenv %{-e*}}}
echo 'python%{python3_pkgversion}-devel'
echo 'python%{python3_pkgversion}dist(pip) >= 19'
echo 'python%{python3_pkgversion}dist(packaging)'
# The first part is for cases when %%{python3_version_nodots} is not yet available
if [ ! -z "%{?python3_version_nodots}" ] && [ %{python3_version_nodots} -lt 38 ]; then
echo 'python%{python3_pkgversion}dist(importlib-metadata)'
fi
# Check if we can generate dependencies on Python extras
if [ "%{py_dist_name []}" == "[]" ]; then
extras_flag=%{?!_python_no_extras_requires:--generate-extras}
else
extras_flag=
fi
# setuptools assumes no pre-existing dist-info
rm -rfv *.dist-info/ >&2
if [ -f %{__python3} ]; then
RPM_TOXENV="%{toxenv}" HOSTNAME="rpmbuild" %{__python3} -s %{_rpmconfigdir}/redhat/pyproject_buildrequires.py $extras_flag --python3_pkgversion %{python3_pkgversion} %{?**}
fi
}
%tox(e:) %{expand:\\\
TOX_TESTENV_PASSENV="${TOX_TESTENV_PASSENV:-*}" \\
PATH="%{buildroot}%{_bindir}:$PATH" \\
PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}" \\
HOSTNAME="rpmbuild" \\
%{__python3} -m tox --current-env -q --recreate -e "%{-e:%{-e*}}%{!-e:%{toxenv}}" %{?*}
}

View File

@ -0,0 +1,8 @@
# this dependency is on purpose
addFilter(r'devel-dependency python3-devel')
# RPM macros, this is expected
addFilter(r'only-non-binary-in-usr-lib')
# spelling errors
addFilter(r'spelling-error .* en_US (toml|setuptools) ')

206
pyproject-rpm-macros.spec Normal file
View File

@ -0,0 +1,206 @@
Name: pyproject-rpm-macros
Summary: RPM macros for PEP 517 Python packages
License: MIT
%bcond_without tests
# Keep the version at zero and increment only release
Version: 0
Release: 31%{?dist}
# Macro files
Source001: macros.pyproject
# Implementation files
Source101: pyproject_buildrequires.py
Source102: pyproject_save_files.py
Source103: pyproject_convert.py
Source104: pyproject_preprocess_record.py
# Tests
Source201: test_pyproject_buildrequires.py
Source202: test_pyproject_save_files.py
# Test data
Source301: pyproject_buildrequires_testcases.yaml
Source302: pyproject_save_files_test_data.yaml
Source303: test_RECORD
# Metadata
Source901: README.md
Source902: LICENSE
URL: https://src.fedoraproject.org/rpms/pyproject-rpm-macros
BuildArch: noarch
%if %{with tests}
BuildRequires: python3dist(pytest)
BuildRequires: python3dist(pyyaml)
BuildRequires: python3dist(packaging)
%if 0%{fedora} < 32
# The %%if should not be needed, it works around:
# https://github.com/rpm-software-management/mock/issues/336
BuildRequires: (python3dist(importlib-metadata) if python3 < 3.8)
%endif
BuildRequires: python3dist(pip)
BuildRequires: python3dist(setuptools)
BuildRequires: python3dist(toml)
BuildRequires: python3dist(tox-current-env) >= 0.0.3
BuildRequires: python3dist(wheel)
%endif
%description
This is a provisional implementation of pyproject RPM macros for Fedora 30+.
These macros are useful for packaging Python projects that use the PEP 517
pyproject.toml file, which specifies the package's build dependencies
(including the build system, such as setuptools, flit or poetry).
%prep
# Not strictly necessary but allows working on file names instead
# of source numbers in install section
%setup -c -T
cp -p %{sources} .
%build
# nothing to do, sources are not buildable
%install
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/
install -m 644 pyproject_preprocess_record.py %{buildroot}%{_rpmconfigdir}/redhat/
%if %{with tests}
%check
export HOSTNAME="rpmbuild" # to speedup tox in network-less mock, see rhbz#1856356
%{python3} -m pytest -vv --doctest-modules
%endif
%files
%{_rpmmacrodir}/macros.pyproject
%{_rpmconfigdir}/redhat/pyproject_buildrequires.py
%{_rpmconfigdir}/redhat/pyproject_convert.py
%{_rpmconfigdir}/redhat/pyproject_save_files.py
%{_rpmconfigdir}/redhat/pyproject_preprocess_record.py
%doc README.md
%license LICENSE
%changelog
* Mon Oct 05 2020 Miro Hrončok <mhroncok@redhat.com> - 0-31
- Support PEP 517 list based backend-path
* Tue Sep 29 2020 Lumír Balhar <lbalhar@redhat.com> - 0-30
- Process RECORD files in %%pyproject_install and remove them
- Support the extras configuration option of tox in %%pyproject_buildrequires -t
- Support multiple -x options for %%pyproject_buildrequires
- Fixes: rhbz#1877977
- Fixes: rhbz#1877978
* Wed Sep 23 2020 Miro Hrončok <mhroncok@redhat.com> - 0-29
- Check the requirements after installing "requires_for_build_wheel"
- If not checked, installing runtime requirements might fail
* Tue 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 04 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
- Remove a no longer useful warning for unrecognized files in %%pyproject_save_files
* Mon Aug 24 2020 Tomas Hrnciar <thrnciar@redhat.com> - 0-26
- Implement automatic detection of %%lang files in %%pyproject_save_files
and mark them with %%lang in filelist
* Fri Aug 14 2020 Miro Hrončok <mhroncok@redhat.com> - 0-25
- Handle Python Extras in %%pyproject_buildrequires on Fedora 33+
* Tue Aug 11 2020 Miro Hrončok <mhroncok@redhat.com> - 0-24
- Allow multiple, comma-separated extras in %%pyproject_buildrequires -x
* Mon Aug 10 2020 Lumír Balhar <lbalhar@redhat.com> - 0-23
- Make macros more universal for alternative Python stacks
* Thu Aug 06 2020 Tomas Hrnciar <thrnciar@redhat.com> - 0-22
- Change %%pyproject_save_files +bindir argument to +auto
to list all unclassified files in filelist
* Tue Aug 04 2020 Miro Hrončok <mhroncok@redhat.com> - 0-21
- Actually implement %%pyproject_extras_subpkg
* Wed Jul 29 2020 Miro Hrončok <mhroncok@redhat.com> - 0-20
- Implement %%pyproject_extras_subpkg
* Tue Jul 28 2020 Fedora Release Engineering <releng@fedoraproject.org> - 0-19
- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild
* Thu Jul 16 2020 Miro Hrončok <mhroncok@redhat.com> - 0-18
- %%pyproject_buildrequires -x (extras requires for tests) now implies -r
(runtime requires) instead of erroring without it for better UX.
* Wed Jul 15 2020 Miro Hrončok <mhroncok@redhat.com> - 0-17
- Set HOSTNAME to prevent tox 3.17+ from a DNS query
- Fixes rhbz#1856356
* Fri Jun 19 2020 Miro Hrončok <mhroncok@redhat.com> - 0-16
- Switch from upstream deprecated pytoml to toml
* Thu May 07 2020 Tomas Hrnciar <thrnciar@redhat.com> - 0-15
- Adapt %%pyproject_install not to create a PEP 610 direct_url.json file
* Wed Apr 15 2020 Patrik Kopkan <pkopkan@redhat.com> - 0-14
- Add %%pyproject_save_file macro for generating file section
- Handle extracting debuginfo from extension modules (#1806625)
* Mon Mar 02 2020 Miro Hrončok <mhroncok@redhat.com> - 0-13
- Tox dependency generator: Handle deps read in from a text file (#1808601)
* Wed Feb 05 2020 Miro Hrončok <mhroncok@redhat.com> - 0-12
- Fallback to setuptools.build_meta:__legacy__ backend instead of setuptools.build_meta
- Properly handle backends with colon
- Preserve existing flags in shebangs of Python files in /usr/bin
* Thu Jan 30 2020 Fedora Release Engineering <releng@fedoraproject.org> - 0-11
- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild
* Fri Nov 15 2019 Patrik Kopkan <pkopkan@redhat.com> - 0-10
- Install wheel in '$PWD/pyproject-macros-wheeldir' to have more explicit path from which we install.
- The path can be changed by redefining %%_pyproject_wheeldir.
* Wed Nov 13 2019 Anna Khaitovich <akhaitov@redhat.com> - 0-9
- Remove stray __pycache__ directory from /usr/bin when running %%pyproject_install
* Fri Oct 25 2019 Miro Hrončok <mhroncok@redhat.com> - 0-8
- When tox fails, print tox output before failing
* Tue Oct 08 2019 Miro Hrončok <mhroncok@redhat.com> - 0-7
- Move a verbose line of %%pyproject_buildrequires from stdout to stderr
* Fri Jul 26 2019 Petr Viktorin <pviktori@redhat.com> - 0-6
- Use importlib_metadata rather than pip freeze
* Fri Jul 26 2019 Miro Hrončok <mhroncok@redhat.com> - 0-5
- Allow to fetch test dependencies from tox
- Add %%tox macro to invoke tests
* Fri Jul 26 2019 Fedora Release Engineering <releng@fedoraproject.org> - 0-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild
* Tue Jul 02 2019 Miro Hrončok <mhroncok@redhat.com> - 0-3
- Add %%pyproject_buildrequires
* Tue Jul 02 2019 Miro Hrončok <mhroncok@redhat.com> - 0-2
- Fix shell syntax errors in %%pyproject_install
- Drop PATH warning in %%pyproject_install
* Fri Jun 28 2019 Patrik Kopkan <pkopkan@redhat.com> - 0-1
- created package

370
pyproject_buildrequires.py Normal file
View File

@ -0,0 +1,370 @@
import os
import sys
import importlib
import argparse
import functools
import traceback
import contextlib
from io import StringIO
import subprocess
import re
import tempfile
import email.parser
print_err = functools.partial(print, file=sys.stderr)
# Some valid Python version specifiers are not supported.
# Whitelist characters we can handle.
VERSION_RE = re.compile('[a-zA-Z0-9.-]+')
class EndPass(Exception):
"""End current pass of generating requirements"""
try:
from packaging.requirements import Requirement, InvalidRequirement
from packaging.utils import canonicalize_name, canonicalize_version
try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata
except ImportError as e:
print_err('Import error:', 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():
captured_out = StringIO()
with contextlib.redirect_stdout(captured_out):
yield
for line in captured_out.getvalue().splitlines():
print_err('HOOK STDOUT:', line)
class Requirements:
"""Requirement printer"""
def __init__(self, get_installed_version, extras=None,
generate_extras=False, python3_pkgversion='3'):
self.get_installed_version = get_installed_version
self.extras = set()
if extras:
for extra in extras:
self.add_extras(*extra.split(','))
self.missing_requirements = False
self.generate_extras = generate_extras
self.python3_pkgversion = python3_pkgversion
def add_extras(self, *extras):
self.extras |= set(e.strip() for e in extras)
@property
def marker_envs(self):
if self.extras:
return [{'extra': e} for e in sorted(self.extras)]
return [{'extra': ''}]
def evaluate_all_environamnets(self, requirement):
for marker_env in self.marker_envs:
if requirement.marker.evaluate(environment=marker_env):
return True
return False
def add(self, requirement_str, *, source=None):
"""Output a Python-style requirement string as RPM dep"""
print_err(f'Handling {requirement_str} from {source}')
try:
requirement = Requirement(requirement_str)
except InvalidRequirement as e:
print_err(
f'WARNING: Skipping invalid requirement: {requirement_str}\n'
+ f' {e}',
)
return
name = canonicalize_name(requirement.name)
if (requirement.marker is not None and
not self.evaluate_all_environamnets(requirement)):
print_err(f'Ignoring alien requirement:', requirement_str)
return
try:
# TODO: check if requirements with extras are satisfied
installed = self.get_installed_version(requirement.name)
except importlib_metadata.PackageNotFoundError:
print_err(f'Requirement not satisfied: {requirement_str}')
installed = None
if installed and installed in requirement.specifier:
print_err(f'Requirement satisfied: {requirement_str}')
print_err(f' (installed: {requirement.name} {installed})')
if requirement.extras:
print_err(f' (extras are currently not checked)')
else:
self.missing_requirements = True
if self.generate_extras:
extra_names = [f'{name}[{extra}]' for extra in sorted(requirement.extras)]
else:
extra_names = []
for name in [name] + extra_names:
together = []
for specifier in sorted(
requirement.specifier,
key=lambda s: (s.operator, s.version),
):
version = canonicalize_version(specifier.version)
if not VERSION_RE.fullmatch(str(specifier.version)):
raise ValueError(
f'Unknown character in version: {specifier.version}. '
+ '(This is probably a bug in pyproject-rpm-macros.)',
)
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"({' with '.join(together)})")
def check(self, *, source=None):
"""End current pass if any unsatisfied dependencies were output"""
if self.missing_requirements:
print_err(f'Exiting dependency generation pass: {source}')
raise EndPass(source)
def extend(self, requirement_strs, *, source=None):
"""add() several requirements"""
for req_str in requirement_strs:
self.add(req_str, source=source)
def get_backend(requirements):
try:
f = open('pyproject.toml')
except FileNotFoundError:
pyproject_data = {}
else:
# lazy import toml here, not needed without pyproject.toml
requirements.add('toml', source='parsing pyproject.toml')
requirements.check(source='parsing pyproject.toml')
import toml
with f:
pyproject_data = toml.load(f)
buildsystem_data = pyproject_data.get('build-system', {})
requirements.extend(
buildsystem_data.get('requires', ()),
source='build-system.requires',
)
backend_name = buildsystem_data.get('build-backend')
if not backend_name:
# https://www.python.org/dev/peps/pep-0517/:
# If the pyproject.toml file is absent, or the build-backend key is
# missing, the source tree is not using this specification, and tools
# should revert to the legacy behaviour of running setup.py
# (either directly, or by implicitly invoking the [following] backend).
backend_name = 'setuptools.build_meta:__legacy__'
requirements.add('setuptools >= 40.8', source='default build backend')
requirements.add('wheel', source='default build backend')
requirements.check(source='build backend')
backend_path = buildsystem_data.get('backend-path')
if backend_path:
# PEP 517 example shows the path as a list, but some projects don't follow that
if isinstance(backend_path, str):
backend_path = [backend_path]
sys.path = backend_path + sys.path
module_name, _, object_name = backend_name.partition(":")
backend_module = importlib.import_module(module_name)
if object_name:
return getattr(backend_module, object_name)
return backend_module
def generate_build_requirements(backend, requirements):
get_requires = getattr(backend, 'get_requires_for_build_wheel', None)
if get_requires:
with hook_call():
new_reqs = get_requires()
requirements.extend(new_reqs, source='get_requires_for_build_wheel')
requirements.check(source='get_requires_for_build_wheel')
def generate_run_requirements(backend, requirements):
hook_name = 'prepare_metadata_for_build_wheel'
prepare_metadata = getattr(backend, hook_name, None)
if not prepare_metadata:
raise ValueError(
'build backend cannot provide build metadata '
+ '(incl. runtime requirements) before buld'
)
with hook_call():
dir_basename = prepare_metadata('.')
with open(dir_basename + '/METADATA') as f:
message = email.parser.Parser().parse(f, headersonly=True)
for key in 'Requires', 'Requires-Dist':
requires = message.get_all(key, ())
requirements.extend(requires, source=f'wheel metadata: {key}')
def parse_tox_requires_lines(lines):
packages = []
for line in lines:
line = line.strip()
if line.startswith('-r'):
path = line[2:]
with open(path) as f:
packages.extend(parse_tox_requires_lines(f.read().splitlines()))
elif line.startswith('-'):
print_err(
f'WARNING: Skipping dependency line: {line}\n'
+ f' tox deps options other than -r are not supported (yet).',
)
elif line:
packages.append(line)
return packages
def generate_tox_requirements(toxenv, requirements):
requirements.add('tox-current-env >= 0.0.3', source='tox itself')
requirements.check(source='tox itself')
with tempfile.NamedTemporaryFile('r') as deps, tempfile.NamedTemporaryFile('r') as extras:
r = subprocess.run(
[sys.executable, '-m', 'tox',
'--print-deps-to', deps.name,
'--print-extras-to', extras.name,
'-qre', toxenv],
check=False,
encoding='utf-8',
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if r.stdout:
print_err(r.stdout, end='')
r.check_returncode()
deplines = deps.read().splitlines()
packages = parse_tox_requires_lines(deplines)
requirements.add_extras(*extras.read().splitlines())
requirements.extend(packages,
source=f'tox --print-deps-only: {toxenv}')
def python3dist(name, op=None, version=None, python3_pkgversion="3"):
prefix = f"python{python3_pkgversion}dist"
if op is None:
if version is not None:
raise AssertionError('op and version go together')
return f'{prefix}({name})'
else:
return f'{prefix}({name}) {op} {version}'
def generate_requires(
*, include_runtime=False, toxenv=None, extras=None,
get_installed_version=importlib_metadata.version, # for dep injection
generate_extras=False, python3_pkgversion="3",
):
"""Generate the BuildRequires for the project in the current directory
This is the main Python entry point.
"""
requirements = Requirements(
get_installed_version, extras=extras or [],
generate_extras=generate_extras,
python3_pkgversion=python3_pkgversion
)
try:
backend = get_backend(requirements)
generate_build_requirements(backend, requirements)
if toxenv is not None:
include_runtime = True
generate_tox_requirements(toxenv, requirements)
if include_runtime:
generate_run_requirements(backend, requirements)
except EndPass:
return
def main(argv):
parser = argparse.ArgumentParser(
description='Generate BuildRequires for a Python project.'
)
parser.add_argument(
'-r', '--runtime', action='store_true',
help='Generate run-time requirements',
)
parser.add_argument(
'-e', '--toxenv', metavar='TOXENVS', default=None,
help=('specify tox environments'
'(implies --tox)'),
)
parser.add_argument(
'-t', '--tox', action='store_true',
help=('generate test tequirements from tox environment '
'(implies --runtime)'),
)
parser.add_argument(
'-x', '--extras', metavar='EXTRAS', action='append',
help='comma separated list of "extras" for runtime requirements '
'(e.g. -x testing,feature-x) (implies --runtime, can be repeated)',
)
parser.add_argument(
'--generate-extras', action='store_true',
help='Generate build requirements on Python Extras',
)
parser.add_argument(
'-p', '--python3_pkgversion', metavar='PYTHON3_PKGVERSION',
default="3", help=('Python version for pythonXdist()'
'or pythonX.Ydist() requirements'),
)
args = parser.parse_args(argv)
if args.toxenv:
args.tox = True
if args.tox:
args.runtime = True
args.toxenv = (args.toxenv or os.getenv('RPM_TOXENV') or
f'py{sys.version_info.major}{sys.version_info.minor}')
if args.extras:
args.runtime = True
try:
generate_requires(
include_runtime=args.runtime,
toxenv=args.toxenv,
extras=args.extras,
generate_extras=args.generate_extras,
python3_pkgversion=args.python3_pkgversion,
)
except Exception:
# Log the traceback explicitly (it's useful debug info)
traceback.print_exc()
exit(1)
if __name__ == '__main__':
main(sys.argv[1:])

View File

@ -0,0 +1,366 @@
No pyproject.toml, nothing installed:
installed:
# empty
expected: |
python3dist(setuptools) >= 40.8
python3dist(wheel)
result: 0
Nothing installed yet:
installed:
# empty
pyproject.toml: |
# empty
expected: |
python3dist(toml)
result: 0
Insufficient version of setuptools:
installed:
setuptools: 5
wheel: 1
toml: 1
pyproject.toml: |
# empty
expected: |
python3dist(toml)
python3dist(setuptools) >= 40.8
python3dist(wheel)
result: 0
No pyproject.toml, empty setup.py:
installed:
setuptools: 50
wheel: 1
setup.py: |
expected: |
python3dist(setuptools) >= 40.8
python3dist(wheel)
python3dist(wheel)
result: 0
Default build system, empty setup.py:
installed:
setuptools: 50
wheel: 1
toml: 1
pyproject.toml: |
# empty
setup.py: |
expected: |
python3dist(toml)
python3dist(setuptools) >= 40.8
python3dist(wheel)
python3dist(wheel)
result: 0
Erroring setup.py:
installed:
setuptools: 50
wheel: 1
setup.py: |
exit(77)
result: 77
Bad character in version:
installed:
toml: 1
pyproject.toml: |
[build-system]
requires = ["pkg == 0.$.^.*"]
except: ValueError
Build system dependencies in pyproject.toml with extras:
generate_extras: true
installed:
setuptools: 50
wheel: 1
toml: 1
pyproject.toml: |
[build-system]
requires = [
"foo",
"bar[baz] > 5",
"ne!=1",
"ge>=1.2",
"le <= 1.2.3",
"lt < 1.2.3.4 ",
" gt > 1.2.3.4.5",
"multi[extras1,extras2] == 6.0",
"combo >2, <5, != 3.0.0",
"invalid!!ignored",
"py2 ; python_version < '2.7'",
"py3 ; python_version > '3.0'",
]
expected: |
python3dist(toml)
python3dist(foo)
python3dist(bar) > 5
python3dist(bar[baz]) > 5
(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) with python3dist(combo) < 5 with python3dist(combo) > 2)
python3dist(py3)
python3dist(setuptools) >= 40.8
python3dist(wheel)
result: 0
Build system dependencies in pyproject.toml without extras:
generate_extras: false
installed:
setuptools: 50
wheel: 1
toml: 1
pyproject.toml: |
[build-system]
requires = [
"bar[baz] > 5",
"multi[extras1,extras2] == 6.0",
]
expected: |
python3dist(toml)
python3dist(bar) > 5
python3dist(multi) = 6
python3dist(setuptools) >= 40.8
python3dist(wheel)
result: 0
Default build system, build dependencies in setup.py:
installed:
setuptools: 50
wheel: 1
setup.py: |
from setuptools import setup
setup(
name='test',
version='0.1',
setup_requires=['foo', 'bar!=2', 'baz~=1.1.1'],
install_requires=['inst'],
)
expected: |
python3dist(setuptools) >= 40.8
python3dist(wheel)
python3dist(wheel)
python3dist(foo)
(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:
installed:
setuptools: 50
wheel: 1
pyyaml: 1
include_runtime: true
setup.py: |
from setuptools import setup
setup(
name='test',
version='0.1',
setup_requires=['pyyaml'], # nb. setuptools will try to install this
install_requires=['inst > 1', 'inst2 < 3'],
)
expected: |
python3dist(setuptools) >= 40.8
python3dist(wheel)
python3dist(wheel)
python3dist(pyyaml)
python3dist(inst) > 1
python3dist(inst2) < 3
result: 0
Run dependencies with extras (not selected):
installed:
setuptools: 50
wheel: 1
pyyaml: 1
include_runtime: true
setup.py: &pytest_setup_py |
# slightly abriged copy of pytest's setup.py
from setuptools import setup
INSTALL_REQUIRES = [
"py>=1.5.0",
"six>=1.10.0",
"setuptools",
"attrs>=17.4.0",
'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"',
'more-itertools>=4.0.0;python_version>"2.7"',
"atomicwrites>=1.0",
'funcsigs>=1.0;python_version<"3.0"',
'pathlib2>=2.2.0;python_version<"3.6"',
'colorama;sys_platform=="win32"',
"pluggy>=0.11",
]
def main():
setup(
setup_requires=["setuptools>=40.0"],
# fmt: off
extras_require={
"testing": [
"argcomplete",
"hypothesis>=3.56",
"nose",
"requests",
"mock;python_version=='2.7'",
],
},
# fmt: on
install_requires=INSTALL_REQUIRES,
)
if __name__ == "__main__":
main()
expected: |
python3dist(setuptools) >= 40.8
python3dist(wheel)
python3dist(wheel)
python3dist(setuptools) >= 40
python3dist(py) >= 1.5
python3dist(six) >= 1.10
python3dist(setuptools)
python3dist(attrs) >= 17.4
python3dist(atomicwrites) >= 1
python3dist(pluggy) >= 0.11
python3dist(more-itertools) >= 4
result: 0
Run dependencies with extras (selected):
installed:
setuptools: 50
wheel: 1
pyyaml: 1
include_runtime: true
extras:
- testing
setup.py: *pytest_setup_py
expected: |
python3dist(setuptools) >= 40.8
python3dist(wheel)
python3dist(wheel)
python3dist(setuptools) >= 40
python3dist(py) >= 1.5
python3dist(six) >= 1.10
python3dist(setuptools)
python3dist(attrs) >= 17.4
python3dist(atomicwrites) >= 1
python3dist(pluggy) >= 0.11
python3dist(more-itertools) >= 4
python3dist(argcomplete)
python3dist(hypothesis) >= 3.56
python3dist(nose)
python3dist(requests)
result: 0
Run dependencies with multiple extras:
installed:
setuptools: 50
wheel: 1
pyyaml: 1
include_runtime: true
extras:
- testing,more-testing
- even-more-testing , cool-feature
setup.py: |
from setuptools import setup
setup(
extras_require={
'testing': ['dep1'],
'more-testing': ['dep2'],
'even-more-testing': ['dep3'],
'cool-feature': ['dep4'],
},
)
expected: |
python3dist(setuptools) >= 40.8
python3dist(wheel)
python3dist(wheel)
python3dist(dep4)
python3dist(dep3)
python3dist(dep2)
python3dist(dep1)
result: 0
Tox dependencies:
installed:
setuptools: 50
wheel: 1
tox: 3.5.3
tox-current-env: 0.0.3
toxenv: py3
setup.py: |
from setuptools import setup
setup(
name='test',
version='0.1',
install_requires=['inst'],
)
tox.ini: |
[tox]
envlist = py36,py37,py38
[testenv]
deps =
toxdep1
toxdep2
commands =
true
expected: |
python3dist(setuptools) >= 40.8
python3dist(wheel)
python3dist(wheel)
python3dist(tox-current-env) >= 0.0.3
python3dist(toxdep1)
python3dist(toxdep2)
python3dist(inst)
result: 0
Tox extras:
installed:
setuptools: 50
wheel: 1
tox: 3.5.3
tox-current-env: 0.0.3
toxenv: py3
setup.py: |
from setuptools import setup
setup(
name='test',
version='0.1',
install_requires=['inst'],
extras_require={
'extra1': ['dep11 > 11', 'dep12'],
'extra2': ['dep21', 'dep22', 'dep23'],
'nope': ['nopedep'],
}
)
tox.ini: |
[tox]
envlist = py36,py37,py38
[testenv]
deps =
toxdep
extras =
extra2
extra1
commands =
true
expected: |
python3dist(setuptools) >= 40.8
python3dist(wheel)
python3dist(wheel)
python3dist(tox-current-env) >= 0.0.3
python3dist(toxdep)
python3dist(inst)
python3dist(dep11) > 11
python3dist(dep12)
python3dist(dep21)
python3dist(dep22)
python3dist(dep23)
result: 0

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))

View File

@ -0,0 +1,85 @@
import argparse
import csv
import json
import os
from pathlib import PosixPath
from pyproject_save_files import BuildrootPath
def read_record(record_path):
"""
A generator yielding individual RECORD triplets.
https://www.python.org/dev/peps/pep-0376/#record
The triplet is str-path, hash, size -- the last two optional.
We will later care only for the paths anyway.
Example:
>>> g = read_record(PosixPath('./test_RECORD'))
>>> next(g)
['../../../bin/__pycache__/tldr.cpython-....pyc', '', '']
>>> next(g)
['../../../bin/tldr', 'sha256=...', '12766']
>>> next(g)
['../../../bin/tldr.py', 'sha256=...', '12766']
"""
with open(record_path, newline="", encoding="utf-8") as f:
yield from csv.reader(
f, delimiter=",", quotechar='"', lineterminator=os.linesep
)
def parse_record(record_path, record_content):
"""
Returns a list with BuildrootPaths parsed from record_content
params:
record_path: RECORD BuildrootPath
record_content: list of RECORD triplets
first item is a str-path relative to directory where dist-info directory is
(it can also be absolute according to the standard, but not from pip)
Examples:
>>> parse_record(BuildrootPath('/usr/lib/python3.7/site-packages/requests-2.22.0.dist-info/RECORD'),
... [('requests/sessions.py', 'sha256=xxx', '666')])
['/usr/lib/python3.7/site-packages/requests/sessions.py']
>>> parse_record(BuildrootPath('/usr/lib/python3.7/site-packages/tldr-0.5.dist-info/RECORD'),
... [('../../../bin/tldr', 'sha256=yyy', '777')])
['/usr/bin/tldr']
"""
sitedir = record_path.parent.parent # trough the dist-info directory
# / with absolute right operand will remove the left operand
# any .. parts are resolved via normpath
return [str((sitedir / row[0]).normpath()) for row in record_content]
def save_parsed_record(record_path, parsed_record, output_file):
content = {}
if output_file.is_file():
content = json.loads(output_file.read_text())
content[str(record_path)] = parsed_record
output_file.write_text(json.dumps(content))
def main(cli_args):
record_path = BuildrootPath.from_real(cli_args.record, root=cli_args.buildroot)
parsed_record = parse_record(record_path, read_record(cli_args.record))
save_parsed_record(record_path, parsed_record, cli_args.output)
def argparser():
parser = argparse.ArgumentParser()
r = parser.add_argument_group("required arguments")
r.add_argument("--buildroot", type=PosixPath, required=True)
r.add_argument("--record", type=PosixPath, required=True)
r.add_argument("--output", type=PosixPath, required=True)
return parser
if __name__ == "__main__":
cli_args = argparser().parse_args()
main(cli_args)

392
pyproject_save_files.py Normal file
View File

@ -0,0 +1,392 @@
import argparse
import fnmatch
import json
import os
from collections import defaultdict
from pathlib import PosixPath, PurePosixPath
class BuildrootPath(PurePosixPath):
"""
This path represents a path in a buildroot.
When absolute, it is "relative" to a buildroot.
E.g. /usr/lib means %{buildroot}/usr/lib
The object carries no buildroot information.
"""
@staticmethod
def from_real(realpath, *, root):
"""
For a given real disk path, return a BuildrootPath in the given root.
For example::
>>> BuildrootPath.from_real(PosixPath('/tmp/buildroot/foo'), root=PosixPath('/tmp/buildroot'))
BuildrootPath('/foo')
"""
return BuildrootPath("/") / realpath.relative_to(root)
def to_real(self, root):
"""
Return a real PosixPath in the given root
For example::
>>> BuildrootPath('/foo').to_real(PosixPath('/tmp/buildroot'))
PosixPath('/tmp/buildroot/foo')
"""
return root / self.relative_to("/")
def normpath(self):
"""
Normalize all the potential /../ parts of the path without touching real files.
PurePaths don't have .resolve().
Paths have .resolve() but it touches real files.
This is an alternative. It assumes there are no symbolic links.
Example:
>>> BuildrootPath('/usr/lib/python/../pypy').normpath()
BuildrootPath('/usr/lib/pypy')
"""
return type(self)(os.path.normpath(self))
def pycached(script, python_version):
"""
For a script BuildrootPath, return a list with that path and its bytecode glob.
Like the %pycached macro.
The glob is represented as a BuildrootPath.
Examples:
>>> pycached(BuildrootPath('/whatever/bar.py'), '3.8')
[BuildrootPath('/whatever/bar.py'), BuildrootPath('/whatever/__pycache__/bar.cpython-38{,.opt-?}.pyc')]
>>> pycached(BuildrootPath('/opt/python3.10/foo.py'), '3.10')
[BuildrootPath('/opt/python3.10/foo.py'), BuildrootPath('/opt/python3.10/__pycache__/foo.cpython-310{,.opt-?}.pyc')]
"""
assert script.suffix == ".py"
pyver = "".join(python_version.split(".")[:2])
pycname = f"{script.stem}.cpython-{pyver}{{,.opt-?}}.pyc"
pyc = script.parent / "__pycache__" / pycname
return [script, pyc]
def add_file_to_module(paths, module_name, module_type, *files):
"""
Helper procedure, adds given files to the module_name of a given module_type
"""
for module in paths["modules"][module_name]:
if module["type"] == module_type:
if files[0] not in module["files"]:
module["files"].extend(files)
break
else:
paths["modules"][module_name].append(
{"type": module_type, "files": list(files)}
)
def add_lang_to_module(paths, module_name, path):
"""
Helper procedure, divides lang files by language and adds them to the module_name
Returns True if the language code detection was successful
"""
for i, parent in enumerate(path.parents):
if i > 0 and parent.name == 'locale':
lang_country_code = path.parents[i-1].name
break
else:
return False
# convert potential en_US to plain en
lang_code = lang_country_code.partition('_')[0]
if module_name not in paths["lang"]:
paths["lang"].update({module_name: defaultdict(list)})
paths["lang"][module_name][lang_code].append(path)
return True
def classify_paths(
record_path, parsed_record_content, sitedirs, python_version
):
"""
For each BuildrootPath in parsed_record_content classify it to a dict structure
that allows to filter the files for the %files section easier.
For the dict structure, look at the beginning of this function's code.
Each "module" is a dict with "type" ("package", "script", "extension") and "files".
"""
distinfo = record_path.parent
paths = {
"metadata": {
"files": [], # regular %file entries with dist-info content
"dirs": [distinfo], # %dir %file entries with dist-info directory
"docs": [], # to be used once there is upstream way to recognize READMEs
"licenses": [], # to be used once there is upstream way to recognize LICENSEs
},
"lang": {}, # %lang entries: [module_name or None][language_code] lists of .mo files
"modules": defaultdict(list), # each importable module (directory, .py, .so)
"other": {"files": []}, # regular %file entries we could not parse :(
}
# In RECORDs generated by pip, there are no directories, only files.
# The example RECORD from PEP 376 does not contain directories either.
# Hence, we'll only assume files, but TODO get it officially documented.
for path in parsed_record_content:
if path.suffix == ".pyc":
# we handle bytecode separately
continue
if path.parent == distinfo:
if path.name in ("RECORD", "REQUESTED"):
# RECORD and REQUESTED files are removed in %pyproject_install
# See PEP 627
continue
# TODO is this a license/documentation?
paths["metadata"]["files"].append(path)
continue
for sitedir in sitedirs:
if sitedir in path.parents:
if path.parent == sitedir:
if path.suffix == ".so":
# extension modules can have 2 suffixes
name = BuildrootPath(path.stem).stem
add_file_to_module(paths, name, "extension", path)
elif path.suffix == ".py":
name = path.stem
add_file_to_module(
paths, name, "script", *pycached(path, python_version)
)
else:
paths["other"]["files"].append(path)
else:
# this file is inside a dir, we classify that dir
index = path.parents.index(sitedir)
module_dir = path.parents[index - 1]
add_file_to_module(paths, module_dir.name, "package", module_dir)
if path.suffix == ".mo":
add_lang_to_module(paths, module_dir.name, path)
break
else:
if path.suffix == ".mo":
add_lang_to_module(paths, None, path) or paths["other"]["files"].append(path)
else:
paths["other"]["files"].append(path)
return paths
def generate_file_list(paths_dict, module_globs, include_others=False):
"""
This function takes the classified paths_dict and turns it into lines
for the %files section. Returns list with text lines, no Path objects.
Only includes files from modules that match module_globs, metadata and
optionaly all other files.
It asserts that all globs match at least one module, raises ValueError otherwise.
Multiple globs matching identical module(s) are OK.
"""
files = set()
if include_others:
files.update(f"{p}" for p in paths_dict["other"]["files"])
try:
for lang_code in paths_dict["lang"][None]:
files.update(f"%lang({lang_code}) {path}" for path in paths_dict["lang"][None][lang_code])
except KeyError:
pass
files.update(f"{p}" for p in paths_dict["metadata"]["files"])
for macro in "dir", "doc", "license":
files.update(f"%{macro} {p}" for p in paths_dict["metadata"][f"{macro}s"])
modules = paths_dict["modules"]
done_modules = set()
done_globs = set()
for glob in module_globs:
for name in modules:
if fnmatch.fnmatchcase(name, glob):
if name not in done_modules:
try:
for lang_code in paths_dict["lang"][name]:
files.update(f"%lang({lang_code}) {path}" for path in paths_dict["lang"][name][lang_code])
except KeyError:
pass
for module in modules[name]:
if module["type"] == "package":
files.update(f"{p}/" for p in module["files"])
else:
files.update(f"{p}" for p in module["files"])
done_modules.add(name)
done_globs.add(glob)
missed = module_globs - done_globs
if missed:
missed_text = ", ".join(sorted(missed))
raise ValueError(f"Globs did not match any module: {missed_text}")
return sorted(files)
def parse_varargs(varargs):
"""
Parse varargs from the %pyproject_save_files macro
Arguments starting with + are treated as a flags, everything else is a glob
Returns as set of globs, boolean flag whether to include all the other files
Raises ValueError for unknown flags and globs with dots (namespace packages).
Good examples:
>>> parse_varargs(['*'])
({'*'}, False)
>>> mods, auto = parse_varargs(['requests*', 'kerberos', '+auto'])
>>> auto
True
>>> sorted(mods)
['kerberos', 'requests*']
>>> mods, auto = parse_varargs(['tldr', 'tensorf*'])
>>> auto
False
>>> sorted(mods)
['tensorf*', 'tldr']
>>> parse_varargs(['+auto'])
(set(), True)
Bad examples:
>>> parse_varargs(['+kinkdir'])
Traceback (most recent call last):
...
ValueError: Invalid argument: +kinkdir
>>> parse_varargs(['good', '+bad', '*ugly*'])
Traceback (most recent call last):
...
ValueError: Invalid argument: +bad
>>> parse_varargs(['+bad', 'my.bad'])
Traceback (most recent call last):
...
ValueError: Invalid argument: +bad
>>> parse_varargs(['mod', 'mod.*'])
Traceback (most recent call last):
...
ValueError: Attempted to use a namespaced package with dot in the glob: mod.*. ...
>>> parse_varargs(['my.bad', '+bad'])
Traceback (most recent call last):
...
ValueError: Attempted to use a namespaced package with dot in the glob: my.bad. ...
"""
include_auto = False
globs = set()
for arg in varargs:
if arg.startswith("+"):
if arg == "+auto":
include_auto = True
else:
raise ValueError(f"Invalid argument: {arg}")
elif "." in arg:
top, *_ = arg.partition(".")
msg = (
f"Attempted to use a namespaced package with dot in the glob: {arg}. "
f"That is not (yet) supported. Use {top} instead and/or file a Bugzilla explaining your use case."
)
raise ValueError(msg)
else:
globs.add(arg)
return globs, include_auto
def load_parsed_record(pyproject_record):
parsed_record = {}
with open(pyproject_record) as pyproject_record_file:
content = json.load(pyproject_record_file)
if len(content) > 1:
raise FileExistsError("%pyproject install has found more than one *.dist-info/RECORD file. "
"Currently, %pyproject_save_files supports only one wheel → one file list mapping. "
"Feel free to open a bugzilla for pyproject-rpm-macros and describe your usecase.")
# Redefine strings stored in JSON to BuildRootPaths
for record_path, files in content.items():
parsed_record[BuildrootPath(record_path)] = [BuildrootPath(f) for f in files]
return parsed_record
def pyproject_save_files(buildroot, sitelib, sitearch, python_version, pyproject_record, varargs):
"""
Takes arguments from the %{pyproject_save_files} macro
Returns list of paths for the %files section
"""
# On 32 bit architectures, sitelib equals to sitearch
# This saves us browsing one directory twice
sitedirs = sorted({sitelib, sitearch})
globs, include_auto = parse_varargs(varargs)
parsed_records = load_parsed_record(pyproject_record)
final_file_list = []
for record_path, files in parsed_records.items():
paths_dict = classify_paths(
record_path, files, sitedirs, python_version
)
final_file_list.extend(
generate_file_list(paths_dict, globs, include_auto)
)
return final_file_list
def main(cli_args):
file_section = pyproject_save_files(
cli_args.buildroot,
cli_args.sitelib,
cli_args.sitearch,
cli_args.python_version,
cli_args.pyproject_record,
cli_args.varargs,
)
cli_args.output.write_text("\n".join(file_section) + "\n", encoding="utf-8")
def argparser():
parser = argparse.ArgumentParser()
r = parser.add_argument_group("required arguments")
r.add_argument("--output", type=PosixPath, required=True)
r.add_argument("--buildroot", type=PosixPath, required=True)
r.add_argument("--sitelib", type=BuildrootPath, required=True)
r.add_argument("--sitearch", type=BuildrootPath, required=True)
r.add_argument("--python-version", type=str, required=True)
r.add_argument("--pyproject-record", type=PosixPath, required=True)
parser.add_argument("varargs", nargs="+")
return parser
if __name__ == "__main__":
cli_args = argparser().parse_args()
main(cli_args)

File diff suppressed because it is too large Load Diff

0
sources Normal file
View File

11
test_RECORD Normal file
View File

@ -0,0 +1,11 @@
../../../bin/__pycache__/tldr.cpython-37.pyc,,
../../../bin/tldr,sha256=6MUiLCWhldmV8OelT2dvPgS7q5GFwuhvd6th0Bb-LH4,12766
../../../bin/tldr.py,sha256=6MUiLCWhldmV8OelT2dvPgS7q5GFwuhvd6th0Bb-LH4,12766
__pycache__/tldr.cpython-37.pyc,,
tldr-0.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
tldr-0.5.dist-info/LICENSE,sha256=q7quAfjDWCYKC_WRk_uaP6d2wwVpOpVjUSkv8l6H7xI,1075
tldr-0.5.dist-info/METADATA,sha256=AN5nYUVxo_zkVaMGKu34YDWWif84oA6uxKmTab213vM,3850
tldr-0.5.dist-info/RECORD,,
tldr-0.5.dist-info/WHEEL,sha256=S8S5VL-stOTSZDYxHyf0KP7eds0J72qrK0Evu3TfyAY,92
tldr-0.5.dist-info/top_level.txt,sha256=xHSI9WD6Y-_hONbi2b_9RIn9oiO7RBGHU3A8geJq3mI,5
tldr.py,sha256=aJlA3tIz4QYYy8e7DZUhPyLCqTwnfFjA7Nubwm9bPe0,12779

View File

@ -0,0 +1,60 @@
from pathlib import Path
import io
import pytest
import yaml
from pyproject_buildrequires import generate_requires
try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata
testcases = {}
with Path(__file__).parent.joinpath('pyproject_buildrequires_testcases.yaml').open() as f:
testcases = yaml.safe_load(f)
@pytest.mark.parametrize('case_name', testcases)
def test_data(case_name, capsys, tmp_path, monkeypatch):
case = testcases[case_name]
cwd = tmp_path.joinpath('cwd')
cwd.mkdir()
monkeypatch.chdir(cwd)
if case.get('xfail'):
pytest.xfail(case.get('xfail'))
for filename in 'pyproject.toml', 'setup.py', 'tox.ini':
if filename in case:
cwd.joinpath(filename).write_text(case[filename])
def get_installed_version(dist_name):
try:
return str(case['installed'][dist_name])
except (KeyError, TypeError):
raise importlib_metadata.PackageNotFoundError(
f'info not found for {dist_name}'
)
try:
generate_requires(
get_installed_version=get_installed_version,
include_runtime=case.get('include_runtime', False),
extras=case.get('extras', []),
toxenv=case.get('toxenv', None),
generate_extras=case.get('generate_extras', False),
)
except SystemExit as e:
assert e.code == case['result']
except Exception as e:
if 'except' not in case:
raise
assert type(e).__name__ == case['except']
else:
assert 0 == case['result']
captured = capsys.readouterr()
assert captured.out == case['expected']

205
test_pyproject_save_files.py Executable file
View File

@ -0,0 +1,205 @@
import pytest
import yaml
from pathlib import Path
from pprint import pprint
from pyproject_preprocess_record import parse_record, read_record, save_parsed_record
from pyproject_save_files import argparser, generate_file_list, BuildrootPath
from pyproject_save_files import main as save_files_main
DIR = Path(__file__).parent
BINDIR = BuildrootPath("/usr/bin")
DATADIR = BuildrootPath("/usr/share")
SITELIB = BuildrootPath("/usr/lib/python3.7/site-packages")
SITEARCH = BuildrootPath("/usr/lib64/python3.7/site-packages")
yaml_file = DIR / "pyproject_save_files_test_data.yaml"
yaml_data = yaml.safe_load(yaml_file.read_text())
EXPECTED_DICT = yaml_data["classified"]
EXPECTED_FILES = yaml_data["dumped"]
TEST_RECORDS = yaml_data["records"]
@pytest.fixture
def tldr_root(tmp_path):
prepare_pyproject_record(tmp_path, package="tldr")
return tmp_path
@pytest.fixture
def pyproject_record(tmp_path):
return tmp_path / "pyproject-record"
def prepare_pyproject_record(tmp_path, package=None, content=None):
"""
Creates RECORD from test data and then uses
functions from pyproject_process_record to convert
it to pyproject-record file which is then
further processed by functions from pyproject_save_files.
"""
record_file = tmp_path / "RECORD"
pyproject_record = tmp_path / "pyproject-record"
if package is not None:
# Get test data and write dist-info/RECORD file
record_path = BuildrootPath(TEST_RECORDS[package]["path"])
record_file.write_text(TEST_RECORDS[package]["content"])
# Parse RECORD file
parsed_record = parse_record(record_path, read_record(record_file))
# Save JSON content to pyproject-record
save_parsed_record(record_path, parsed_record, pyproject_record)
elif content is not None:
save_parsed_record(*content, output_file=pyproject_record)
@pytest.fixture
def output(tmp_path):
return tmp_path / "pyproject_files"
def test_parse_record_tldr():
record_path = BuildrootPath(TEST_RECORDS["tldr"]["path"])
record_content = read_record(DIR / "test_RECORD")
output = list(parse_record(record_path, record_content))
pprint(output)
expected = [
str(BINDIR / "__pycache__/tldr.cpython-37.pyc"),
str(BINDIR / "tldr"),
str(BINDIR / "tldr.py"),
str(SITELIB / "__pycache__/tldr.cpython-37.pyc"),
str(SITELIB / "tldr-0.5.dist-info/INSTALLER"),
str(SITELIB / "tldr-0.5.dist-info/LICENSE"),
str(SITELIB / "tldr-0.5.dist-info/METADATA"),
str(SITELIB / "tldr-0.5.dist-info/RECORD"),
str(SITELIB / "tldr-0.5.dist-info/WHEEL"),
str(SITELIB / "tldr-0.5.dist-info/top_level.txt"),
str(SITELIB / "tldr.py"),
]
assert output == expected
def test_parse_record_tensorflow():
long = "tensorflow_core/include/tensorflow/core/common_runtime/base_collective_executor.h"
record_path = SITEARCH / "tensorflow-2.1.0.dist-info/RECORD"
record_content = [
["../../../bin/toco_from_protos", "sha256=hello", "289"],
[f"../../../lib/python3.7/site-packages/{long}", "sha256=darkness", "1024"],
["tensorflow-2.1.0.dist-info/METADATA", "sha256=friend", "2859"],
]
output = list(parse_record(record_path, record_content))
pprint(output)
expected = [
str(BINDIR / "toco_from_protos"),
str(SITELIB / long),
str(SITEARCH / "tensorflow-2.1.0.dist-info/METADATA"),
]
assert output == expected
def remove_others(expected):
return [p for p in expected if not (p.startswith(str(BINDIR)) or p.endswith(".pth") or p.rpartition(' ')[-1].startswith(str(DATADIR)))]
@pytest.mark.parametrize("include_auto", (True, False))
@pytest.mark.parametrize("package, glob, expected", EXPECTED_FILES)
def test_generate_file_list(package, glob, expected, include_auto):
paths_dict = EXPECTED_DICT[package]
modules_glob = {glob}
if not include_auto:
expected = remove_others(expected)
tested = generate_file_list(paths_dict, modules_glob, include_auto)
assert tested == expected
def test_generate_file_list_unused_glob():
paths_dict = EXPECTED_DICT["kerberos"]
modules_glob = {"kerberos", "unused_glob1", "unused_glob2", "kerb*"}
with pytest.raises(ValueError) as excinfo:
generate_file_list(paths_dict, modules_glob, True)
assert "unused_glob1, unused_glob2" in str(excinfo.value)
assert "kerb" not in str(excinfo.value)
def default_options(output, mock_root, pyproject_record):
return [
"--output",
str(output),
"--buildroot",
str(mock_root),
"--sitelib",
str(SITELIB),
"--sitearch",
str(SITEARCH),
"--python-version",
"3.7", # test data are for 3.7,
"--pyproject-record",
str(pyproject_record)
]
@pytest.mark.parametrize("include_auto", (True, False))
@pytest.mark.parametrize("package, glob, expected", EXPECTED_FILES)
def test_cli(tmp_path, package, glob, expected, include_auto, pyproject_record):
prepare_pyproject_record(tmp_path, package)
output = tmp_path / "files"
globs = [glob, "+auto"] if include_auto else [glob]
cli_args = argparser().parse_args([*default_options(output, tmp_path, pyproject_record), *globs])
save_files_main(cli_args)
if not include_auto:
expected = remove_others(expected)
tested = output.read_text()
assert tested == "\n".join(expected) + "\n"
def test_cli_no_pyproject_record(tmp_path, pyproject_record):
output = tmp_path / "files"
cli_args = argparser().parse_args([*default_options(output, tmp_path, pyproject_record), "tldr*"])
with pytest.raises(FileNotFoundError):
save_files_main(cli_args)
def test_cli_too_many_RECORDS(tldr_root, output, pyproject_record):
# Two calls to simulate how %pyproject_install process more than one RECORD file
prepare_pyproject_record(tldr_root,
content=("foo/bar/dist-info/RECORD", []))
prepare_pyproject_record(tldr_root,
content=("foo/baz/dist-info/RECORD", []))
cli_args = argparser().parse_args([*default_options(output, tldr_root, pyproject_record), "tldr*"])
with pytest.raises(FileExistsError):
save_files_main(cli_args)
def test_cli_bad_argument(tldr_root, output, pyproject_record):
cli_args = argparser().parse_args(
[*default_options(output, tldr_root, pyproject_record), "tldr*", "+foodir"]
)
with pytest.raises(ValueError):
save_files_main(cli_args)
def test_cli_bad_option(tldr_root, output, pyproject_record):
prepare_pyproject_record(tldr_root.parent, content=("RECORD1", []))
cli_args = argparser().parse_args(
[*default_options(output, tldr_root, pyproject_record), "tldr*", "you_cannot_have_this"]
)
with pytest.raises(ValueError):
save_files_main(cli_args)
def test_cli_bad_namespace(tldr_root, output, pyproject_record):
cli_args = argparser().parse_args(
[*default_options(output, tldr_root, pyproject_record), "tldr.didntread"]
)
with pytest.raises(ValueError):
save_files_main(cli_args)

49
tests/mocktest.sh Executable file
View File

@ -0,0 +1,49 @@
#!/usr/bin/bash -eux
. /etc/os-release
fedora=$VERSION_ID
config="/tmp/fedora-${fedora}-x86_64-ci.cfg"
# create mock config if not present
# this makes sure tested version of pyproject-rpm-macros is available
# TODO: check if it has precedence if the release was not bumped in tested PR
if [ ! -f $config ]; then
original="/etc/mock/fedora-${fedora}-x86_64.cfg"
cp $original $config
echo -e '\n\nconfig_opts[f"{config_opts.package_manager}.conf"] += """' >> $config
# The zuul CI has zuul-build.repo
# The Jenkins CI has test-<pkgname>.repo
# We run this code from various packages, so we support any <pkgname>
if [ -f /etc/yum.repos.d/zuul-build.repo ]; then
cat /etc/yum.repos.d/zuul-build.repo >> $config
else
cat /etc/yum.repos.d/test-*.repo >> $config
fi
echo -e '\n"""\n' >> $config
fi
# prepare the rpmbuild folders, make sure nothing relevant is there
mkdir -p ~/rpmbuild/{SOURCES,SRPMS}
rm -f ~/rpmbuild/SRPMS/${1}-*.src.rpm
# download the sources and create SRPM
spectool -g -R ${1}.spec
rpmbuild -bs ${1}.spec
# build the SRPM in mock
res=0
mock -r $config --enablerepo=local init
mock -r $config --enablerepo=local ~/rpmbuild/SRPMS/${1}-*.src.rpm || res=$?
# move the results to the artifacts directory, so we can examine them
artifacts=${TEST_ARTIFACTS:-/tmp/artifacts}
pushd /var/lib/mock/fedora-*-x86_64/result
mv *.rpm ${artifacts}/ || :
for log in *.log; do
mv ${log} ${artifacts}/${1}-${log}
done
popd
exit $res

52
tests/printrun.spec Normal file
View File

@ -0,0 +1,52 @@
Name: printrun
Version: 2.0.0~rc6
%global upstream_version 2.0.0rc6
Release: 0%{?dist}
Summary: RepRap printer interface and tools
License: GPLv3+ and FSFAP
URL: https://github.com/kliment/Printrun
Source0: https://github.com/kliment/Printrun/archive/%{name}-%{upstream_version}.tar.gz
# fix locale location
Patch0: https://github.com/kliment/Printrun/pull/1101.patch
BuildRequires: pyproject-rpm-macros
BuildRequires: python3-devel
BuildRequires: gcc
%description
This package contains lang files outside of printrun module.
Building this tests that lang files are marked with %%lang in filelist.
%prep
%autosetup -p1 -n Printrun-printrun-%{upstream_version}
%generate_buildrequires
%pyproject_buildrequires
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files printrun +auto
%check
# Internal check if generated lang entries are same as
# the ones generated using %%find_lang
%find_lang pronterface
%find_lang plater
grep '^%%lang' %{pyproject_files} | sort > tested.lang
sort pronterface.lang plater.lang > expected.lang
diff tested.lang expected.lang
%files -f %{pyproject_files}
%doc README*
%license COPYING

51
tests/python-clikit.spec Normal file
View File

@ -0,0 +1,51 @@
%global pypi_name clikit
Name: python-%{pypi_name}
Version: 0.3.1
Release: 1%{?dist}
Summary: Builds beautiful and testable command line interfaces
License: MIT
URL: https://github.com/sdispater/clikit
Source0: %{pypi_source}
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
%description
Tests building with the poetry build backend.
%package -n python3-%{pypi_name}
Summary: %{summary}
%description -n python3-%{pypi_name}
%{summary}.
%prep
%autosetup -p1 -n %{pypi_name}-%{version}
%generate_buildrequires
%pyproject_buildrequires
%build
%pyproject_wheel
%install
%pyproject_install
%check
# Internal check that the RECORD and REQUESTED files are
# always removed in %%pyproject_wheel
test ! $(find %{buildroot}%{python3_sitelib}/ | grep -E "\.dist-info/RECORD$")
test ! $(find %{buildroot}%{python3_sitelib}/ | grep -E "\.dist-info/REQUESTED$")
%files -n python3-%{pypi_name}
%doc README.md
%license LICENSE
%{python3_sitelib}/%{pypi_name}/
%{python3_sitelib}/%{pypi_name}-%{version}.dist-info/

View File

@ -0,0 +1,50 @@
Name: python-distroinfo
Version: 0.3.2
Release: 0%{?dist}
Summary: Parsing and querying distribution metadata stored in text/YAML files
License: ASL 2.0
URL: https://github.com/softwarefactory-project/distroinfo
Source0: %{pypi_source distroinfo}
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
BuildRequires: python3-devel
BuildRequires: python3-pytest
BuildRequires: git-core
%description
This package uses setuptools and pbr.
It has setup_requires and tests that %%pyproject_buildrequires correctly
handles that including runtime requirements.
%package -n python3-distroinfo
Summary: %{summary}
%description -n python3-distroinfo
...
%prep
%autosetup -p1 -n distroinfo-%{version}
%generate_buildrequires
%pyproject_buildrequires -r
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files distroinfo
%check
%pytest
%files -n python3-distroinfo -f %{pyproject_files}
%doc README.rst AUTHORS
%license LICENSE

67
tests/python-django.spec Normal file
View File

@ -0,0 +1,67 @@
Name: python-django
Version: 3.0.7
Release: 0%{?dist}
Summary: A high-level Python Web framework
License: BSD
URL: https://www.djangoproject.com/
Source0: %{pypi_source Django}
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
BuildRequires: python3-devel
%description
This package contains lang files.
Building this tests that lang files are marked with %%lang in filelist.
%package -n python3-django
Summary: %{summary}
%description -n python3-django
...
%prep
%autosetup -p1 -n Django-%{version}
%py3_shebang_fix django/conf/project_template/manage.py-tpl django/bin/django-admin.py
%if 0%{?fedora} < 32 && 0%{?rhel} < 9
# Python RPM dependency generator doesn't support ~= yet
# https://bugzilla.redhat.com/show_bug.cgi?id=1758141
sed -i 's/asgiref ~= /asgiref >= /' setup.py
%endif
%generate_buildrequires
%pyproject_buildrequires
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files django
# remove .po files
find %{buildroot} -name "*.po" | xargs rm -f
%check
# Internal check if generated lang entries are same as
# the ones generated using %%find_lang
%find_lang django
%find_lang djangojs
grep '^%%lang' %{pyproject_files} | sort > tested.lang
sort django.lang djangojs.lang > expected.lang
diff tested.lang expected.lang
%files -n python3-django -f %{pyproject_files}
%doc README.rst
%license LICENSE
%{_bindir}/django-admin
%{_bindir}/django-admin.py

View File

@ -0,0 +1,73 @@
Name: python-dns-lexicon
Version: 3.4.0
Release: 0%{?dist}
Summary: Manipulate DNS records on various DNS providers in a standardized/agnostic way
License: MIT
URL: https://github.com/AnalogJ/lexicon
Source0: %{url}/archive/v%{version}/lexicon-%{version}.tar.gz
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
BuildRequires: python3-devel
%description
This package has extras specified in tox configuration,
we test that the extras are installed when -e is used.
This package also uses a custom toxenv and creates several extras subpackages.
%package -n python3-dns-lexicon
Summary: %{summary}
%description -n python3-dns-lexicon
...
%pyproject_extras_subpackage -n python3-dns-lexicon plesk route53
%prep
%autosetup -n lexicon-%{version}
# The tox configuration lists a [dev] extra, but that installs nothing (is missing).
# The test requirements are only specified via poetry.dev-dependencies.
# Here we amend the data a bit so we can test more things, adding the tests deps to the dev extra:
sed -i \
's/\[tool.poetry.extras\]/'\
'pytest = {version = ">3", optional = true}\n'\
'vcrpy = {version = ">1", optional = true}\n\n'\
'[tool.poetry.extras]\n'\
'dev = ["pytest", "vcrpy"]/' pyproject.toml
%generate_buildrequires
%if 0%{?fedora} >= 33 || 0%{?rhel} >= 9
# We use the "light" toxenv because the default one installs the [full] extra and we don't have all the deps.
# Note that [full] contains [plesk] and [route53] but we specify them manually instead:
%pyproject_buildrequires -e light -x plesk -x route53
%else
# older Fedoras don't have the required runtime dependencies, so we don't test it there
%pyproject_buildrequires
%endif
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files lexicon
%if 0%{?fedora} >= 33 || 0%{?rhel} >= 9
%check
# we cannot use %%tox here, because the configured commands call poetry directly :/
# we use %%pytest instead, running a subset of tests not to waste CI time
%pytest -k "test_route53 or test_plesk"
%endif
%files -n python3-dns-lexicon -f %{pyproject_files}
%license LICENSE
%doc README.rst
%{_bindir}/lexicon

View File

@ -0,0 +1,51 @@
%global pypi_name entrypoints
Name: python-%{pypi_name}
Version: 0.3
Release: 0%{?dist}
Summary: Discover and load entry points from installed packages
License: MIT
URL: https://entrypoints.readthedocs.io/
Source0: %{pypi_source}
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
%description
This package contains one .py module
Building this tests the flit build backend.
%package -n python3-%{pypi_name}
Summary: %{summary}
%description -n python3-%{pypi_name}
%{summary}.
%prep
%autosetup -p1 -n %{pypi_name}-%{version}
%generate_buildrequires
%pyproject_buildrequires
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files entrypoints
%check
# Internal check: Top level __pycache__ is never owned
grep -vE '/__pycache__$' %{pyproject_files}
grep -vE '/__pycache__/$' %{pyproject_files}
grep -F '/__pycache__/' %{pyproject_files}
%files -n python3-%{pypi_name} -f %{pyproject_files}
%doc README.rst
%license LICENSE

View File

@ -0,0 +1,47 @@
Name: python-flit-core
Version: 3.0.0
Release: 0%{?dist}
Summary: Distribution-building parts of Flit
License: BSD
URL: https://pypi.org/project/flit-core/
Source0: %{pypi_source flit_core}
BuildArch: noarch
BuildRequires: python3-devel
BuildRequires: pyproject-rpm-macros
%description
Test a build with pyproject.toml backend-path = .
flit-core builds with flit-core.
%package -n python3-flit-core
Summary: %{summary}
%description -n python3-flit-core
...
%prep
%autosetup -p1 -n flit_core-%{version}
%generate_buildrequires
%pyproject_buildrequires
%build
%if 0%{?fedora} < 33 && 0%{?rhel} < 9
# the old pip version cannot handle backend-path properly, let's help it:
export PYTHONPATH=$PWD
%endif
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files flit_core
%files -n python3-flit-core -f %{pyproject_files}

66
tests/python-httpbin.spec Normal file
View File

@ -0,0 +1,66 @@
Name: python-httpbin
Version: 0.7.0
Release: 0%{?dist}
Summary: HTTP Request & Response Service, written in Python + Flask
License: MIT
URL: https://github.com/Runscope/httpbin
Source0: %{url}/archive/v%{version}/httpbin-%{version}.tar.gz
BuildArch: noarch
BuildRequires: python3-devel
BuildRequires: pyproject-rpm-macros
%description
This package buildrequires a package with extra: raven[flask].
%package -n python3-httpbin
Summary: %{summary}
%if 0%{?fedora} < 33 && 0%{?rhel} < 9
# Old Fedoras don't understand Python extras yet
# This package needs raven[flask]
# So we add the transitive dependencies manually:
BuildRequires: %{py3_dist blinker flask}
Requires: %{py3_dist blinker flask}
%endif
%description -n python3-httpbin
%{summary}.
%prep
%autosetup -n httpbin-%{version}
# brotlipy wrapper is not packaged, httpbin works fine with brotli
sed -i s/brotlipy/brotli/ setup.py
# update test_httpbin.py to reflect new behavior of werkzeug
sed -i /Content-Length/d test_httpbin.py
%generate_buildrequires
%pyproject_buildrequires -t
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files httpbin
%check
%tox
# Internal check for our macros
# The runtime dependencies contain raven[flask], we assert we got them.
# The %%tox above also dies without it, but this makes it more explicit
%{python3} -c 'import blinker, flask' # transitive deps
%files -n python3-httpbin -f %{pyproject_files}
%doc README*
%license LICENSE*

View File

@ -0,0 +1,41 @@
Name: python-ipykernel
Version: 5.2.1
Release: 0%{?dist}
Summary: IPython Kernel for Jupyter
License: BSD
URL: https://github.com/ipython/ipykernel
Source0: https://github.com/ipython/ipykernel/archive/v%{version}/ipykernel-%{version}.tar.gz
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
BuildRequires: python3-devel
%description
This package contains data files.
Building this tests that data files are not listed when +auto is not used
with %%pyproject_save_files.
%package -n python3-ipykernel
Summary: %{summary}
%description -n python3-ipykernel
...
%prep
%autosetup -p1 -n ipykernel-%{version}
%generate_buildrequires
%pyproject_buildrequires
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files 'ipykernel*' +auto
%files -n python3-ipykernel -f %{pyproject_files}
%license COPYING.md
%doc README.md

55
tests/python-isort.spec Normal file
View File

@ -0,0 +1,55 @@
%global modname isort
Name: python-%{modname}
Version: 4.3.21
Release: 7%{?dist}
Summary: Python utility / library to sort Python imports
License: MIT
URL: https://github.com/timothycrosley/%{modname}
Source0: %{url}/archive/%{version}-2/%{modname}-%{version}-2.tar.gz
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
%description
This package contains executables.
Building this tests that executables are not listed when +auto is not used
with %%pyproject_save_files.
%package -n python3-%{modname}
Summary: %{summary}
%description -n python3-%{modname}
%{summary}.
%prep
%autosetup -n %{modname}-%{version}-2
%generate_buildrequires
%pyproject_buildrequires -r
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files isort
%check
# Internal check if the instalation outputs expected result
test -d %{buildroot}%{python3_sitelib}/%{modname}/
test -d %{buildroot}%{python3_sitelib}/%{modname}-%{version}.dist-info/
# Internal check that executables are not present when +auto was not used with %%pyproject_save_files
grep -vF %{buildroot}%{_bindir}/%{modname} %{pyproject_files}
%files -n python3-%{modname} -f %{pyproject_files}
%doc README.rst *.md
%license LICENSE
%{_bindir}/%{modname}

81
tests/python-ldap.spec Normal file
View File

@ -0,0 +1,81 @@
Name: python-ldap
Version: 3.3.0
Release: 0%{?dist}
License: Python
Summary: An object-oriented API to access LDAP directory servers
Source0: %{pypi_source}
BuildRequires: pyproject-rpm-macros
BuildRequires: cyrus-sasl-devel
BuildRequires: gcc
BuildRequires: openldap-clients
BuildRequires: openldap-devel
BuildRequires: openldap-servers
BuildRequires: openssl-devel
%description
This package contains extension modules. Does not contain pyproject.toml.
Has multiple files and directories.
Building this tests:
- the proper files are installed in the proper places
- module glob in %%pyproject_save_files (some modules are included, some not)
- combined manual and generated Buildrequires
%package -n python3-ldap
Summary: %{summary}
%description -n python3-ldap
%{summary}
%prep
%autosetup
%generate_buildrequires
%pyproject_buildrequires -t
%build
%pyproject_wheel
%install
%pyproject_install
# We can pass multiple globs
%pyproject_save_files 'ldap*' '*ldap'
%check
%tox
# Internal check if the instalation outputs expected files
test -d %{buildroot}%{python3_sitearch}/__pycache__/
test -d %{buildroot}%{python3_sitearch}/python_ldap-%{version}.dist-info/
test -d %{buildroot}%{python3_sitearch}/ldap/
test -f %{buildroot}%{python3_sitearch}/ldapurl.py
test -f %{buildroot}%{python3_sitearch}/ldif.py
test -d %{buildroot}%{python3_sitearch}/slapdtest/
test -f %{buildroot}%{python3_sitearch}/_ldap.cpython-*.so
# Internal check: Unmatched modules are not supposed to be listed in %%{pyproject_files}
# We'll list them explicitly
grep -vF %{python3_sitearch}/ldif.py %{pyproject_files}
grep -vF %{python3_sitearch}/__pycache__/ldif.cpython-%{python3_version_nodots}.pyc %{pyproject_files}
grep -vF %{python3_sitearch}/__pycache__/ldif.cpython-%{python3_version_nodots}.opt-1.pyc %{pyproject_files}
grep -vF %{python3_sitearch}/slapdtest/ %{pyproject_files}
# Internal check: Top level __pycache__ is never owned
grep -vE '/__pycache__$' %{pyproject_files}
grep -vE '/__pycache__/$' %{pyproject_files}
%files -n python3-ldap -f %{pyproject_files}
%license LICENCE
%doc CHANGES README TODO Demo
# Explicitly listed files can be combined with automation
%pycached %{python3_sitearch}/ldif.py
%{python3_sitearch}/slapdtest/

58
tests/python-mistune.spec Normal file
View File

@ -0,0 +1,58 @@
Name: python-mistune
Version: 0.8.3
Release: 11%{?dist}
Summary: Markdown parser for Python
License: BSD
URL: https://github.com/lepture/mistune
Source0: %{url}/archive/v%{version}.tar.gz
BuildRequires: gcc
BuildRequires: pyproject-rpm-macros
# optional dependency, listed explicitly to have the extension module:
BuildRequires: python3-Cython
%description
This package contains an extension module. Does not contain pyproject.toml.
Has a script (.py) and extension (.so) with identical name.
Building this tests:
- installing both a script and an extension with the same name
- default build backend without pyproject.toml
%package -n python3-mistune
Summary: %summary
%description -n python3-mistune
%{summary}
%prep
%autosetup -n mistune-%{version}
%generate_buildrequires
%pyproject_buildrequires
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files mistune
%check
# Internal check for our macros
# making sure that pyproject_install outputs these files so that we can test behaviour of %%pyproject_save_files
# when a package has multiple files with the same name (here script and extension)
test -f %{buildroot}%{python3_sitearch}/mistune.py
test -f %{buildroot}%{python3_sitearch}/mistune.cpython-*.so
%files -n python3-mistune -f %{pyproject_files}
%doc README.rst
%license LICENSE

View File

@ -0,0 +1,52 @@
%global pypi_name openqa_client
Name: python-%{pypi_name}
Version: 4.0.0
Release: 1%{?dist}
Summary: Python client library for openQA API
License: GPLv2+
URL: https://github.com/os-autoinst/openQA-python-client
Source0: %{pypi_source}
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
%description
This package uses tox.ini file with recursive deps (via the -r option).
%package -n python3-%{pypi_name}
Summary: %{summary}
%description -n python3-%{pypi_name}
%{summary}.
%prep
%autosetup -p1 -n %{pypi_name}-%{version}
# setuptools-git is needed to build the source distribution, but not
# for packaging, which *starts* from the source distribution
# we sed it out to save ourselves a dependency, but that is not strictly required
sed -i -e 's., "setuptools-git"..g' pyproject.toml
%generate_buildrequires
%pyproject_buildrequires -t
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files %{pypi_name}
%check
%tox
%files -n python3-%{pypi_name} -f %{pyproject_files}
%doc README.*
%license COPYING

54
tests/python-pluggy.spec Normal file
View File

@ -0,0 +1,54 @@
%global pypi_name pluggy
Name: python-%{pypi_name}
Version: 0.13.0
Release: 1%{?dist}
Summary: The plugin manager stripped of pytest specific details
License: MIT
URL: https://github.com/pytest-dev/pluggy
Source0: %{pypi_source}
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
%description
A pure Python library. The package contains tox.ini. Does not contain executables.
Building this tests:
- generating runtime and testing dependencies
- running tests with %%tox
- the %%pyproject_save_files +auto option works without actual executables
- pyproject.toml with the setuptools backend and setuptools-scm
%package -n python3-%{pypi_name}
Summary: %{summary}
%description -n python3-%{pypi_name}
%{summary}.
%prep
%autosetup -p1 -n %{pypi_name}-%{version}
%generate_buildrequires
%pyproject_buildrequires -t
%build
%pyproject_wheel
%install
%pyproject_install
# There are no executables, but we are allowed to pass +auto anyway
%pyproject_save_files pluggy +auto
%check
%tox
%files -n python3-%{pypi_name} -f %{pyproject_files}
%doc README.rst
%license LICENSE

View File

@ -0,0 +1,49 @@
Name: python-poetry-core
Version: 1.0.0
Release: 0%{?dist}
Summary: Poetry PEP 517 Build Backend
License: MIT
URL: https://pypi.org/project/poetry-core/
Source0: %{pypi_source poetry-core}
BuildArch: noarch
BuildRequires: python3-devel
BuildRequires: pyproject-rpm-macros
%description
Test a build with pyproject.toml backend-path = [.]
poetry-core builds with poetry-core.
%package -n python3-poetry-core
Summary: %{summary}
%description -n python3-poetry-core
...
%prep
%autosetup -p1 -n poetry-core-%{version}
%generate_buildrequires
%pyproject_buildrequires
%build
%if 0%{?fedora} < 33 && 0%{?rhel} < 9
# the old pip version cannot handle backend-path properly, let's help it:
export PYTHONPATH=$PWD
%endif
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files poetry
%files -n python3-poetry-core -f %{pyproject_files}
%doc README.md
%license LICENSE

53
tests/python-pytest.spec Normal file
View File

@ -0,0 +1,53 @@
%global pypi_name pytest
Name: python-%{pypi_name}
Version: 4.4.2
Release: 0%{?dist}
Summary: Simple powerful testing with Python
License: MIT
URL: https://pytest.org
Source0: %{pypi_source}
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
%description
This is a pure Python package with executables. It has a test suite in tox.ini
and test dependencies specified via the [test] extra.
Building this tests:
- generating runtime and test dependencies by both tox.ini and extras
- pyproject.toml with the setuptools backend and setuptools-scm
- passing arguments into %%tox
%package -n python3-%{pypi_name}
Summary: %{summary}
%description -n python3-%{pypi_name}
%{summary}.
%prep
%autosetup -p1 -n %{pypi_name}-%{version}
%generate_buildrequires
%pyproject_buildrequires -x testing -t
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files '*pytest' +auto
%check
# Only run one test (which uses a test-only dependency, hypothesis)
# See how to pass options trough the macro to tox, trough tox to pytest
%tox -- -- -k metafunc
%files -n python3-%{pypi_name} -f %{pyproject_files}
%doc README.rst
%doc CHANGELOG.rst
%license LICENSE

View File

@ -0,0 +1,56 @@
Name: python-requests
Version: 2.24.0
Release: 0%{?dist}
Summary: Requests is an elegant and simple HTTP library for Python
License: ASL 2.0
URL: https://requests.readthedocs.io/
Source0: %{pypi_source requests}
BuildArch: noarch
BuildRequires: python3-devel
BuildRequires: pyproject-rpm-macros
%description
This package uses multiple extras in %%pyproject_extras_subpkg and in
%%pyproject_buildrequires.
This test is mostly obsoleted by python-dns-lexicon.spec on Fedora 33+,
but we keep it around until Fedora 32 EOL.
%package -n python3-requests
Summary: %{summary}
%description -n python3-requests
%{summary}.
%pyproject_extras_subpkg -n python3-requests security socks
%prep
%autosetup -n requests-%{version}
%generate_buildrequires
%pyproject_buildrequires -x security,socks
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files requests
%check
# Internal check for our macros
# making sure that %%pyproject_buildrequires pulled in deps for both extras
%{python3} -c 'import cryptography, socks'
%files -n python3-requests -f %{pyproject_files}
%doc README.*
%license LICENSE

View File

@ -0,0 +1,55 @@
Name: python-setuptools_scm
Version: 3.5.0
Release: 0%{?dist}
Summary: The blessed package to manage your versions by SCM tags
License: MIT
URL: https://github.com/pypa/setuptools_scm/
Source0: %{pypi_source setuptools_scm}
BuildArch: noarch
BuildRequires: python3-devel
BuildRequires: pyproject-rpm-macros
%description
Here we test that %%pyproject_extras_subpkg works and generates
setuptools_scm[toml] extra subpackage.
Note that it only works on Fedora 33+.
%package -n python3-setuptools_scm
Summary: %{summary}
%description -n python3-setuptools_scm
...
%pyproject_extras_subpkg -n python3-setuptools_scm toml
%prep
%autosetup -p1 -n setuptools_scm-%{version}
%generate_buildrequires
%pyproject_buildrequires
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files setuptools_scm
%check
# Internal check for our macros
# making sure that %%{pyproject_ghost_distinfo} has the right content
test -f %{pyproject_ghost_distinfo}
test "$(cat %{pyproject_ghost_distinfo})" == "%ghost %{python3_sitelib}/setuptools_scm-%{version}.dist-info"
%files -n python3-setuptools_scm -f %{pyproject_files}
%doc README.rst
%doc CHANGELOG.rst
%license LICENSE

View File

@ -0,0 +1,46 @@
Name: python-zope-event
Version: 4.2.0
Release: 0%{?dist}
Summary: Zope Event Publication
License: ZPLv2.1
URL: https://pypi.python.org/pypi/zope.event/
Source0: %{pypi_source zope.event}
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
BuildRequires: python3-devel
%description
This package contains .pth files.
Building this tests that .pth files are not listed when +auto is not used
with %%pyproject_save_files.
%package -n python3-zope-event
Summary: %{summary}
%description -n python3-zope-event
...
%prep
%setup -q -n zope.event-%{version}
%generate_buildrequires
%pyproject_buildrequires
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files zope +auto
%check
# Internal check that the RECORD and REQUESTED files are
# always removed in %%pyproject_wheel
test ! $(find %{buildroot}%{python3_sitelib}/ | grep -E "\.dist-info/RECORD$")
test ! $(find %{buildroot}%{python3_sitelib}/ | grep -E "\.dist-info/REQUESTED$")
%files -n python3-zope-event -f %{pyproject_files}
%doc README.rst
%license LICENSE.txt

79
tests/tests.yml Normal file
View File

@ -0,0 +1,79 @@
---
- hosts: localhost
tags:
- classic
tasks:
- dnf:
name: "*"
state: latest
- hosts: localhost
roles:
- role: standard-test-basic
tags:
- classic
tests:
- pytest:
dir: .
run: ./mocktest.sh python-pytest
- entrypoints:
dir: .
run: ./mocktest.sh python-entrypoints
- pluggy:
dir: .
run: ./mocktest.sh python-pluggy
- clikit:
dir: .
run: ./mocktest.sh python-clikit
- distroinfo:
dir: .
run: ./mocktest.sh python-distroinfo
- tldr:
dir: .
run: ./mocktest.sh tldr
- openqa_client:
dir: .
run: ./mocktest.sh python-openqa_client
- httpbin:
dir: .
run: ./mocktest.sh python-httpbin
- ldap:
dir: .
run: ./mocktest.sh python-ldap
- isort:
dir: .
run: ./mocktest.sh python-isort
- mistune:
dir: .
run: ./mocktest.sh python-mistune
- setuptools_scm:
dir: .
run: ./mocktest.sh python-setuptools_scm
- requests:
dir: .
run: ./mocktest.sh python-requests
- ipykernel:
dir: .
run: ./mocktest.sh python-ipykernel
- zope:
dir: .
run: ./mocktest.sh python-zope-event
- django:
dir: .
run: ./mocktest.sh python-django
- printrun:
dir: .
run: ./mocktest.sh printrun
- dns_lexicon:
dir: .
run: ./mocktest.sh python-dns-lexicon
- flit_core:
dir: .
run: ./mocktest.sh python-flit-core
- poetry_core:
dir: .
run: ./mocktest.sh python-poetry-core
required_packages:
- mock
- rpmdevtools
- rpm-build

45
tests/tldr.spec Normal file
View File

@ -0,0 +1,45 @@
Name: tldr
Version: 0.4.4
Release: 1%{?dist}
Summary: Simplified and community-driven man pages
License: MIT
URL: https://github.com/tldr-pages/tldr-python-client
Source0: %{pypi_source}
BuildArch: noarch
BuildRequires: pyproject-rpm-macros
%description
A Python package containing executables.
Building this tests:
- there are no bytecompiled files in %%{_bindir}
- the executable's shebang is adjusted properly
- file direct_url.json isn't created
%prep
%autosetup -n %{name}-%{version}
%generate_buildrequires
%pyproject_buildrequires
%build
%pyproject_wheel
%install
%pyproject_install
%pyproject_save_files tldr +auto
%check
# Internal check for our macros: tests we don't ship __pycache__ in bindir
test ! -d %{buildroot}%{_bindir}/__pycache__
# Internal check for our macros: tests we have a proper shebang line
head -n1 %{buildroot}%{_bindir}/%{name}.py | grep -E '#!\s*%{python3}\s+%{py3_shbang_opts}\s*$'
# Internal check for our macros: tests that direct_url.json file wasn't created
test ! -e %{buildroot}%{python3_sitelib}/*.dist-info/direct_url.json
%files -f %pyproject_files
%license LICENSE
%doc README.md