Skip to content

Commit 524cd1c

Browse files
committed
feat: support test-groups
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 66d45de commit 524cd1c

9 files changed

+118
-24
lines changed

.pre-commit-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ repos:
2929
args: ["--python-version=3.8"]
3030
additional_dependencies: &mypy-dependencies
3131
- bracex
32+
- dependency-groups>=1.2
3233
- nox
3334
- orjson
3435
- packaging

bin/generate_schema.py

+3
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@
164164
test-extras:
165165
description: Install your wheel for testing using `extras_require`
166166
type: string_array
167+
test-groups:
168+
description: Install extra groups when testing
169+
type: string_array
167170
test-requires:
168171
description: Install Python dependencies before running the tests
169172
type: string_array

cibuildwheel/options.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
2323
from .logger import log
2424
from .oci_container import OCIContainerEngineConfig
25-
from .projectfiles import get_requires_python_str
25+
from .projectfiles import get_dependency_groups, get_requires_python_str
2626
from .typing import PLATFORMS, PlatformName
2727
from .util import (
2828
MANYLINUX_ARCHS,
@@ -92,6 +92,7 @@ class BuildOptions:
9292
before_test: str | None
9393
test_requires: list[str]
9494
test_extras: str
95+
test_groups: list[str]
9596
build_verbosity: int
9697
build_frontend: BuildFrontendConfig | None
9798
config_settings: str
@@ -568,6 +569,13 @@ def __init__(
568569
disallow=DISALLOWED_OPTIONS,
569570
)
570571

572+
self.project_dir = Path(command_line_arguments.package_dir)
573+
try:
574+
with self.project_dir.joinpath("pyproject.toml").open("rb") as f:
575+
self.pyproject_toml = tomllib.load(f)
576+
except FileNotFoundError:
577+
self.pyproject_toml = {}
578+
571579
@property
572580
def config_file_path(self) -> Path | None:
573581
args = self.command_line_arguments
@@ -584,8 +592,10 @@ def config_file_path(self) -> Path | None:
584592

585593
@functools.cached_property
586594
def package_requires_python_str(self) -> str | None:
587-
args = self.command_line_arguments
588-
return get_requires_python_str(Path(args.package_dir))
595+
return get_requires_python_str(self.project_dir, self.pyproject_toml)
596+
597+
def dependency_groups(self, *groups: str) -> tuple[str, ...]:
598+
return get_dependency_groups(self.pyproject_toml, *groups)
589599

590600
@property
591601
def globals(self) -> GlobalOptions:
@@ -672,6 +682,9 @@ def build_options(self, identifier: str | None) -> BuildOptions:
672682
"test-requires", option_format=ListFormat(sep=" ")
673683
).split()
674684
test_extras = self.reader.get("test-extras", option_format=ListFormat(sep=","))
685+
test_groups_str = self.reader.get("test-groups", option_format=ListFormat(sep=","))
686+
test_groups = [x for x in test_groups_str.split(",") if x]
687+
test_dependency_groups = self.dependency_groups(*test_groups)
675688
build_verbosity_str = self.reader.get("build-verbosity")
676689

