diff --git a/macros.aaa-pyproject-srpm b/macros.aaa-pyproject-srpm index 9bfe84eb96cbc454edd2763472beb876df929a97..fd4deced10b71a4345acd02fff8b701a2474bdb2 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' # Declarative buildsystem, requires RPM 4.20+ to work diff --git a/macros.pyproject b/macros.pyproject index 7a3a898d82ffafd7ee6a726c503b1277e68f2e6e..6578cdaf46946c25ecc88d44786625beeeac4c0f 100644 --- a/macros.pyproject +++ b/macros.pyproject @@ -2,8 +2,14 @@ # For the main Python it's empty, for all others it's "-3.X" %_pyproject_files_pkgversion %{expr:"%{python3_pkgversion}" != "3" ? "-%{python3_pkgversion}" : ""} +# In RPM < 4.20 (4.19.9x is 4.20 alpha), there is no guaranteed, RPM-controlled per-build directory (%%mkbuilddir step). +# Hence we use %%{buildsubdir} if available. +# On newer RPM 4.20+ this is no longer necessary and breaks the declarative buildsystem: +# https://github.com/rpm-software-management/rpm/issues/3890 +%_pyproject_buildsubdir_compat %[ v"0%{?rpmversion}" < v"4.19.90" ? "%{?buildsubdir:/%{buildsubdir}}" : ""] + # This is a directory where wheels are stored and installed from, absolute -%_pyproject_wheeldir %{_builddir}%{?buildsubdir:/%{buildsubdir}}/pyproject-wheeldir%{_pyproject_files_pkgversion} +%_pyproject_wheeldir %{_builddir}%{_pyproject_buildsubdir_compat}/pyproject-wheeldir%{_pyproject_files_pkgversion} # 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 @@ -12,7 +18,7 @@ # 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 %{_builddir}%{?buildsubdir:/%{buildsubdir}}/.pyproject-builddir%{_pyproject_files_pkgversion} +%_pyproject_builddir %{_builddir}%{_pyproject_buildsubdir_compat}/.pyproject-builddir%{_pyproject_files_pkgversion} # We prefix all created files with this value to make them unique # Ideally, we would put them into %%{buildsubdir}, but that value changes during the spec @@ -25,6 +31,11 @@ %_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 +# TODO: Make such a list an actual source of %%set_build_flags (in redhat-rpm-config) +# Cannot use %%gsub directly to preserve EL 9 compatibility +%_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 +44,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} } @@ -67,14 +79,15 @@ echo $(IFS=:; echo "${pyproject_build_lib[*]}") )} -%pyproject3_install() %{expand:\\\ +%pyproject_install() %{expand:\\\ specifier=$(ls %{_pyproject_wheeldir}/*.whl | xargs basename --multiple | sed -E 's/([^-]+)-([^-]+)-.+\\\.whl/\\\1==\\\2/') -if [ -z $specifier ]; then +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 + %py3_shebang_fix %{buildroot}%{_bindir}/* rm -rfv %{buildroot}%{_bindir}/__pycache__ fi rm -f %{_pyproject_ghost_distinfo} @@ -115,7 +128,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 +149,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 +164,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 +183,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 +191,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,8 +223,9 @@ 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}" \\\ - 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 + %{_pyproject_build_flags} \\\ + TMPDIR="%{_pyproject_builddir}" \\\ + RPM_TOXENV="%{toxenv}" FEDORA=%{?fedora} 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 # Incomplete .dist-info dir might confuse importlib.metadata @@ -219,7 +240,7 @@ PATH="%{buildroot}%{_bindir}:$PATH" \\ PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}" \\ %{?__pytest_addopts:PYTEST_ADDOPTS="${PYTEST_ADDOPTS:-} %{__pytest_addopts}"}} \\ HOSTNAME="rpmbuild" \\ -%{__python3} -m tox --current-env -q --recreate -e "%{-e:%{-e*}}%{!-e:%{toxenv}}" %{?*} +%{__python3} -m tox --current-env --assert-config -q --recreate -e "%{-e:%{-e*}}%{!-e:%{toxenv}}" %{?*} } diff --git a/pyproject-rpm-macros.spec b/pyproject-rpm-macros.spec index fc8db25ccf4a9a2832705be7d1f7cb1a61584fe0..af54290eb218c6dc2e1f486955d5a14f66e34584 100644 --- a/pyproject-rpm-macros.spec +++ b/pyproject-rpm-macros.spec @@ -1,17 +1,27 @@ -%bcond_with tests -%bcond pytest_xdist %{undefined openEuler} -%bcond tox_tests %{undefined openEuler} - Name: pyproject-rpm-macros Summary: RPM macros for PEP 517 Python packages +# SPDX License: MIT -Version: 1.15.0 + +%bcond tests 1 +# pytest-xdist and tox are not desired in RHEL +%bcond pytest_xdist %{undefined openEuler} +%bcond tox_tests %{undefined openEuler} + +# The idea is to follow the spirit of semver +# Given version X.Y.Z: +# Increment X and reset Y.Z when there is a *major* incompatibility +# Increment Y and reset Z when new macros or features are added +# Increment Z when this is a bugfix or a cosmetic change +# Dropping support for EOL Fedoras is *not* considered a breaking change +Version: 1.18.4 Release: 1 -URL: https://src.fedoraproject.org/rpms/pyproject-rpm-macros +# Macro files Source001: macros.pyproject Source002: macros.aaa-pyproject-srpm +# Implementation files Source101: pyproject_buildrequires.py Source102: pyproject_save_files.py Source103: pyproject_convert.py @@ -20,35 +30,42 @@ Source105: pyproject_construct_toxenv.py Source106: pyproject_requirements_txt.py Source107: pyproject_wheel.py +# Tests Source201: test_pyproject_buildrequires.py Source202: test_pyproject_save_files.py Source203: test_pyproject_requirements_txt.py Source204: compare_mandata.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: python3-pytest +BuildRequires: python3dist(pytest) %if %{with pytest_xdist} BuildRequires: python3dist(pytest-xdist) %endif -BuildRequires: python3-pyyaml -BuildRequires: python3-packaging -BuildRequires: python3-pip -BuildRequires: python3-setuptools +BuildRequires: python3dist(pyyaml) +BuildRequires: python3dist(packaging) +BuildRequires: python3dist(pip) +BuildRequires: python3dist(setuptools) %if %{with tox_tests} -BuildRequires: python3dist(tox-current-env) >= 0.0.6 +BuildRequires: python3dist(tox-current-env) >= 0.0.16 %endif -BuildRequires: python3-wheel +BuildRequires: (python3dist(wheel) if python3dist(setuptools) < 71) BuildRequires: (python3dist(tomli) if python3 < 3.11) %endif +# We build on top of those: BuildRequires: python-rpm-macros BuildRequires: python-srpm-macros BuildRequires: python3-rpm-macros @@ -57,9 +74,14 @@ Requires: python-srpm-macros Requires: python3-rpm-macros Requires: (pyproject-srpm-macros = %{?epoch:%{epoch}:}%{version}-%{release} if pyproject-srpm-macros) +# We use the following tools outside of coreutils Requires: /usr/bin/find Requires: /usr/bin/sed +# This package requires the %%generate_buildrequires functionality. +# It has been introduced in RPM 4.15 (4.14.90 is the alpha of 4.15). +# What we need is rpmlib(DynamicBuildRequires), but that is impossible to (Build)Require. +# Also, we need to avoid 4.19.90..4.19.91-7 due to rhbz#2284187 Requires: ((rpm-build >= 4.14.90 with (rpm-build < 4.19.90 or rpm-build >= 4.19.91-8)) if rpm-build) BuildRequires: rpm-build >= 4.14.90 @@ -89,13 +111,14 @@ When used in %%generate_buildrequires, it will generate BuildRequires for pyproject-rpm-macros. When both packages are installed, the full version takes precedence. + %prep # Not strictly necessary but allows working on file names instead # of source numbers in install section %setup -c -T cp -p %{sources} . -#generate_buildrequires +%generate_buildrequires # nothing to do, this is here just to assert we have that functionality %build @@ -114,22 +137,16 @@ 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"} # brp-compress is provided as an argument to get the right directory macro expansion -python3 compare_mandata.py -f %{_rpmconfigdir}/brp-compress +%{__python3} compare_mandata.py -f %{_rpmconfigdir}/brp-compress %endif -rm -rf %{buildroot}%{_rpmconfigdir}/openEuler/__pycache__ + %files %{_rpmmacrodir}/macros.pyproject @@ -150,5 +167,8 @@ rm -rf %{buildroot}%{_rpmconfigdir}/openEuler/__pycache__ %changelog +* Tue Oct 28 2025 liweigang - 1.18.4-1 +- update to version 1.18.4 + * Fri Sep 20 2024 misaka00251 - 1.15.0-1 - Init package diff --git a/pyproject_buildrequires.py b/pyproject_buildrequires.py index 515da95dfce3dacdedf7658dfff0e9b89fbce6c5..bba4f00dfbd0c76e02aa206a973db5c219473fd8 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 @@ -21,6 +22,15 @@ from pyproject_wheel import parse_config_settings_args # Allow only the forms we know we can handle. VERSION_RE = re.compile(r'[a-zA-Z0-9.-]+(\.\*)?') +# To avoid breakage on Fedora 40-42, +# we don't assert tox configuration there. +# This can be removed when Fedora 42 goes EOL. +# Note that %tox still uses --assert-config +# because %tox without config is dangerous (false sense of tests). +# Running %pyproject_buildrequires -t/-e without tox config is wrong, but not dangerous. +FEDORA = int(os.getenv('FEDORA') or 0) +TOX_ASSERT_CONFIG_OPTS = () if 40 <= FEDORA < 43 else ('--assert-config',) + class EndPass(Exception): """End current pass of generating requirements""" @@ -34,6 +44,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 +110,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 +134,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 +238,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 +247,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 +277,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') @@ -279,7 +299,7 @@ def get_backend(requirements): def generate_build_requirements(backend, requirements): get_requires = getattr(backend, 'get_requires_for_build_wheel', None) if get_requires: - new_reqs = get_requires(config_settings=requirements.config_settings) + new_reqs = get_requires(requirements.config_settings) requirements.extend(new_reqs, source='get_requires_for_build_wheel') requirements.check(source='get_requires_for_build_wheel') @@ -289,7 +309,7 @@ def parse_metadata_file(metadata_file): def requires_from_parsed_metadata_file(message): - return {k: message.get_all(k, ()) for k in ('Requires', 'Requires-Dist')} + return {k: message.get_all(k, ()) for k in ('Requires-Dist',)} def package_name_from_parsed_metadata_file(message): @@ -310,10 +330,11 @@ 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) + dir_basename = prepare_metadata('.', requirements.config_settings) with open(dir_basename + '/METADATA') as metadata_file: name, requires = package_name_and_requires_from_metadata_file(metadata_file) for key, req in requires.items(): @@ -368,8 +389,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) @@ -377,7 +425,7 @@ def generate_run_requirements(backend, requirements, *, build_wheel, wheeldir): def generate_tox_requirements(toxenv, requirements): toxenv = ','.join(toxenv) - requirements.add('tox-current-env >= 0.0.6', source='tox itself') + requirements.add('tox-current-env >= 0.0.16', source='tox itself') requirements.check(source='tox itself') with tempfile.NamedTemporaryFile('r') as deps, \ tempfile.NamedTemporaryFile('r') as extras, \ @@ -387,6 +435,7 @@ def generate_tox_requirements(toxenv, requirements): '--print-deps-to', deps.name, '--print-extras-to', extras.name, '--no-provision', provision.name, + *TOX_ASSERT_CONFIG_OPTS, '-q', '-r', '-e', toxenv], check=False, encoding='utf-8', @@ -419,6 +468,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 +577,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 +596,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 +613,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 +645,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 +660,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 +678,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 +733,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 892e3c3d2b3463239ac04ab0bf79d09abb3db399..ae26569f80bcabd49cbfd523ae8c0107313772ad 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" @@ -396,7 +456,7 @@ tox dependencies: setuptools: 50 wheel: 1 tox: 3.5.3 - tox-current-env: 0.0.6 + tox-current-env: 0.0.16 toxenv: - py3 setup.py: | @@ -416,19 +476,24 @@ 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(tox-current-env) >= 0.0.16 python3dist(toxdep1) python3dist(toxdep2) python3dist(inst) - - | # tox 4 + - | # tox 4 with setuptools 70+ + python3dist(setuptools) >= 40.8 + python3dist(tox-current-env) >= 0.0.16 + 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-current-env) >= 0.0.16 python3dist(tox) python3dist(toxdep1) python3dist(toxdep2) @@ -440,7 +505,7 @@ tox extras: setuptools: 50 wheel: 1 tox: 3.5.3 - tox-current-env: 0.0.6 + tox-current-env: 0.0.16 generate_extras: true toxenv: - py3 @@ -468,11 +533,10 @@ 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(tox-current-env) >= 0.0.16 python3dist(toxdep) python3dist(inst) python3dist(dep11) > 11.0 @@ -482,11 +546,23 @@ 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.16 + 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-current-env) >= 0.0.16 python3dist(tox) python3dist(toxdep) python3dist(inst) @@ -504,7 +580,7 @@ tox provision unsatisfied: setuptools: 50 wheel: 1 tox: 3.5.3 - tox-current-env: 0.0.6 + tox-current-env: 0.0.16 toxenv: - py3 setup.py: | @@ -525,19 +601,24 @@ 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-current-env) >= 0.0.16 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.16 + 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-current-env) >= 0.0.16 python3dist(tox) >= 3.999 python3dist(setuptools) > 40.0 python3dist(wheel) > 2.0 @@ -549,7 +630,7 @@ tox provision satisfied: setuptools: 50 wheel: 1 tox: 3.5.3 - tox-current-env: 0.0.6 + tox-current-env: 0.0.16 toxenv: - py3 setup.py: | @@ -569,21 +650,27 @@ 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-current-env) >= 0.0.16 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.16 + 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(tox-current-env) >= 0.0.16 python3dist(setuptools) > 40.0 python3dist(tox) >= 3.5 python3dist(toxdep1) @@ -596,7 +683,7 @@ tox provision no minversion: setuptools: 50 wheel: 1 tox: 3.5.3 - tox-current-env: 0.0.6 + tox-current-env: 0.0.16 toxenv: - py3 setup.py: | @@ -611,18 +698,22 @@ 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(tox-current-env) >= 0.0.16 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.16 + 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(tox-current-env) >= 0.0.16 python3dist(setuptools) > 40.0 python3dist(wheel) > 2.0 python3dist(tox) @@ -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,430 @@ 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.16 + 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.16 + python3dist(tox) >= 4.22 + python3dist(tox) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + - | # setuptools < 70 + python3dist(setuptools) + python3dist(wheel) + python3dist(tox-current-env) >= 0.0.16 + python3dist(tox) >= 4.22 + python3dist(tox) + python3dist(pytest) >= 5 + python3dist(pytest-mock) + result: 0 + +Plain Requires fields in core metadata is ignored: + installed: + setuptools: 50 + wheel: 1 + include_runtime: true + setup.py: | + from setuptools import setup + setup( + name='test', + version='0.1', + requires=['ignore_me'], + ) + expected: + - | # setuptools 70+ + python3dist(setuptools) >= 40.8 + - | # setuptools < 70 + python3dist(setuptools) >= 40.8 + python3dist(wheel) + result: 0 diff --git a/pyproject_save_files.py b/pyproject_save_files.py index 93de7a2951a04744a6f7c9f28d6248d516d26758..7ae4193c51fae520ab3023b65ac58345459fff33 100644 --- a/pyproject_save_files.py +++ b/pyproject_save_files.py @@ -137,12 +137,11 @@ def add_file_to_module(paths, module_name, module_type, files_dirs, *files): """ for module in paths["modules"][module_name]: if module["type"] == module_type: - if files[0] not in module[files_dirs]: - module[files_dirs].extend(files) + module[files_dirs].update(files) break else: paths["modules"][module_name].append( - {"type": module_type, "files": [], "dirs": [], files_dirs: list(files)} + {"type": module_type, "files": set(), "dirs": set(), files_dirs: set(files)} ) @@ -348,7 +347,7 @@ def classify_paths( "docs": [], # to be used once there is upstream way to recognize READMEs "licenses": [], # %license entries parsed from dist-info METADATA file }, - "lang": {}, # %lang entries: [module_name or None][language_code] lists of .mo files + "lang": {}, # %lang entries: [module_name or None][language_code] lists of .mo and .qm files "modules": defaultdict(list), # each importable module (directory, .py, .so) "module_names": set(), # qualified names of each importable module ("foo.bar.baz") "other": {"files": []}, # regular %file entries we could not parse :( @@ -357,7 +356,7 @@ def classify_paths( license_files = metadata.get_all('License-File') license_directory = distinfo / 'licenses' # See PEP 639 "Root License Directory" # setuptools was the first known build backend to implement License-File. - # Unfortunately they don't put licenses to the license directory (yet): + # Unfortunately they didn't put licenses to the license directory in setuptools<78: # https://github.com/pypa/setuptools/issues/3596 # Hence, we check licenses in both licenses and dist-info license_directories = (license_directory, distinfo) @@ -412,7 +411,7 @@ def classify_paths( for parent in list(path.parents)[:index]: # no direct slice until Python 3.10 add_file_to_module(paths, module_dir.name, "package", "dirs", parent) is_lang = False - if path.suffix == ".mo": + if path.suffix == ".mo" or path.suffix == ".qm": is_lang = add_lang_to_module(paths, module_dir.name, path) if not is_lang: if path.suffix == ".py": @@ -425,7 +424,7 @@ def classify_paths( add_file_to_module(paths, module_dir.name, "package", "files", path) break else: - if path.suffix == ".mo": + if path.suffix == ".mo" or path.suffix == ".qm": add_lang_to_module(paths, None, path) or paths["other"]["files"].append(path) else: path = normalize_manpage_filename(prefix, path) @@ -614,8 +613,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 +783,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 +798,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 +850,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 +864,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 +889,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_RECORD b/test_RECORD new file mode 100644 index 0000000000000000000000000000000000000000..e917ce9e8d9110435fd1e40fade2521b9c1c01db --- /dev/null +++ b/test_RECORD @@ -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 diff --git a/test_pyproject_buildrequires.py b/test_pyproject_buildrequires.py index 0fa07db6c65ebb0e65569d06d6b8de22491dbc2a..d6f9852a6beef6789a51acaea0f117170b6fc5ac 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'), ) @@ -79,15 +102,15 @@ def test_data(case_name, capfd, tmp_path, monkeypatch): assert 'expected' in case or 'stderr_contains' in case out, err = capfd.readouterr() - dependencies = output.read_text() + dependencies = sorted(output.read_text().splitlines()) if 'expected' in case: expected = case['expected'] if isinstance(expected, list): # at least one of them needs to match - assert dependencies in expected + assert dependencies in [sorted(e.splitlines()) for e in expected] else: - assert dependencies == expected + assert dependencies == sorted(expected.splitlines()) # stderr_contains may be a string or list of strings stderr_contains = case.get('stderr_contains')