From 6143f72e9b6ee2380e1cfe942fa6ff687e9120c6 Mon Sep 17 00:00:00 2001 From: jchzhou Date: Thu, 13 Mar 2025 11:15:40 +0800 Subject: [PATCH] Update to 1.17.0 Signed-off-by: jchzhou --- macros.aaa-pyproject-srpm | 4 +- macros.pyproject | 97 ++- pyproject-rpm-macros.spec | 21 +- pyproject_buildrequires.py | 214 ++++++- pyproject_buildrequires_testcases.yaml | 848 ++++++++++++++++++++----- pyproject_save_files.py | 26 +- test_pyproject_buildrequires.py | 25 +- 7 files changed, 1014 insertions(+), 221 deletions(-) diff --git a/macros.aaa-pyproject-srpm b/macros.aaa-pyproject-srpm index 9bfe84e..b394be5 100644 --- a/macros.aaa-pyproject-srpm +++ b/macros.aaa-pyproject-srpm @@ -3,8 +3,8 @@ # When this file is installed but macros.pyproject is not # this macro will cause the package with the real macro to be installed. # When macros.pyproject is installed, it overrides this macro. -# Note: This needs to maintain the same set of options as the real macro. -%pyproject_buildrequires(rRxtNwe:C:) echo 'pyproject-rpm-macros' && exit 0 +# Note: This takes arbitrary options, to ease addition of new options to the real macro. +%pyproject_buildrequires(-) echo 'pyproject-rpm-macros' && exit 0 # Declarative buildsystem, requires RPM 4.20+ to work diff --git a/macros.pyproject b/macros.pyproject index 7a3a898..4c143a3 100644 --- a/macros.pyproject +++ b/macros.pyproject @@ -25,6 +25,9 @@ %_pyproject_record %{_builddir}/%{_pyproject_files_prefix}-pyproject-record %_pyproject_buildrequires %{_builddir}/%{_pyproject_files_prefix}-pyproject-buildrequires +# Internal macro, takes %%set_build_flags and strips all the exports +%_pyproject_build_flags %{lua:local exports = rpm.expand('%{set_build_flags} ;'); print((exports:gsub('%s*;+%s+export%s+[%u_]+%s*;+%s*', ' ')))} + # Avoid leaking %%{_pyproject_builddir} to pytest collection # https://bugzilla.redhat.com/show_bug.cgi?id=1935212 # The value is read and used by the %%pytest and %%tox macros: @@ -33,7 +36,8 @@ %pyproject_wheel(C:) %{expand:\\\ %_set_pytest_addopts mkdir -p "%{_pyproject_builddir}" -CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}" TMPDIR="%{_pyproject_builddir}" \\\ +%{_pyproject_build_flags} \\\ +TMPDIR="%{_pyproject_builddir}" \\\ %{__python3} -Bs %{_rpmconfigdir}/openEuler/pyproject_wheel.py %{?**} %{_pyproject_wheeldir} } @@ -66,6 +70,48 @@ done echo $(IFS=:; echo "${pyproject_build_lib[*]}") )} +########## +# oE specific: may conflict with #pyproject_install macro at https://gitee.com/src-openeuler/openEuler-rpm-config/blob/master/macros.python +# use %pyproject3_install() instead +########## +%pyproject_install() %{expand:\\\ +specifier=$(ls %{_pyproject_wheeldir}/*.whl | xargs basename --multiple | sed -E 's/([^-]+)-([^-]+)-.+\\\.whl/\\\1==\\\2/') +if [ -z $specifier ]; then + echo 'ERROR: %%%%pyproject_install found no wheel in %%%%{_pyproject_wheeldir} %{_pyproject_wheeldir}' >&2 + exit 1 +fi +TMPDIR="%{_pyproject_builddir}" %{__python3} -m pip install --root %{buildroot} --prefix %{_prefix} --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 + 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}/openEuler \\ + %{__python3} -B %{_rpmconfigdir}/openEuler/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" >&2 + rm %{_pyproject_ghost_distinfo} # any attempt to use this will fail +fi +} %pyproject3_install() %{expand:\\\ specifier=$(ls %{_pyproject_wheeldir}/*.whl | xargs basename --multiple | sed -E 's/([^-]+)-([^-]+)-.+\\\.whl/\\\1==\\\2/') @@ -106,6 +152,9 @@ if [ $lines -ne 1 ]; then fi } +########## +# end +########## # 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:FaA) %{expand:%{?python_extras_subpkg:%{python_extras_subpkg%{?!-i:%{?!-f:%{?!-F: -f %{_pyproject_ghost_distinfo}}}} %**}}} @@ -115,7 +164,7 @@ fi # https://github.com/rpm-software-management/rpm/issues/1749#issuecomment-1020420616 # Since we support both ways, we pass either 4.19 or 4.18 to the script, so it knows which one to use # Rather than passing the actual version, we let RPM compare the versions, as it is easier done here than in Python -%pyproject_save_files(lL) %{expand:\\\ +%pyproject_save_files(lLM) %{expand:\\\ %{expr:v"0%{?rpmversion}" >= v"4.18.90" ? "RPM_FILES_ESCAPE=4.19" : "RPM_FILES_ESCAPE=4.18" } \\ %{__python3} %{_rpmconfigdir}/openEuler/pyproject_save_files.py \\ --output-files "%{pyproject_files}" \\ @@ -136,8 +185,7 @@ if [ ! -f "%{_pyproject_modules}" ]; then echo 'ERROR: %%%%pyproject_check_import only works when %%%%pyproject_save_files is used' >&2 exit 1 fi -# TODO support %py3_check_import macros -#%py3_check_import -f "%{_pyproject_modules}" %{?**} +%py3_check_import -f "%{_pyproject_modules}" %{?**} } @@ -152,13 +200,16 @@ fi %default_toxenv py%{python3_version_nodots} %toxenv %{default_toxenv} +%_pyproject_tomlidep %["%{python3_pkgversion}" == "3"\ + ? "echo '(python%{python3_pkgversion}dist(tomli) if python%{python3_pkgversion}-devel < 3.11)'"\ + : "%[v"%{python3_pkgversion}" < v"3.11"\ + ? "echo 'python%{python3_pkgversion}dist(tomli)'"\ + : "true # will use tomllib, echo nothing"\ + ]"\ + ] -# Note: Keep the options in sync with this macro from macros.aaa-pyproject-srpm -%pyproject_buildrequires(rRxtNwe:C:) %{expand:\\\ +%pyproject_buildrequires(rRxtNwpe:g:C:) %{expand:\\\ %_set_pytest_addopts -# The _auto_set_build_flags feature does not do this in %%generate_buildrequires section, -# but we want to get an environment consistent with %%build: -%{?_auto_set_build_flags:%set_build_flags} # The default flags expect the package note file to exist # see https://bugzilla.redhat.com/show_bug.cgi?id=2097535 %{?_package_note_flags:%_generate_package_note_file} @@ -168,6 +219,7 @@ fi %{-e:%{error:The -R and -e options are mutually exclusive}} %{-t:%{error:The -R and -t options are mutually exclusive}} %{-w:%{error:The -R and -w options are mutually exclusive}} +%{-p:%{error:The -R and -p options are mutually exclusive}} } %{-N: %{-r:%{error:The -N and -r options are mutually exclusive}} @@ -175,25 +227,29 @@ fi %{-e:%{error:The -N and -e options are mutually exclusive}} %{-t:%{error:The -N and -t options are mutually exclusive}} %{-w:%{error:The -N and -w options are mutually exclusive}} +%{-p:%{error:The -N and -p options are mutually exclusive}} %{-C:%{error:The -N and -C options are mutually exclusive}} +%{-g:if [ -f pyproject.toml ]; then + %_pyproject_tomlidep +fi} +} +%{-w: +%{!?__pyproject_buildrequires_w_warned:%{warn:The %%pyproject_buildrequires -w option is deprecated. +It's not efficient to build the wheel several times during the build. +The option is not scheduled for removal, but packagers should use the -p option instead. +}%global __pyproject_buildrequires_w_warned 1} +%{-p:%{error:The -w and -p options are mutually exclusive}} } %{-e:%{expand:%global toxenv %(%{__python3} -s %{_rpmconfigdir}/openEuler/pyproject_construct_toxenv.py %{?**})}} echo 'pyproject-rpm-macros' # first stdout line matches the implementation in macros.aaa-pyproject-srpm echo 'python%{python3_pkgversion}-devel' -echo 'python%{python3_pkgversion}dist(pip) >= 19' echo 'python%{python3_pkgversion}dist(packaging)' -%{!-N:if [ -f pyproject.toml ]; then - %["%{python3_pkgversion}" == "3" - ? "echo '(python%{python3_pkgversion}dist(tomli) if python%{python3_pkgversion}-devel < 3.11)'" - : "%[v"%{python3_pkgversion}" < v"3.11" - ? "echo 'python%{python3_pkgversion}dist(tomli)'" - : "true # will use tomllib, echo nothing" - ]" - ] +%{!-N:echo 'python%{python3_pkgversion}dist(pip) >= 19' +if [ -f pyproject.toml ]; then + %_pyproject_tomlidep elif [ -f setup.py ]; then # Note: If the default requirements change, also change them in the script! echo 'python%{python3_pkgversion}dist(setuptools) >= 40.8' - echo 'python%{python3_pkgversion}dist(wheel)' else echo 'ERROR: Neither pyproject.toml nor setup.py found, consider using %%%%pyproject_buildrequires -N if this is not a Python package.' >&2 exit 1 @@ -203,7 +259,8 @@ rm -rfv *.dist-info/ >&2 if [ -f %{__python3} ]; then mkdir -p "%{_pyproject_builddir}" echo -n > %{_pyproject_buildrequires} - CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}" TMPDIR="%{_pyproject_builddir}" \\\ + %{_pyproject_build_flags} \\\ + TMPDIR="%{_pyproject_builddir}" \\\ RPM_TOXENV="%{toxenv}" HOSTNAME="rpmbuild" %{__python3} -Bs %{_rpmconfigdir}/openEuler/pyproject_buildrequires.py %{?!_python_no_extras_requires:--generate-extras} --python3_pkgversion %{python3_pkgversion} --wheeldir %{_pyproject_wheeldir} --output %{_pyproject_buildrequires} %{?**} >&2 cat %{_pyproject_buildrequires} fi diff --git a/pyproject-rpm-macros.spec b/pyproject-rpm-macros.spec index fc8db25..7febf93 100644 --- a/pyproject-rpm-macros.spec +++ b/pyproject-rpm-macros.spec @@ -5,7 +5,7 @@ Name: pyproject-rpm-macros Summary: RPM macros for PEP 517 Python packages License: MIT -Version: 1.15.0 +Version: 1.17.0 Release: 1 URL: https://src.fedoraproject.org/rpms/pyproject-rpm-macros @@ -46,7 +46,9 @@ BuildRequires: python3-setuptools BuildRequires: python3dist(tox-current-env) >= 0.0.6 %endif BuildRequires: python3-wheel -BuildRequires: (python3dist(tomli) if python3 < 3.11) +# for fixing dep parsing failure on Eulermaker +# py311 already available on 24.03 series or newer +# BuildRequires: (python3dist(tomli) if python3 < 3.11) %endif BuildRequires: python-rpm-macros @@ -114,22 +116,14 @@ install -pm 644 pyproject_construct_toxenv.py %{buildroot}%{_rpmconfigdir}/openE install -pm 644 pyproject_requirements_txt.py %{buildroot}%{_rpmconfigdir}/openEuler/ install -pm 644 pyproject_wheel.py %{buildroot}%{_rpmconfigdir}/openEuler/ -%check -# assert the two signatures of %%pyproject_buildrequires match exactly -signature1="$(grep '^%%pyproject_buildrequires' macros.pyproject | cut -d' ' -f1)" -signature2="$(grep '^%%pyproject_buildrequires' macros.aaa-pyproject-srpm | cut -d' ' -f1)" -test "$signature1" == "$signature2" -# but also assert we are not comparing empty strings -test "$signature1" != "" - %if %{with tests} +%check export HOSTNAME="rpmbuild" # to speedup tox in network-less mock, see rhbz#1856356 -pytest -vv --doctest-modules %{?with_pytest_xdist:-n auto} %{!?with_tox_tests:-k "not tox"} +%pytest -vv --doctest-modules %{?with_pytest_xdist:-n auto} %{!?with_tox_tests:-k "not tox"} # brp-compress is provided as an argument to get the right directory macro expansion python3 compare_mandata.py -f %{_rpmconfigdir}/brp-compress %endif -rm -rf %{buildroot}%{_rpmconfigdir}/openEuler/__pycache__ %files %{_rpmmacrodir}/macros.pyproject @@ -150,5 +144,8 @@ rm -rf %{buildroot}%{_rpmconfigdir}/openEuler/__pycache__ %changelog +* Thu Mar 13 2025 jchzhou - 1.17.0-1 +- Upgrade to 1.17.0 + * Fri Sep 20 2024 misaka00251 - 1.15.0-1 - Init package diff --git a/pyproject_buildrequires.py b/pyproject_buildrequires.py index 515da95..da8a239 100644 --- a/pyproject_buildrequires.py +++ b/pyproject_buildrequires.py @@ -10,6 +10,7 @@ import subprocess import re import tempfile import email.parser +import functools import pathlib import zipfile @@ -34,6 +35,7 @@ def print_err(*args, **kwargs): try: + from packaging.markers import Marker from packaging.requirements import Requirement, InvalidRequirement from packaging.utils import canonicalize_name except ImportError as e: @@ -99,18 +101,23 @@ class Requirements: return True return False - def add(self, requirement_str, *, package_name=None, source=None): + def add(self, requirement, *, package_name=None, source=None, extra=None): """Output a Python-style requirement string as RPM dep""" + + requirement_str = str(requirement) print_err(f'Handling {requirement_str} from {source}') - try: - requirement = Requirement(requirement_str) - except InvalidRequirement: - hint = guess_reason_for_invalid_requirement(requirement_str) - message = f'Requirement {requirement_str!r} from {source} is invalid.' - if hint: - message += f' Hint: {hint}' - raise ValueError(message) + # requirements read initially from the metadata are strings + # further on we work with them as Requirement instances + if not isinstance(requirement, Requirement): + try: + requirement = Requirement(requirement) + except InvalidRequirement: + hint = guess_reason_for_invalid_requirement(requirement) + message = f'Requirement {requirement!r} from {source} is invalid.' + if hint: + message += f' Hint: {hint}' + raise ValueError(message) if requirement.url: print_err( @@ -118,10 +125,17 @@ class Requirements: ) name = canonicalize_name(requirement.name) + + if extra is not None: + extra_str = f'extra == "{extra}"' + if requirement.marker is not None: + extra_str = f'({requirement.marker}) and {extra_str}' + requirement.marker = Marker(extra_str) + if (requirement.marker is not None and not self.evaluate_all_environments(requirement)): print_err(f'Ignoring alien requirement:', requirement_str) - self.ignored_alien_requirements.append(requirement_str) + self.ignored_alien_requirements.append(requirement) return # Handle self-referencing requirements @@ -215,7 +229,8 @@ def toml_load(opened_binary_file): return tomllib.load(opened_binary_file) -def get_backend(requirements): +@functools.cache +def load_pyproject(): try: f = open('pyproject.toml', 'rb') except FileNotFoundError: @@ -223,6 +238,11 @@ def get_backend(requirements): else: with f: pyproject_data = toml_load(f) + return pyproject_data + + +def get_backend(requirements): + pyproject_data = load_pyproject() buildsystem_data = pyproject_data.get('build-system', {}) requirements.extend( @@ -248,15 +268,6 @@ def get_backend(requirements): # with pyproject.toml without a specified build backend. # If the default requirements change, also change them in the macro! requirements.add('setuptools >= 40.8', source='default build backend') - # PEP 517 doesn't mandate depending on wheel when the default backend is used. - # Historically, it used to be assumed as necessary, but later it turned out to be wrong. - # See the removal in pip and build: - # https://github.com/pypa/pip/pull/12449 - # https://github.com/pypa/build/pull/716 - # However, the requirement *will* be generated by setuptools anyway - # as part of get_requires_for_build_wheel(). - # So we might as well keep it to skip one redundant step. - requirements.add('wheel', source='default build backend') requirements.check(source='build backend') @@ -310,8 +321,9 @@ def generate_run_requirements_hook(backend, requirements): raise ValueError( 'The build backend cannot provide build metadata ' '(incl. runtime requirements) before build. ' - 'Use the provisional -w flag to build the wheel and parse the metadata from it, ' - 'or use the -R flag not to generate runtime dependencies.' + 'If the dependencies are specified in the pyproject.toml [project] ' + 'table, you can use the -p flag to read them. ' + 'Alternatively, use the -R flag not to generate runtime dependencies.' ) dir_basename = prepare_metadata('.', config_settings=requirements.config_settings) with open(dir_basename + '/METADATA') as metadata_file: @@ -368,8 +380,35 @@ def generate_run_requirements_wheel(backend, requirements, wheeldir): raise RuntimeError('Could not find *.dist-info/METADATA in built wheel.') -def generate_run_requirements(backend, requirements, *, build_wheel, wheeldir): - if build_wheel: +def generate_run_requirements_pyproject(requirements): + pyproject_data = load_pyproject() + + if not (project_table := pyproject_data.get('project', {})): + raise ValueError('Could not find the [project] table in pyproject.toml.') + + dynamic_fields = project_table.get('dynamic', []) + if 'dependencies' in dynamic_fields or 'optional-dependencies' in dynamic_fields: + raise ValueError('Could not read the dependencies or optional-dependencies ' + 'from the [project] table in pyproject.toml, as the field is dynamic.') + + dependencies = project_table.get('dependencies', []) + name = project_table.get('name') + requirements.extend(dependencies, + package_name=name, + source=f'pyproject.toml generated metadata: [dependencies] ({name})') + + optional_dependencies = project_table.get('optional-dependencies', {}) + for extra, dependencies in optional_dependencies.items(): + requirements.extend(dependencies, + package_name=name, + source=f'pyproject.toml generated metadata: [optional-dependencies] {extra} ({name})', + extra=extra) + + +def generate_run_requirements(backend, requirements, *, build_wheel, read_pyproject_dependencies, wheeldir): + if read_pyproject_dependencies: + generate_run_requirements_pyproject(requirements) + elif build_wheel: generate_run_requirements_wheel(backend, requirements, wheeldir) else: generate_run_requirements_hook(backend, requirements) @@ -419,6 +458,103 @@ def generate_tox_requirements(toxenv, requirements): source=f'tox --print-deps-only: {toxenv}') +def tox_dependency_groups(toxenv): + # We call this command separately instead of folding it into the previous one + # becasue --print-dependency-groups-to only works with tox 4.22+ and tox-current-env 0.0.14+. + # We handle failure gracefully: upstreams using dependency_groups should require tox >= 4.22. + toxenv = ','.join(toxenv) + with tempfile.NamedTemporaryFile('r') as groups: + r = subprocess.run( + [sys.executable, '-m', 'tox', + '--print-dependency-groups-to', groups.name, + '-q', '-e', toxenv], + check=False, + encoding='utf-8', + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + if r.returncode == 0: + if r.stdout: + print_err(r.stdout, end='') + if output := groups.read().strip(): + return output.splitlines() + return [] + + +def generate_dependency_groups(requested_groups, requirements): + """Adapted from https://peps.python.org/pep-0735/#reference-implementation (public domain)""" + from collections import defaultdict + + def _normalize_name(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() + + def _normalize_group_names(dependency_groups: dict) -> dict: + original_names = defaultdict(list) + normalized_groups = {} + + for group_name, value in dependency_groups.items(): + normed_group_name = _normalize_name(group_name) + original_names[normed_group_name].append(group_name) + normalized_groups[normed_group_name] = value + + errors = [] + for normed_name, names in original_names.items(): + if len(names) > 1: + errors.append(f"{normed_name} ({', '.join(names)})") + if errors: + raise ValueError(f"Duplicate dependency group names: {', '.join(errors)}") + + return normalized_groups + + def _resolve_dependency_group( + dependency_groups: dict, group: str, past_groups: tuple[str, ...] = () + ) -> list[str]: + if group in past_groups: + raise ValueError(f"Cyclic dependency group include: {group} -> {past_groups}") + + if group not in dependency_groups: + raise LookupError(f"Dependency group '{group}' not found") + + raw_group = dependency_groups[group] + if not isinstance(raw_group, list): + raise ValueError(f"Dependency group '{group}' is not a list") + + realized_group = [] + for item in raw_group: + if isinstance(item, str): + realized_group.append(item) + elif isinstance(item, dict): + if tuple(item.keys()) != ("include-group",): + raise ValueError(f"Invalid dependency group item: {item}") + + include_group = _normalize_name(next(iter(item.values()))) + realized_group.extend( + _resolve_dependency_group( + dependency_groups, include_group, past_groups + (group,) + ) + ) + else: + raise ValueError(f"Invalid dependency group item: {item}") + + return realized_group + + def resolve(dependency_groups: dict, group: str) -> list[str]: + if not isinstance(dependency_groups, dict): + raise TypeError("Dependency Groups table is not a dict") + return _resolve_dependency_group(dependency_groups, _normalize_name(group)) + + pyproject_data = load_pyproject() + dependency_groups_raw = pyproject_data.get("dependency-groups", {}) + dependency_groups = _normalize_group_names(dependency_groups_raw) + + for group_names in requested_groups: + for group_name in group_names.split(","): + requirements.extend( + resolve(dependency_groups, group_name), + source=f"Dependency group {group_name}", + ) + + def python3dist(name, op=None, version=None, python3_pkgversion="3"): prefix = f"python{python3_pkgversion}dist" @@ -431,9 +567,10 @@ def python3dist(name, op=None, version=None, python3_pkgversion="3"): def generate_requires( - *, include_runtime=False, build_wheel=False, wheeldir=None, toxenv=None, extras=None, + *, include_runtime=False, build_wheel=False, wheeldir=None, toxenv=None, extras=None, dependency_groups=None, get_installed_version=importlib.metadata.version, # for dep injection generate_extras=False, python3_pkgversion="3", requirement_files=None, use_build_system=True, + read_pyproject_dependencies=False, output, config_settings=None, ): """Generate the BuildRequires for the project in the current directory @@ -449,9 +586,10 @@ def generate_requires( config_settings=config_settings, ) + dependency_groups = dependency_groups or [] try: - if (include_runtime or toxenv) and not use_build_system: - raise ValueError('-N option cannot be used in combination with -r, -e, -t, -x options') + if (include_runtime or toxenv or read_pyproject_dependencies) and not use_build_system: + raise ValueError('-N option cannot be used in combination with -r, -e, -t, -x, -p options') if requirement_files: for req_file in requirement_files: requirements.extend( @@ -465,8 +603,12 @@ def generate_requires( if toxenv: include_runtime = True generate_tox_requirements(toxenv, requirements) + dependency_groups.extend(tox_dependency_groups(toxenv)) + if dependency_groups: + generate_dependency_groups(dependency_groups, requirements) if include_runtime: - generate_run_requirements(backend, requirements, build_wheel=build_wheel, wheeldir=wheeldir) + generate_run_requirements(backend, requirements, build_wheel=build_wheel, + read_pyproject_dependencies=read_pyproject_dependencies, wheeldir=wheeldir) except EndPass: return finally: @@ -493,7 +635,7 @@ def main(argv): help=argparse.SUPPRESS, ) parser.add_argument( - '-p', '--python3_pkgversion', metavar='PYTHON3_PKGVERSION', + '--python3_pkgversion', metavar='PYTHON3_PKGVERSION', default="3", help=argparse.SUPPRESS, ) parser.add_argument( @@ -508,6 +650,11 @@ def main(argv): help='comma separated list of "extras" for runtime requirements ' '(e.g. -x testing,feature-x) (implies --runtime, can be repeated)', ) + parser.add_argument( + '-g', '--dependency-groups', metavar='GROUPS', action='append', + help='comma separated list of dependency groups (PEP 735) for requirements ' + '(e.g. -g tests,docs) (can be repeated)', + ) parser.add_argument( '-t', '--tox', action='store_true', help=('generate test tequirements from tox environment ' @@ -521,7 +668,12 @@ def main(argv): parser.add_argument( '-w', '--wheel', action='store_true', default=False, help=('Generate run-time requirements by building the wheel ' - '(useful for build backends without the prepare_metadata_for_build_wheel hook)'), + '(useful for build backends without the prepare_metadata_for_build_wheel hook, deprecated)'), + ) + parser.add_argument( + '-p', '--read-pyproject-dependencies', action='store_true', default=False, + help=('Generate dependencies from [project] table of pyproject.toml ' + 'instead of calling prepare_metadata_for_build_wheel hook)'), ) parser.add_argument( '-R', '--no-runtime', action='store_false', dest='runtime', @@ -571,10 +723,12 @@ def main(argv): wheeldir=args.wheeldir, toxenv=args.toxenv, extras=args.extras, + dependency_groups=args.dependency_groups, generate_extras=args.generate_extras, python3_pkgversion=args.python3_pkgversion, requirement_files=args.requirement_files, use_build_system=args.use_build_system, + read_pyproject_dependencies=args.read_pyproject_dependencies, output=args.output, config_settings=parse_config_settings_args(args.config_settings), ) diff --git a/pyproject_buildrequires_testcases.yaml b/pyproject_buildrequires_testcases.yaml index 892e3c3..a374fa0 100644 --- a/pyproject_buildrequires_testcases.yaml +++ b/pyproject_buildrequires_testcases.yaml @@ -23,7 +23,6 @@ Insufficient version of setuptools: setup.py: | expected: | python3dist(setuptools) >= 40.8 - python3dist(wheel) result: 0 No pyproject.toml, empty setup.py: @@ -32,10 +31,12 @@ No pyproject.toml, empty setup.py: wheel: 1 include_runtime: false setup.py: | - expected: | - python3dist(setuptools) >= 40.8 - python3dist(wheel) - python3dist(wheel) + expected: + - | # setuptools 70+ + python3dist(setuptools) >= 40.8 + - | # setuptools < 70 + python3dist(setuptools) >= 40.8 + python3dist(wheel) result: 0 Default build system, empty setup.py: @@ -47,10 +48,12 @@ Default build system, empty setup.py: pyproject.toml: | # empty setup.py: | - expected: | - python3dist(setuptools) >= 40.8 - python3dist(wheel) - python3dist(wheel) + expected: + - | # setuptools 70+ + python3dist(setuptools) >= 40.8 + - | # setuptools < 70 + python3dist(setuptools) >= 40.8 + python3dist(wheel) result: 0 pyproject.toml with build-backend and setup.py: @@ -212,13 +215,18 @@ Default build system, build dependencies in setup.py: 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) + expected: + - | # setuptools 70+ + python3dist(setuptools) >= 40.8 + python3dist(foo) + (python3dist(bar) < 2 or python3dist(bar) > 2) + (python3dist(baz) >= 1.1.1 with python3dist(baz) < 1.2) + - | # setuptools < 70 + python3dist(setuptools) >= 40.8 + 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: @@ -234,13 +242,18 @@ Default build system, run dependencies in setup.py: 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.0 - python3dist(inst2) < 3~~ + expected: + - | # setuptools 70+ + python3dist(setuptools) >= 40.8 + python3dist(pyyaml) + python3dist(inst) > 1.0 + python3dist(inst2) < 3~~ + - | # setuptools < 70 + python3dist(setuptools) >= 40.8 + python3dist(wheel) + python3dist(pyyaml) + python3dist(inst) > 1.0 + python3dist(inst2) < 3~~ result: 0 Run dependencies with extras (not selected): @@ -287,18 +300,28 @@ Run dependencies with extras (not selected): 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 + expected: + - | # setuptools 70+ + python3dist(setuptools) >= 40.8 + 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 + - | # setuptools < 70 + python3dist(setuptools) >= 40.8 + 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): @@ -310,22 +333,36 @@ Run dependencies with extras (selected): 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) + expected: + - | # setuptools 70+ + python3dist(setuptools) >= 40.8 + 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) + - | # setuptools < 70 + python3dist(setuptools) >= 40.8 + 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: @@ -348,16 +385,24 @@ Run dependencies with multiple extras: 'cool-feature': ['dep4[FOO,BAR]'], }, ) - expected: | - python3dist(setuptools) >= 40.8 - python3dist(wheel) - python3dist(wheel) - python3dist(dep4) - python3dist(dep4[bar]) - python3dist(dep4[foo]) - python3dist(dep3) - python3dist(dep2) - python3dist(dep1) + expected: + - | # setuptools 70+ + python3dist(setuptools) >= 40.8 + python3dist(dep4) + python3dist(dep4[bar]) + python3dist(dep4[foo]) + python3dist(dep3) + python3dist(dep2) + python3dist(dep1) + - | # setuptools < 70 + python3dist(setuptools) >= 40.8 + python3dist(wheel) + python3dist(dep4) + python3dist(dep4[bar]) + python3dist(dep4[foo]) + python3dist(dep3) + python3dist(dep2) + python3dist(dep1) result: 0 Run dependencies with extras and build wheel option: @@ -371,23 +416,38 @@ Run dependencies with extras and build wheel option: extras: - testing setup.py: *pytest_setup_py - expected: | - python3dist(setuptools) >= 40.8 - python3dist(wheel) - python3dist(wheel) - python3dist(setuptools) >= 40 - python3dist(pip) >= 19 - 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) + expected: + - | # setuptools 70+ + python3dist(setuptools) >= 40.8 + python3dist(setuptools) >= 40 + python3dist(pip) >= 19 + 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) + - | # setuptools < 70 + python3dist(setuptools) >= 40.8 + python3dist(wheel) + python3dist(setuptools) >= 40 + python3dist(pip) >= 19 + 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 stderr_contains: "Reading metadata from {wheeldir}/pytest-6.6.6-py3-none-any.whl" @@ -416,17 +476,22 @@ tox dependencies: commands = true expected: - - | # tox 3 + - | # tox 3 with setuptools < 70 python3dist(setuptools) >= 40.8 python3dist(wheel) - python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(toxdep1) python3dist(toxdep2) python3dist(inst) - - | # tox 4 + - | # tox 4 with setuptools 70+ + python3dist(setuptools) >= 40.8 + python3dist(tox-current-env) >= 0.0.6 + python3dist(tox) + python3dist(toxdep1) + python3dist(toxdep2) + python3dist(inst) + - | # tox 4 with setuptools < 70 python3dist(setuptools) >= 40.8 - python3dist(wheel) python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(tox) @@ -468,10 +533,9 @@ tox extras: commands = true expected: - - | # tox 3 + - | # tox 3 with setuptools < 70 python3dist(setuptools) >= 40.8 python3dist(wheel) - python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(toxdep) python3dist(inst) @@ -482,9 +546,21 @@ tox extras: python3dist(dep23) python3dist(extra-dep) python3dist(extra-dep[extra_dep]) - - | # tox 4 + - | # tox 4 with setuptools 70+ + python3dist(setuptools) >= 40.8 + python3dist(tox-current-env) >= 0.0.6 + python3dist(tox) + python3dist(toxdep) + python3dist(inst) + python3dist(dep11) > 11.0 + python3dist(dep12) + python3dist(dep21) + python3dist(dep22) + python3dist(dep23) + python3dist(extra-dep) + python3dist(extra-dep[extra_dep]) + - | # tox 4 with setuptools < 70 python3dist(setuptools) >= 40.8 - python3dist(wheel) python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(tox) @@ -525,17 +601,22 @@ tox provision unsatisfied: toxdep1 toxdep2 expected: - - | # tox 3 + - | # tox 3 with setuptools < 70 python3dist(setuptools) >= 40.8 python3dist(wheel) - python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(tox) >= 3.999 python3dist(setuptools) > 40.0 python3dist(wheel) > 2.0 - - | # tox 4 + - | # tox 4 with setuptools 70+ + python3dist(setuptools) >= 40.8 + python3dist(tox-current-env) >= 0.0.6 + python3dist(tox) >= 3.999 + python3dist(setuptools) > 40.0 + python3dist(wheel) > 2.0 + python3dist(tox) >= 3.999 + - | # tox 4 with setuptools < 70 python3dist(setuptools) >= 40.8 - python3dist(wheel) python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(tox) >= 3.999 @@ -569,19 +650,25 @@ tox provision satisfied: toxdep1 toxdep2 expected: - - | # tox 3 + - | # tox 3 with setuptools < 70 python3dist(setuptools) >= 40.8 python3dist(wheel) - python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(tox) >= 3.5 python3dist(setuptools) > 40.0 python3dist(toxdep1) python3dist(toxdep2) python3dist(inst) - - | # tox 4 + - | # tox 4 with setuptools 70+ + python3dist(setuptools) >= 40.8 + python3dist(tox-current-env) >= 0.0.6 + python3dist(setuptools) > 40.0 + python3dist(tox) >= 3.5 + python3dist(toxdep1) + python3dist(toxdep2) + python3dist(inst) + - | # tox 4 with setuptools < 70 python3dist(setuptools) >= 40.8 - python3dist(wheel) python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(setuptools) > 40.0 @@ -611,16 +698,20 @@ tox provision no minversion: setuptools > 40 wheel > 2 expected: - - | # tox 3 + - | # tox 3 with setuptools < 70 python3dist(setuptools) >= 40.8 python3dist(wheel) - python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(setuptools) > 40.0 python3dist(wheel) > 2.0 - - | # tox 4 + - | # tox 4 with setuptools 70+ + python3dist(setuptools) >= 40.8 + python3dist(tox-current-env) >= 0.0.6 + python3dist(setuptools) > 40.0 + python3dist(wheel) > 2.0 + python3dist(tox) + - | # tox 4 with setuptools < 70 python3dist(setuptools) >= 40.8 - python3dist(wheel) python3dist(wheel) python3dist(tox-current-env) >= 0.0.6 python3dist(setuptools) > 40.0 @@ -677,15 +768,22 @@ Default build system, met deps in requirements file: SQLAlchemy>=1.0.10,<1.1.0 # Zebra protocol service requirement_files: - requirements.txt - expected: | - ((python3dist(lxml) < 3.7 or python3dist(lxml) > 3.7) with python3dist(lxml) >= 2.3) - python3dist(ncclient) - (python3dist(cryptography) < 1.5.2 or python3dist(cryptography) > 1.5.2) - python3dist(paramiko) - (python3dist(sqlalchemy) < 1.1~~ with python3dist(sqlalchemy) >= 1.0.10) - python3dist(setuptools) >= 40.8 - python3dist(wheel) - python3dist(wheel) + expected: + - | # setuptools 70+ + ((python3dist(lxml) < 3.7 or python3dist(lxml) > 3.7) with python3dist(lxml) >= 2.3) + python3dist(ncclient) + (python3dist(cryptography) < 1.5.2 or python3dist(cryptography) > 1.5.2) + python3dist(paramiko) + (python3dist(sqlalchemy) < 1.1~~ with python3dist(sqlalchemy) >= 1.0.10) + python3dist(setuptools) >= 40.8 + - | # setuptools < 70 + ((python3dist(lxml) < 3.7 or python3dist(lxml) > 3.7) with python3dist(lxml) >= 2.3) + python3dist(ncclient) + (python3dist(cryptography) < 1.5.2 or python3dist(cryptography) > 1.5.2) + python3dist(paramiko) + (python3dist(sqlalchemy) < 1.1~~ with python3dist(sqlalchemy) >= 1.0.10) + python3dist(setuptools) >= 40.8 + python3dist(wheel) result: 0 With pyproject.toml, requirements file and with -N option: @@ -850,11 +948,16 @@ Pre-releases are accepted: "cffi", ] build-backend = "setuptools.build_meta" - expected: | - python3dist(setuptools) - python3dist(wheel) - python3dist(cffi) - python3dist(wheel) + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(wheel) + python3dist(cffi) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(cffi) + python3dist(wheel) stderr_contains: "Requirement satisfied: cffi" result: 0 @@ -869,10 +972,12 @@ Stdout from wrapped subprocess does not appear in output: os.system('echo LEAK?') from setuptools import setup setup(name='test', version='0.1') - expected: | - python3dist(setuptools) >= 40.8 - python3dist(wheel) - python3dist(wheel) + expected: + - | # setuptools 70+ + python3dist(setuptools) >= 40.8 + - | # setuptools < 70 + python3dist(setuptools) >= 40.8 + python3dist(wheel) result: 0 pyproject.toml with runtime dependencies: @@ -892,10 +997,14 @@ pyproject.toml with runtime dependencies: "foo", 'importlib-metadata; python_version<"3.8"', ] - expected: | - python3dist(setuptools) - python3dist(wheel) - python3dist(foo) + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(foo) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(foo) result: 0 pyproject.toml with runtime dependencies and partially selected extras: @@ -906,7 +1015,7 @@ pyproject.toml with runtime dependencies and partially selected extras: tomli: 1 extras: - tests - pyproject.toml: | + pyproject.toml: &pyproject_with_extras | [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" @@ -920,12 +1029,18 @@ pyproject.toml with runtime dependencies and partially selected extras: [project.optional-dependencies] tests = ["pytest>=5", "pytest-mock"] docs = ["sphinx", "python-docs-theme"] - expected: | - python3dist(setuptools) - python3dist(wheel) - python3dist(foo) - python3dist(pytest) >= 5 - python3dist(pytest-mock) + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(foo) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(foo) + python3dist(pytest) >= 5 + python3dist(pytest-mock) result: 0 Self-referencing extras (sooner): @@ -951,14 +1066,22 @@ Self-referencing extras (sooner): tests = pytest>=5; pytest-mock docs = sphinx; python-docs-theme dev = my_package[docs,tests] - expected: | - python3dist(setuptools) - python3dist(wheel) - python3dist(foo) - python3dist(sphinx) - python3dist(python-docs-theme) - python3dist(pytest) >= 5 - python3dist(pytest-mock) + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(foo) + python3dist(sphinx) + python3dist(python-docs-theme) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(foo) + python3dist(sphinx) + python3dist(python-docs-theme) + python3dist(pytest) >= 5 + python3dist(pytest-mock) result: 0 Self-referencing extras (later): @@ -984,14 +1107,22 @@ Self-referencing extras (later): tests = pytest>=5; pytest-mock docs = sphinx; python-docs-theme xdev = my_package[docs,tests] - expected: | - python3dist(setuptools) - python3dist(wheel) - python3dist(foo) - python3dist(sphinx) - python3dist(python-docs-theme) - python3dist(pytest) >= 5 - python3dist(pytest-mock) + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(foo) + python3dist(sphinx) + python3dist(python-docs-theme) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(foo) + python3dist(sphinx) + python3dist(python-docs-theme) + python3dist(pytest) >= 5 + python3dist(pytest-mock) result: 0 Self-referencing extras (maze): @@ -1016,14 +1147,22 @@ Self-referencing extras (maze): forward = my_package[backward]; forwarddep backward = my_package[left,right]; backwarddep never = my_package[forward]; neverdep - expected: | - python3dist(setuptools) - python3dist(wheel) - python3dist(backwarddep) - python3dist(forwarddep) - python3dist(leftdep) - python3dist(rightdep) - python3dist(startdep) + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(backwarddep) + python3dist(forwarddep) + python3dist(leftdep) + python3dist(rightdep) + python3dist(startdep) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(backwarddep) + python3dist(forwarddep) + python3dist(leftdep) + python3dist(rightdep) + python3dist(startdep) result: 0 config_settings_control: @@ -1062,3 +1201,410 @@ config_settings: expected: | python3dist(test-config-setting) result: 0 + +pyproject.toml with runtime dependencies read from it: + skipif: not SETUPTOOLS_60 + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + tomli: 1 + pyproject.toml: | + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + [project] + name = "my_package" + version = "0.1" + dependencies = [ + "foo", + 'importlib-metadata; python_version<"3.8"', + ] + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(foo) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(foo) + result: 0 + +pyproject.toml with extras - only runtime dependencies read from it: + skipif: not SETUPTOOLS_60 + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + tomli: 1 + pyproject.toml: *pyproject_with_extras + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(foo) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(foo) + result: 0 + +pyproject.toml with runtime dependencies and partially selected extras read from it: + skipif: not SETUPTOOLS_60 + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + tomli: 1 + extras: + - tests + pyproject.toml: *pyproject_with_extras + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(foo) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(foo) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + result: 0 + +pyproject.toml with runtime dependencies and all extras read from it: + skipif: not SETUPTOOLS_60 + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + tomli: 1 + extras: + - tests + - docs + pyproject.toml: *pyproject_with_extras + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(foo) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + python3dist(sphinx) + python3dist(python-docs-theme) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(foo) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + python3dist(sphinx) + python3dist(python-docs-theme) + result: 0 + +pyproject.toml without dependencies: + skipif: not SETUPTOOLS_60 + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + tomli: 1 + pyproject.toml: | + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + [project] + name = "my_package" + version = "0.1" + expected: + - | # setuptools 70+ + python3dist(setuptools) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + result: 0 + +pyproject.toml without project table: + skipif: not SETUPTOOLS_60 + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + pyproject.toml: | + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + except: ValueError + +no pyproject.toml: + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + except: FileNotFoundError + +pyproject.toml with dynamic dependencies: + skipif: not SETUPTOOLS_60 + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + tomli: 1 + pyproject.toml: | + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + [project] + name = "my_package" + version = "0.1" + dynamic = ["dependencies"] + [tool.setuptools.dynamic] + dependencies = { file = ["deps.txt"] } + deps.txt: | + foo < 7.0 + sphinx + except: ValueError + +pyproject.toml with dynamic optional dependencies: + skipif: not SETUPTOOLS_60 + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + tomli: 1 + extras: + - docs + pyproject.toml: | + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + [project] + name = "my_package" + version = "0.1" + dynamic = ["optional-dependencies"] + [tool.setuptools.dynamic.optional-dependencies.docs] + file = ["deps.txt"] + deps.txt: | + sphinx~=7.0.1 + except: ValueError + +pyproject.toml with dynamic table and no deps: + skipif: not SETUPTOOLS_60 + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + tomli: 1 + pyproject.toml: | + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + [project] + name = "my_package" + version = "0.1" + dynamic = ["readme"] + [tool.setuptools.dynamic] + readme = { file = ["readme.txt"] } + readme.txt: | + nothing interesting here + expected: + - | # setuptools 70+ + python3dist(setuptools) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + result: 0 + +pyproject.toml with self-referencing extras: + skipif: not SETUPTOOLS_60 + read_pyproject_dependencies: true + installed: + setuptools: 50 + wheel: 1 + tomli: 1 + extras: + - test + pyproject.toml: | + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + [project] + name = "contourpy" + version = "0.1" + dependencies = ["numpy >= 1.23"] + [project.optional-dependencies] + bokeh = ["bokeh", "selenium"] + test = ["contourpy[test-no-images]", "matplotlib", "Pillow"] + test-no-images = ["pytest", "pytest-rerunfailures", "wurlitzer"] + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(numpy) >= 1.23 + python3dist(matplotlib) + python3dist(pillow) + python3dist(pytest) + python3dist(pytest-rerunfailures) + python3dist(wurlitzer) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(numpy) >= 1.23 + python3dist(matplotlib) + python3dist(pillow) + python3dist(pytest) + python3dist(pytest-rerunfailures) + python3dist(wurlitzer) + result: 0 + +pyproject.toml with dependency-groups not requested: + use_build_system: false + pyproject.toml: | + [dependency-groups] + tests = ["pytest>=5", "pytest-mock"] + docs = ["sphinx", "python-docs-theme"] + expected: "\n" + result: 0 + +pyproject.toml with dependency-groups and build system: + skipif: not SETUPTOOLS_60 + use_build_system: true + installed: + setuptools: 50 + wheel: 1 + tomli: 1 + dependency_groups: + - tests + pyproject.toml: | + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + [project] + name = "my_package" + version = "0.1" + [dependency-groups] + tests = ["pytest>=5", "pytest-mock"] + docs = ["sphinx", "python-docs-theme"] + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + result: 0 + +pyproject.toml with dependency-groups one requested: + use_build_system: false + dependency_groups: + - tests + pyproject.toml: | + [dependency-groups] + tests = ["pytest>=5", "pytest-mock"] + docs = ["sphinx", "python-docs-theme"] + expected: | + python3dist(pytest) >= 5 + python3dist(pytest-mock) + result: 0 + +pyproject.toml with dependency-groups two requested: + use_build_system: false + dependency_groups: + - tests + - docs + pyproject.toml: | + [dependency-groups] + tests = ["pytest>=5", "pytest-mock"] + docs = ["sphinx", "python-docs-theme"] + expected: | + python3dist(pytest) >= 5 + python3dist(pytest-mock) + python3dist(sphinx) + python3dist(python-docs-theme) + result: 0 + +pyproject.toml with dependency-groups two requested via comma: + use_build_system: false + dependency_groups: + - tests,docs + pyproject.toml: | + [dependency-groups] + tests = ["pytest>=5", "pytest-mock"] + docs = ["sphinx", "python-docs-theme"] + expected: | + python3dist(pytest) >= 5 + python3dist(pytest-mock) + python3dist(sphinx) + python3dist(python-docs-theme) + result: 0 + +pyproject.toml with include-group: + use_build_system: false + dependency_groups: + - tests_docs + pyproject.toml: | + [dependency-groups] + tests = ["pytest>=5", "pytest-mock"] + docs = ["sphinx", "python-docs-theme"] + typing = ["mypy"] + tests-docs = [{include-group = "tests"}, {include-group = "docs"}, "pytest-sphinx"] + expected: | + python3dist(pytest) >= 5 + python3dist(pytest-mock) + python3dist(sphinx) + python3dist(python-docs-theme) + python3dist(pytest-sphinx) + result: 0 + +pyproject.toml with dependency-groups nonexisting requested: + use_build_system: false + dependency_groups: + - typing + pyproject.toml: | + [dependency-groups] + tests = ["pytest>=5", "pytest-mock"] + docs = ["sphinx", "python-docs-theme"] + except: LookupError + +tox with dependency_groups: + skipif: not (SETUPTOOLS_60 and TOX_4_22) + installed: + setuptools: 50 + wheel: 1 + tox: 4.22 + tox-current-env: 0.0.14 + toxenv: + - py3 + pyproject.toml: | + [build-system] + requires = ["setuptools"] + build-backend = "setuptools.build_meta" + [project] + name = "my_package" + version = "0.1" + [dependency-groups] + tests = ["pytest>=5", "pytest-mock"] + docs = ["sphinx", "python-docs-theme"] + [tool.tox] + requires = ["tox>=4.22"] + [tool.tox.env_run_base] + dependency_groups = ["tests"] + commands = [["pytest"]] + expected: + - | # setuptools 70+ + python3dist(setuptools) + python3dist(tox-current-env) >= 0.0.6 + python3dist(tox) >= 4.22 + python3dist(tox) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(tox-current-env) >= 0.0.6 + python3dist(tox) >= 4.22 + python3dist(tox) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + result: 0 diff --git a/pyproject_save_files.py b/pyproject_save_files.py index 93de7a2..dd6b05c 100644 --- a/pyproject_save_files.py +++ b/pyproject_save_files.py @@ -614,8 +614,10 @@ def generate_file_list(paths_dict, module_globs, include_others=False): # Users using '*' don't care about the files in the package, so it's ok # not to fail the build when no modules are detected # There can be legitimate reasons to create a package without Python modules - if not modules and fnmatch.fnmatchcase("", glob): - done_globs.add(glob) + if not modules: + for glob in module_globs: + if fnmatch.fnmatchcase("", glob): + done_globs.add(glob) missed = module_globs - done_globs if missed: @@ -782,7 +784,7 @@ def dist_metadata(buildroot, record_path): return dist.metadata -def pyproject_save_files_and_modules(buildroot, sitelib, sitearch, python_version, pyproject_record, prefix, assert_license, varargs): +def pyproject_save_files_and_modules(buildroot, sitelib, sitearch, python_version, pyproject_record, prefix, assert_license, allow_no_modules, varargs): """ Takes arguments from the %{pyproject_save_files} macro @@ -797,6 +799,15 @@ def pyproject_save_files_and_modules(buildroot, sitelib, sitearch, python_versio sitedirs = sorted({sitelib, sitearch}) globs, include_auto = parse_varargs(varargs) + if not globs and not allow_no_modules: + raise ValueError( + "At least one module glob needs to be provided to %pyproject_save_files. " + "Alternatively, use -M to indicate no Python modules should be saved." + ) + if globs and allow_no_modules: + raise ValueError( + "%pyproject_save_files -M cannot be used together with module globs." + ) parsed_records = load_parsed_record(pyproject_record) final_file_list = [] @@ -840,6 +851,7 @@ def main(cli_args): cli_args.pyproject_record, cli_args.prefix, cli_args.assert_license, + cli_args.allow_no_modules, cli_args.varargs, ) @@ -853,7 +865,7 @@ def argparser(): prog="%pyproject_save_files", add_help=False, # custom usage to add +auto - usage="%(prog)s [-l|-L] MODULE_GLOB [MODULE_GLOB ...] [+auto]", + usage="%(prog)s [-l|-L] MODULE_GLOB|-M [MODULE_GLOB ...] [+auto]", ) parser.add_argument( '--help', action='help', @@ -878,7 +890,11 @@ def argparser(): help="Don't fail when no License-File (PEP 639) is found (the default).", ) parser.add_argument( - "varargs", nargs="+", metavar="MODULE_GLOB", + "-M", "--allow-no-modules", action="store_true", default=False, + help="Don't fail when no globs are provided, only include non-modules data in the generated filelist.", + ) + parser.add_argument( + "varargs", nargs="*", metavar="MODULE_GLOB", help="Shell-like glob matching top-level module names to save into %%{pyproject_files}", ) return parser diff --git a/test_pyproject_buildrequires.py b/test_pyproject_buildrequires.py index 0fa07db..815c916 100644 --- a/test_pyproject_buildrequires.py +++ b/test_pyproject_buildrequires.py @@ -6,16 +6,36 @@ import pytest import setuptools import yaml -from pyproject_buildrequires import generate_requires +from pyproject_buildrequires import generate_requires, load_pyproject SETUPTOOLS_VERSION = packaging.version.parse(setuptools.__version__) SETUPTOOLS_60 = SETUPTOOLS_VERSION >= packaging.version.parse('60') +try: + import tox +except ImportError: + TOX_4_22 = False +else: + TOX_VERSION = packaging.version.parse(tox.__version__) + TOX_4_22 = TOX_VERSION >= packaging.version.parse('4.22') + testcases = {} with Path(__file__).parent.joinpath('pyproject_buildrequires_testcases.yaml').open() as f: testcases = yaml.safe_load(f) +@pytest.fixture(autouse=True) +def clear_pyproject_data(): + """ + Clear pyproject data before each test. + In reality we build one RPM package at a time, so we can keep the once-loaded + pyproject.toml contents. + When testing, the cached data would leak the once-loaded data to all the + following test cases. + """ + load_pyproject.cache_clear() + + @pytest.mark.parametrize('case_name', testcases) def test_data(case_name, capfd, tmp_path, monkeypatch): case = testcases[case_name] @@ -51,6 +71,7 @@ def test_data(case_name, capfd, tmp_path, monkeypatch): requirement_files = case.get('requirement_files', []) requirement_files = [open(f) for f in requirement_files] use_build_system = case.get('use_build_system', True) + read_pyproject_dependencies = case.get('read_pyproject_dependencies', False) try: generate_requires( get_installed_version=get_installed_version, @@ -58,10 +79,12 @@ def test_data(case_name, capfd, tmp_path, monkeypatch): build_wheel=case.get('build_wheel', False), wheeldir=str(wheeldir), extras=case.get('extras', []), + dependency_groups=case.get('dependency_groups', []), toxenv=case.get('toxenv', None), generate_extras=case.get('generate_extras', False), requirement_files=requirement_files, use_build_system=use_build_system, + read_pyproject_dependencies=read_pyproject_dependencies, output=output, config_settings=case.get('config_settings'), ) -- Gitee