677690
build_frontend_str = self.reader.get(
@@ -771,8 +784,9 @@ def build_options(self, identifier: str | None) -> BuildOptions:
771784
return BuildOptions(
772785
globals=self.globals,
773786
test_command=test_command,
774-
test_requires=test_requires,
787+
test_requires=[*test_requires, *test_dependency_groups],
775788
test_extras=test_extras,
789+
test_groups=test_groups,
776790
before_test=before_test,
777791
before_build=before_build,
778792
before_all=before_all,

cibuildwheel/projectfiles.py

+22-7
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import configparser
55
import contextlib
66
from pathlib import Path
7+
from typing import Any
78

8-
from ._compat import tomllib
9+
import dependency_groups
910

1011

1112
def get_parent(node: ast.AST | None, depth: int = 1) -> ast.AST | None:
@@ -84,15 +85,12 @@ def setup_py_python_requires(content: str) -> str | None:
8485
return None
8586

8687

87-
def get_requires_python_str(package_dir: Path) -> str | None:
88+
def get_requires_python_str(package_dir: Path, pyproject_toml: dict[str, Any]) -> str | None:
8889
"""Return the python requires string from the most canonical source available, or None"""
8990

9091
# Read in from pyproject.toml:project.requires-python
91-
with contextlib.suppress(FileNotFoundError):
92-
with (package_dir / "pyproject.toml").open("rb") as f1:
93-
info = tomllib.load(f1)
94-
with contextlib.suppress(KeyError, IndexError, TypeError):
95-
return str(info["project"]["requires-python"])
92+
with contextlib.suppress(KeyError, IndexError, TypeError):
93+
return str(pyproject_toml["project"]["requires-python"])
9694

9795
# Read in from setup.cfg:options.python_requires
9896
config = configparser.ConfigParser()
@@ -106,3 +104,20 @@ def get_requires_python_str(package_dir: Path) -> str | None:
106104
return setup_py_python_requires(f2.read())
107105

108106
return None
107+
108+
109+
def get_dependency_groups(pyproject_toml: dict[str, Any], *groups: str) -> tuple[str, ...]:
110+
"""
111+
Get the packages in dependency-groups for a package.
112+
"""
113+
114+
if not groups:
115+
return ()
116+
117+
try:
118+
dependency_groups_toml = pyproject_toml["dependency-groups"]
119+
except KeyError:
120+
msg = f"Didn't find [dependency-groups], which are needed to resolve {groups!r}."
121+
raise KeyError(msg) from None
122+
123+
return dependency_groups.resolve(dependency_groups_toml, *groups)

cibuildwheel/resources/defaults.toml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ test-command = ""
2020
before-test = ""
2121
test-requires = []
2222
test-extras = []
23+
test-groups = []
2324

2425
container-engine = "docker"
2526

docs/options.md

+34
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,40 @@ Platform-specific environment variables are also available:<br/>
16041604

16051605
In configuration files, you can use an inline array, and the items will be joined with a comma.
16061606

1607+
1608+
### `CIBW_TEST_GROUPS` {: #test-groups}
1609+
> Install your wheel for testing using `dependency-groups`
1610+
1611+
List of
1612+
[dependency-groups](https://peps.python.org/pep-0735)
1613+
that should be included when installing the wheel prior to running the
1614+
tests. This can be used to avoid having to redefine test dependencies in
1615+
`CIBW_TEST_REQUIRES` if they are already defined in `pyproject.toml`.
1616+
1617+
Platform-specific environment variables are also available:<br/>
1618+
`CIBW_TEST_GROUPS_MACOS` | `CIBW_TEST_GROUPS_WINDOWS` | `CIBW_TEST_GROUPS_LINUX` | `CIBW_TEST_GROUPS_PYODIDE`
1619+
1620+
#### Examples
1621+
1622+
!!! tab examples "Environment variables"
1623+
1624+
```yaml
1625+
# Will cause the wheel to be installed with these groups of dependencies
1626+
CIBW_TEST_GROUPS: "test,qt"
1627+
```
1628+
1629+
Separate multiple items with a comma.
1630+
1631+
!!! tab examples "pyproject.toml"
1632+
1633+
```toml
1634+
[tool.cibuildwheel]
1635+
# Will cause the wheel to be installed with these groups of dependencies
1636+
test-groups = ["test", "qt"]
1637+
```
1638+
1639+
In configuration files, you can use an inline array, and the items will be joined with a comma.
1640+
16071641
### `CIBW_TEST_SKIP` {: #test-skip}
16081642
> Skip running tests on some builds
16091643

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ dependencies = [
4343
"bashlex!=0.13",
4444
"bracex",
4545
"certifi",
46+
"dependency-groups>=1.2",
4647
"filelock",
4748
"packaging>=20.9",
4849
"platformdirs",

unit_test/options_toml_test.py

+8
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
test-command = "pyproject"
2323
test-requires = "something"
2424
test-extras = ["one", "two"]
25+
test-groups = ["three", "four"]
2526
2627
manylinux-x86_64-image = "manylinux1"
2728
@@ -60,6 +61,7 @@ def test_simple_settings(tmp_path, platform, fname):
6061
== 'THING="OTHER" FOO="BAR"'
6162
)
6263
assert options_reader.get("test-extras", option_format=ListFormat(",")) == "one,two"
64+
assert options_reader.get("test-groups", option_format=ListFormat(",")) == "three,four"
6365

6466
assert options_reader.get("manylinux-x86_64-image") == "manylinux1"
6567
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
@@ -85,7 +87,9 @@ def test_envvar_override(tmp_path, platform):
8587
"CIBW_MANYLINUX_X86_64_IMAGE": "manylinux_2_24",
8688
"CIBW_TEST_COMMAND": "mytest",
8789
"CIBW_TEST_REQUIRES": "docs",
90+
"CIBW_TEST_GROUPS": "mgroup,two",
8891
"CIBW_TEST_REQUIRES_LINUX": "scod",
92+
"CIBW_TEST_GROUPS_LINUX": "lgroup",
8993
},
9094
)
9195

@@ -99,6 +103,10 @@ def test_envvar_override(tmp_path, platform):
99103
options_reader.get("test-requires", option_format=ListFormat(" "))
100104
== {"windows": "docs", "macos": "docs", "linux": "scod"}[platform]
101105
)
106+
assert (
107+
options_reader.get("test-groups", option_format=ListFormat(","))
108+
== {"windows": "mgroup,two", "macos": "mgroup,two", "linux": "lgroup"}[platform]
109+
)
102110
assert options_reader.get("test-command") == "mytest"
103111

104112

unit_test/projectfiles_test.py

+30-13
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
from textwrap import dedent
44

5-
from cibuildwheel.projectfiles import get_requires_python_str, setup_py_python_requires
5+
from cibuildwheel._compat import tomllib
6+
from cibuildwheel.projectfiles import (
7+
get_dependency_groups,
8+
get_requires_python_str,
9+
setup_py_python_requires,
10+
)
611

712

