%pyproject_buildrequires: Add support for dependency groups (PEP 735), via the -g flag
This commit is contained in:
parent
80d9abe0f4
commit
307d2bef63
13
README.md
13
README.md
@ -124,6 +124,14 @@ For example, if upstream suggests installing test dependencies with
|
|||||||
%generate_buildrequires
|
%generate_buildrequires
|
||||||
%pyproject_buildrequires -x testing
|
%pyproject_buildrequires -x testing
|
||||||
|
|
||||||
|
For projects that specify test requirements using [PEP 735] dependency groups,
|
||||||
|
these can be added using the `-g` flag.
|
||||||
|
Multiple groups can be supplied by repeating the flag or as a comma separated list.
|
||||||
|
For example, if upstream uses a dependency group called `tests`, the test deps would be generated by:
|
||||||
|
|
||||||
|
%generate_buildrequires
|
||||||
|
%pyproject_buildrequires -g tests
|
||||||
|
|
||||||
For projects that specify test requirements in their [tox] configuration,
|
For projects that specify test requirements in their [tox] configuration,
|
||||||
these can be added using the `-t` flag (default tox environment)
|
these can be added using the `-t` flag (default tox environment)
|
||||||
or the `-e` flag followed by the tox environment.
|
or the `-e` flag followed by the tox environment.
|
||||||
@ -153,10 +161,12 @@ such plugins will be BuildRequired as well.
|
|||||||
Not all plugins are guaranteed to play well with [tox-current-env],
|
Not all plugins are guaranteed to play well with [tox-current-env],
|
||||||
in worst case, patch/sed the requirement out from the tox configuration.
|
in worst case, patch/sed the requirement out from the tox configuration.
|
||||||
|
|
||||||
Note that neither `-x` or `-t` can be used with `-R`,
|
Note that neither `-x` or `-t` can be used with `-R` or `-N`,
|
||||||
because runtime dependencies are always required for testing.
|
because runtime dependencies are always required for testing.
|
||||||
You can only use those options if the build backend supports the [prepare-metadata-for-build-wheel hook],
|
You can only use those options if the build backend supports the [prepare-metadata-for-build-wheel hook],
|
||||||
or together with `-p` or `-w`.
|
or together with `-p` or `-w`.
|
||||||
|
However, using `-g` with `-R` or `-N` is supported because dependency groups don't need to be used for testing
|
||||||
|
and can be obtained by reading `pyproject.toml` only.
|
||||||
|
|
||||||
[tox]: https://tox.readthedocs.io/
|
[tox]: https://tox.readthedocs.io/
|
||||||
[tox-current-env]: https://github.com/fedora-python/tox-current-env/
|
[tox-current-env]: https://github.com/fedora-python/tox-current-env/
|
||||||
@ -520,6 +530,7 @@ so be prepared for problems.
|
|||||||
[PEP 517]: https://www.python.org/dev/peps/pep-0517/
|
[PEP 517]: https://www.python.org/dev/peps/pep-0517/
|
||||||
[PEP 518]: https://www.python.org/dev/peps/pep-0518/
|
[PEP 518]: https://www.python.org/dev/peps/pep-0518/
|
||||||
[PEP 639]: https://www.python.org/dev/peps/pep-0639/
|
[PEP 639]: https://www.python.org/dev/peps/pep-0639/
|
||||||
|
[PEP 735]: https://www.python.org/dev/peps/pep-0735/
|
||||||
[pip's documentation]: https://pip.pypa.io/en/stable/cli/pip_install/#vcs-support
|
[pip's documentation]: https://pip.pypa.io/en/stable/cli/pip_install/#vcs-support
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
# this macro will cause the package with the real macro to be installed.
|
# this macro will cause the package with the real macro to be installed.
|
||||||
# When macros.pyproject is installed, it overrides this macro.
|
# When macros.pyproject is installed, it overrides this macro.
|
||||||
# Note: This needs to maintain the same set of options as the real macro.
|
# Note: This needs to maintain the same set of options as the real macro.
|
||||||
%pyproject_buildrequires(rRxtNwpe:C:) echo 'pyproject-rpm-macros' && exit 0
|
%pyproject_buildrequires(rRxtNwpe:g:C:) echo 'pyproject-rpm-macros' && exit 0
|
||||||
|
|
||||||
|
|
||||||
# Declarative buildsystem, requires RPM 4.20+ to work
|
# Declarative buildsystem, requires RPM 4.20+ to work
|
||||||
|
@ -158,9 +158,16 @@ fi
|
|||||||
%default_toxenv py%{python3_version_nodots}
|
%default_toxenv py%{python3_version_nodots}
|
||||||
%toxenv %{default_toxenv}
|
%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
|
# Note: Keep the options in sync with this macro from macros.aaa-pyproject-srpm
|
||||||
%pyproject_buildrequires(rRxtNwpe:C:) %{expand:\\\
|
%pyproject_buildrequires(rRxtNwpe:g:C:) %{expand:\\\
|
||||||
%_set_pytest_addopts
|
%_set_pytest_addopts
|
||||||
# The default flags expect the package note file to exist
|
# The default flags expect the package note file to exist
|
||||||
# see https://bugzilla.redhat.com/show_bug.cgi?id=2097535
|
# see https://bugzilla.redhat.com/show_bug.cgi?id=2097535
|
||||||
@ -181,6 +188,9 @@ fi
|
|||||||
%{-w:%{error:The -N and -w options are mutually exclusive}}
|
%{-w:%{error:The -N and -w options are mutually exclusive}}
|
||||||
%{-p:%{error:The -N and -p options are mutually exclusive}}
|
%{-p:%{error:The -N and -p options are mutually exclusive}}
|
||||||
%{-C:%{error:The -N and -C options are mutually exclusive}}
|
%{-C:%{error:The -N and -C options are mutually exclusive}}
|
||||||
|
%{-g:if [ -f pyproject.toml ]; then
|
||||||
|
%_pyproject_tomlidep
|
||||||
|
fi}
|
||||||
}
|
}
|
||||||
%{-w:
|
%{-w:
|
||||||
%{-p:%{error:The -w and -p options are mutually exclusive}}
|
%{-p:%{error:The -w and -p options are mutually exclusive}}
|
||||||
@ -191,13 +201,7 @@ echo 'python%{python3_pkgversion}-devel'
|
|||||||
echo 'python%{python3_pkgversion}dist(packaging)'
|
echo 'python%{python3_pkgversion}dist(packaging)'
|
||||||
%{!-N:echo 'python%{python3_pkgversion}dist(pip) >= 19'
|
%{!-N:echo 'python%{python3_pkgversion}dist(pip) >= 19'
|
||||||
if [ -f pyproject.toml ]; then
|
if [ -f pyproject.toml ]; then
|
||||||
%["%{python3_pkgversion}" == "3"
|
%_pyproject_tomlidep
|
||||||
? "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"
|
|
||||||
]"
|
|
||||||
]
|
|
||||||
elif [ -f setup.py ]; then
|
elif [ -f setup.py ]; then
|
||||||
# Note: If the default requirements change, also change them in the script!
|
# 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(setuptools) >= 40.8'
|
||||||
|
@ -14,7 +14,7 @@ License: MIT
|
|||||||
# Increment Y and reset Z when new macros or features are added
|
# Increment Y and reset Z when new macros or features are added
|
||||||
# Increment Z when this is a bugfix or a cosmetic change
|
# Increment Z when this is a bugfix or a cosmetic change
|
||||||
# Dropping support for EOL Fedoras is *not* considered a breaking change
|
# Dropping support for EOL Fedoras is *not* considered a breaking change
|
||||||
Version: 1.15.1
|
Version: 1.16.0
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
|
|
||||||
# Macro files
|
# Macro files
|
||||||
@ -173,6 +173,10 @@ export HOSTNAME="rpmbuild" # to speedup tox in network-less mock, see rhbz#1856
|
|||||||
|
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Mon Nov 04 2024 Miro Hrončok <mhroncok@redhat.com> - 1.16.0-1
|
||||||
|
- %%pyproject_buildrequires: Add support for dependency groups (PEP 735), via the -g flag
|
||||||
|
- Fixes: rhbz#2318849
|
||||||
|
|
||||||
* Thu Oct 03 2024 Karolina Surma <ksurma@redhat.com> - 1.15.1-1
|
* Thu Oct 03 2024 Karolina Surma <ksurma@redhat.com> - 1.15.1-1
|
||||||
- Fix handling of self-referencing extras when reading pyproject.toml
|
- Fix handling of self-referencing extras when reading pyproject.toml
|
||||||
|
|
||||||
|
@ -468,6 +468,80 @@ def generate_tox_requirements(toxenv, requirements):
|
|||||||
source=f'tox --print-deps-only: {toxenv}')
|
source=f'tox --print-deps-only: {toxenv}')
|
||||||
|
|
||||||
|
|
||||||
|
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"):
|
def python3dist(name, op=None, version=None, python3_pkgversion="3"):
|
||||||
prefix = f"python{python3_pkgversion}dist"
|
prefix = f"python{python3_pkgversion}dist"
|
||||||
|
|
||||||
@ -480,7 +554,7 @@ def python3dist(name, op=None, version=None, python3_pkgversion="3"):
|
|||||||
|
|
||||||
|
|
||||||
def generate_requires(
|
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
|
get_installed_version=importlib.metadata.version, # for dep injection
|
||||||
generate_extras=False, python3_pkgversion="3", requirement_files=None, use_build_system=True,
|
generate_extras=False, python3_pkgversion="3", requirement_files=None, use_build_system=True,
|
||||||
read_pyproject_dependencies=False,
|
read_pyproject_dependencies=False,
|
||||||
@ -514,7 +588,9 @@ def generate_requires(
|
|||||||
generate_build_requirements(backend, requirements)
|
generate_build_requirements(backend, requirements)
|
||||||
if toxenv:
|
if toxenv:
|
||||||
include_runtime = True
|
include_runtime = True
|
||||||
generate_tox_requirements(toxenv, requirements)
|
generate_tox_requirements(toxenv, requirements) # TODO extend dependency_groups
|
||||||
|
if dependency_groups:
|
||||||
|
generate_dependency_groups(dependency_groups, requirements)
|
||||||
if include_runtime:
|
if include_runtime:
|
||||||
generate_run_requirements(backend, requirements, build_wheel=build_wheel,
|
generate_run_requirements(backend, requirements, build_wheel=build_wheel,
|
||||||
read_pyproject_dependencies=read_pyproject_dependencies, wheeldir=wheeldir)
|
read_pyproject_dependencies=read_pyproject_dependencies, wheeldir=wheeldir)
|
||||||
@ -559,6 +635,11 @@ def main(argv):
|
|||||||
help='comma separated list of "extras" for runtime requirements '
|
help='comma separated list of "extras" for runtime requirements '
|
||||||
'(e.g. -x testing,feature-x) (implies --runtime, can be repeated)',
|
'(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(
|
parser.add_argument(
|
||||||
'-t', '--tox', action='store_true',
|
'-t', '--tox', action='store_true',
|
||||||
help=('generate test tequirements from tox environment '
|
help=('generate test tequirements from tox environment '
|
||||||
@ -627,6 +708,7 @@ def main(argv):
|
|||||||
wheeldir=args.wheeldir,
|
wheeldir=args.wheeldir,
|
||||||
toxenv=args.toxenv,
|
toxenv=args.toxenv,
|
||||||
extras=args.extras,
|
extras=args.extras,
|
||||||
|
dependency_groups=args.dependency_groups,
|
||||||
generate_extras=args.generate_extras,
|
generate_extras=args.generate_extras,
|
||||||
python3_pkgversion=args.python3_pkgversion,
|
python3_pkgversion=args.python3_pkgversion,
|
||||||
requirement_files=args.requirement_files,
|
requirement_files=args.requirement_files,
|
||||||
|
@ -1278,3 +1278,110 @@ pyproject.toml with self-referencing extras:
|
|||||||
python3dist(pytest-rerunfailures)
|
python3dist(pytest-rerunfailures)
|
||||||
python3dist(wurlitzer)
|
python3dist(wurlitzer)
|
||||||
result: 0
|
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: |
|
||||||
|
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
|
||||||
|
@ -71,6 +71,7 @@ def test_data(case_name, capfd, tmp_path, monkeypatch):
|
|||||||
build_wheel=case.get('build_wheel', False),
|
build_wheel=case.get('build_wheel', False),
|
||||||
wheeldir=str(wheeldir),
|
wheeldir=str(wheeldir),
|
||||||
extras=case.get('extras', []),
|
extras=case.get('extras', []),
|
||||||
|
dependency_groups=case.get('dependency_groups', []),
|
||||||
toxenv=case.get('toxenv', None),
|
toxenv=case.get('toxenv', None),
|
||||||
generate_extras=case.get('generate_extras', False),
|
generate_extras=case.get('generate_extras', False),
|
||||||
requirement_files=requirement_files,
|
requirement_files=requirement_files,
|
||||||
|
Loading…
Reference in New Issue
Block a user