diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py
index 2d3e32a3..8dd21a1a 100644
--- a/cibuildwheel/options.py
+++ b/cibuildwheel/options.py
@@ -833,9 +833,11 @@ class Options:
msg = f"Failed to parse test runtime config. {e}"
raise errors.ConfigurationError(msg) from e
- test_requires = self.reader.get(
- "test-requires", option_format=ListFormat(sep=" ")
- ).split()
+ test_requires = shlex.split(
+ self.reader.get(
+ "test-requires", option_format=ListFormat(sep=" ", quote=shlex.quote)
+ )
+ )
test_extras = self.reader.get("test-extras", option_format=ListFormat(sep=","))
test_groups_str = self.reader.get("test-groups", option_format=ListFormat(sep=" "))
test_groups = [x for x in test_groups_str.split() if x]
@@ -932,9 +934,11 @@ class Options:
)
audit_command = audit_command_str.split(" && ") if audit_command_str else []
- audit_requires = self.reader.get(
- "audit-requires", option_format=ListFormat(sep=" ")
- ).split()
+ audit_requires = shlex.split(
+ self.reader.get(
+ "audit-requires", option_format=ListFormat(sep=" ", quote=shlex.quote)
+ )
+ )
return BuildOptions(
globals=self.globals,
diff --git a/docs/options.md b/docs/options.md
index 2e17043c..542d12ba 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -1670,6 +1670,10 @@ Platform-specific environment variables are also available:
# Install specific versions of test dependencies
[tool.cibuildwheel]
test-requires = ["pytest==8.2.2", "packaging==24.1"]
+
+ # Dependency specifiers with environment markers are supported
+ [tool.cibuildwheel]
+ test-requires = ["pytest", "pyzstd; python_version >= '3.14'"]
```
In configuration files, you can use an array, and the items will be joined with a space.
@@ -1682,6 +1686,10 @@ Platform-specific environment variables are also available:
# Install specific versions of test dependencies
CIBW_TEST_REQUIRES: pytest==8.2.2 packaging==24.1
+
+ # Use shell-style quoting around specifiers containing spaces, such as
+ # those with environment markers
+ CIBW_TEST_REQUIRES: "pytest 'pyzstd; python_version >= \"3.14\"'"
```
diff --git a/test/test_android.py b/test/test_android.py
index 4037b004..59cec2c2 100644
--- a/test/test_android.py
+++ b/test/test_android.py
@@ -405,7 +405,11 @@ def test_environment_markers(tmp_path: Path) -> None:
**cp313_env,
"CIBW_TEST_COMMAND": f"python -m pytest {test_filename}",
"CIBW_TEST_SOURCES": test_filename,
- "CIBW_TEST_REQUIRES": "pytest certifi;sys_platform=='android' platformdirs;sys_platform!='android'",
+ "CIBW_TEST_REQUIRES": (
+ "pytest"
+ " 'certifi; sys_platform == \"android\"'"
+ " 'platformdirs; sys_platform != \"android\"'"
+ ),
},
)
diff --git a/unit_test/options_test.py b/unit_test/options_test.py
index 6b586038..aafeb161 100644
--- a/unit_test/options_test.py
+++ b/unit_test/options_test.py
@@ -106,6 +106,35 @@ def test_options_1(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
assert local.pyodide_version == "0.29.4"
+def test_test_and_audit_requires_with_dependency_specifiers(tmp_path: Path) -> None:
+ """Regression test for https://github.com/pypa/cibuildwheel/issues/2912"""
+ pyproject_toml = tmp_path / "pyproject.toml"
+ pyproject_toml.write_text(
+ """
+[tool.cibuildwheel]
+test-requires = [
+ "pytest",
+ "pyzstd; python_version >= '3.14'",
+ "zarr>=3",
+]
+audit-requires = ["abi3audit; python_version >= '3.9'"]
+"""
+ )
+
+ args = CommandLineArguments.defaults()
+ args.package_dir = tmp_path
+
+ options = Options(platform="linux", command_line_arguments=args, env={})
+ build_options = options.build_options(identifier=None)
+
+ assert build_options.test_requires == [
+ "pytest",
+ "pyzstd; python_version >= '3.14'",
+ "zarr>=3",
+ ]
+ assert build_options.audit_requires == ["abi3audit; python_version >= '3.9'"]
+
+
def test_passthrough(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
with tmp_path.joinpath("pyproject.toml").open("w") as f:
f.write(PYPROJECT_1)