813
def test_read_setup_py_simple(tmp_path):
@@ -23,7 +28,7 @@ def test_read_setup_py_simple(tmp_path):
2328
)
2429

2530
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) == "1.23"
26-
assert get_requires_python_str(tmp_path) == "1.23"
31+
assert get_requires_python_str(tmp_path, {}) == "1.23"
2732

2833

2934
def test_read_setup_py_if_main(tmp_path):
@@ -45,7 +50,7 @@ def test_read_setup_py_if_main(tmp_path):
4550
)
4651

4752
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) == "1.23"
48-
assert get_requires_python_str(tmp_path) == "1.23"
53+
assert get_requires_python_str(tmp_path, {}) == "1.23"
4954

5055

5156
def test_read_setup_py_if_main_reversed(tmp_path):
@@ -67,7 +72,7 @@ def test_read_setup_py_if_main_reversed(tmp_path):
6772
)
6873

6974
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) == "1.23"
70-
assert get_requires_python_str(tmp_path) == "1.23"
75+
assert get_requires_python_str(tmp_path, {}) == "1.23"
7176

7277

7378
def test_read_setup_py_if_invalid(tmp_path):
@@ -89,7 +94,7 @@ def test_read_setup_py_if_invalid(tmp_path):
8994
)
9095

9196
assert not setup_py_python_requires(tmp_path.joinpath("setup.py").read_text())
92-
assert not get_requires_python_str(tmp_path)
97+
assert not get_requires_python_str(tmp_path, {})
9398

9499

95100
def test_read_setup_py_full(tmp_path):
@@ -115,7 +120,7 @@ def test_read_setup_py_full(tmp_path):
115120
assert (
116121
setup_py_python_requires(tmp_path.joinpath("setup.py").read_text(encoding="utf8")) == "1.24"
117122
)
118-
assert get_requires_python_str(tmp_path) == "1.24"
123+
assert get_requires_python_str(tmp_path, {}) == "1.24"
119124

120125

121126
def test_read_setup_py_assign(tmp_path):
@@ -138,7 +143,7 @@ def test_read_setup_py_assign(tmp_path):
138143
)
139144

140145
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) is None
141-
assert get_requires_python_str(tmp_path) is None
146+
assert get_requires_python_str(tmp_path, {}) is None
142147

143148

144149
def test_read_setup_py_None(tmp_path):
@@ -161,7 +166,7 @@ def test_read_setup_py_None(tmp_path):
161166
)
162167

163168
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) is None
164-
assert get_requires_python_str(tmp_path) is None
169+
assert get_requires_python_str(tmp_path, {}) is None
165170

166171

167172
def test_read_setup_py_empty(tmp_path):
@@ -183,7 +188,7 @@ def test_read_setup_py_empty(tmp_path):
183188
)
184189

185190
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) is None
186-
assert get_requires_python_str(tmp_path) is None
191+
assert get_requires_python_str(tmp_path, {}) is None
187192

188193

189194
def test_read_setup_cfg(tmp_path):
@@ -199,7 +204,7 @@ def test_read_setup_cfg(tmp_path):
199204
)
200205
)
201206

202-
assert get_requires_python_str(tmp_path) == "1.234"
207+
assert get_requires_python_str(tmp_path, {}) == "1.234"
203208

204209

205210
def test_read_setup_cfg_empty(tmp_path):
@@ -215,7 +220,7 @@ def test_read_setup_cfg_empty(tmp_path):
215220
)
216221
)
217222

218-
assert get_requires_python_str(tmp_path) is None
223+
assert get_requires_python_str(tmp_path, {}) is None
219224

220225

221226
def test_read_pyproject_toml(tmp_path):
@@ -231,8 +236,10 @@ def test_read_pyproject_toml(tmp_path):
231236
"""
232237
)
233238
)
239+
with open(tmp_path / "pyproject.toml", "rb") as f:
240+
pyproject_toml = tomllib.load(f)
234241

235-
assert get_requires_python_str(tmp_path) == "1.654"
242+
assert get_requires_python_str(tmp_path, pyproject_toml) == "1.654"
236243

237244

238245
def test_read_pyproject_toml_empty(tmp_path):
@@ -245,5 +252,15 @@ def test_read_pyproject_toml_empty(tmp_path):
245252
"""
246253
)
247254
)
255+
with open(tmp_path / "pyproject.toml", "rb") as f:
256+
pyproject_toml = tomllib.load(f)
248257

249-
assert get_requires_python_str(tmp_path) is None
258+
assert get_requires_python_str(tmp_path, pyproject_toml) is None
259+
260+
261+
def test_read_dep_groups():
262+
pyproject_toml = {"dependency-groups": {"group1": ["pkg1", "pkg2"], "group2": ["pkg3"]}}
263+
assert get_dependency_groups(pyproject_toml) == ()
264+
assert get_dependency_groups(pyproject_toml, "group1") == ("pkg1", "pkg2")
265+
assert get_dependency_groups(pyproject_toml, "group2") == ("pkg3",)
266+
assert get_dependency_groups(pyproject_toml, "group1", "group2") == ("pkg1", "pkg2", "pkg3")

0 commit comments

Comments
 (0)