Skip to content

Commit

Permalink
feat: support test-groups (#2063)
Browse files Browse the repository at this point in the history
* feat: support test-groups

Signed-off-by: Henry Schreiner <[email protected]>

* refactor: address review comments

Signed-off-by: Henry Schreiner <[email protected]>

* tests: add a integration test

Signed-off-by: Henry Schreiner <[email protected]>

* Apply suggestions from code review

Co-authored-by: Matthieu Darbois <[email protected]>

* fix: better error messages based on feedback

Signed-off-by: Henry Schreiner <[email protected]>

---------

Signed-off-by: Henry Schreiner <[email protected]>
Co-authored-by: Matthieu Darbois <[email protected]>
  • Loading branch information
henryiii and mayeut authored Nov 4, 2024
1 parent d2c7614 commit cc1d977
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 24 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ repos:
args: ["--python-version=3.8"]
additional_dependencies: &mypy-dependencies
- bracex
- dependency-groups>=1.2
- nox
- orjson
- packaging
Expand Down
3 changes: 3 additions & 0 deletions bin/generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@
test-extras:
description: Install your wheel for testing using `extras_require`
type: string_array
test-groups:
description: Install extra groups when testing
type: string_array
test-requires:
description: Install Python dependencies before running the tests
type: string_array
Expand Down
23 changes: 19 additions & 4 deletions cibuildwheel/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
from .logger import log
from .oci_container import OCIContainerEngineConfig
from .projectfiles import get_requires_python_str
from .projectfiles import get_requires_python_str, resolve_dependency_groups
from .typing import PLATFORMS, PlatformName
from .util import (
MANYLINUX_ARCHS,
Expand Down Expand Up @@ -92,6 +92,7 @@ class BuildOptions:
before_test: str | None
test_requires: list[str]
test_extras: str
test_groups: list[str]
build_verbosity: int
build_frontend: BuildFrontendConfig | None
config_settings: str
Expand Down Expand Up @@ -550,6 +551,8 @@ def get(


class Options:
pyproject_toml: dict[str, Any] | None

def __init__(
self,
platform: PlatformName,
Expand All @@ -568,6 +571,13 @@ def __init__(
disallow=DISALLOWED_OPTIONS,
)

self.package_dir = Path(command_line_arguments.package_dir)
try:
with self.package_dir.joinpath("pyproject.toml").open("rb") as f:
self.pyproject_toml = tomllib.load(f)
except FileNotFoundError:
self.pyproject_toml = None

@property
def config_file_path(self) -> Path | None:
args = self.command_line_arguments
Expand All @@ -584,8 +594,7 @@ def config_file_path(self) -> Path | None:

@functools.cached_property
def package_requires_python_str(self) -> str | None:
args = self.command_line_arguments
return get_requires_python_str(Path(args.package_dir))
return get_requires_python_str(self.package_dir, self.pyproject_toml)

@property
def globals(self) -> GlobalOptions:
Expand Down Expand Up @@ -672,6 +681,11 @@ def build_options(self, identifier: str | None) -> BuildOptions:
"test-requires", option_format=ListFormat(sep=" ")
).split()
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]
test_requirements_from_groups = resolve_dependency_groups(
self.pyproject_toml, *test_groups
)
build_verbosity_str = self.reader.get("build-verbosity")

build_frontend_str = self.reader.get(
Expand Down Expand Up @@ -771,8 +785,9 @@ def build_options(self, identifier: str | None) -> BuildOptions:
return BuildOptions(
globals=self.globals,
test_command=test_command,
test_requires=test_requires,
test_requires=[*test_requires, *test_requirements_from_groups],
test_extras=test_extras,
test_groups=test_groups,
before_test=before_test,
before_build=before_build,
before_all=before_all,
Expand Down
35 changes: 28 additions & 7 deletions cibuildwheel/projectfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import configparser
import contextlib
from pathlib import Path
from typing import Any

from ._compat import tomllib
import dependency_groups


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


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

# Read in from pyproject.toml:project.requires-python
with contextlib.suppress(FileNotFoundError):
with (package_dir / "pyproject.toml").open("rb") as f1:
info = tomllib.load(f1)
with contextlib.suppress(KeyError, IndexError, TypeError):
return str(info["project"]["requires-python"])
with contextlib.suppress(KeyError, IndexError, TypeError):
return str((pyproject_toml or {})["project"]["requires-python"])

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

return None


def resolve_dependency_groups(
pyproject_toml: dict[str, Any] | None, *groups: str
) -> tuple[str, ...]:
"""
Get the packages in dependency-groups for a package.
"""

if not groups:
return ()

if pyproject_toml is None:
msg = f"Didn't find a pyproject.toml, so can't read [dependency-groups] {groups!r} from it!"
raise FileNotFoundError(msg)

try:
dependency_groups_toml = pyproject_toml["dependency-groups"]
except KeyError:
msg = f"Didn't find [dependency-groups] in pyproject.toml, which is needed to resolve {groups!r}."
raise KeyError(msg) from None

return dependency_groups.resolve(dependency_groups_toml, *groups)
30 changes: 30 additions & 0 deletions cibuildwheel/resources/cibuildwheel.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,21 @@
],
"title": "CIBW_TEST_EXTRAS"
},
"test-groups": {
"description": "Install extra groups when testing",
"oneOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"title": "CIBW_TEST_GROUPS"
},
"test-requires": {
"description": "Install Python dependencies before running the tests",
"oneOf": [
Expand Down Expand Up @@ -571,6 +586,9 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
}
Expand Down Expand Up @@ -675,6 +693,9 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
}
Expand Down Expand Up @@ -720,6 +741,9 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
}
Expand Down Expand Up @@ -778,6 +802,9 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
}
Expand Down Expand Up @@ -823,6 +850,9 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-groups": {
"$ref": "#/properties/test-groups"
},
"test-requires": {
"$ref": "#/properties/test-requires"
}
Expand Down
1 change: 1 addition & 0 deletions cibuildwheel/resources/defaults.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ test-command = ""
before-test = ""
test-requires = []
test-extras = []
test-groups = []

container-engine = "docker"

Expand Down
34 changes: 34 additions & 0 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,40 @@ Platform-specific environment variables are also available:<br/>

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


### `CIBW_TEST_GROUPS` {: #test-groups}
> Specify test dependencies from your project's `dependency-groups`
List of
[dependency-groups](https://peps.python.org/pep-0735)
that should be included when installing the wheel prior to running the
tests. This can be used to avoid having to redefine test dependencies in
`CIBW_TEST_REQUIRES` if they are already defined in `pyproject.toml`.

Platform-specific environment variables are also available:<br/>
`CIBW_TEST_GROUPS_MACOS` | `CIBW_TEST_GROUPS_WINDOWS` | `CIBW_TEST_GROUPS_LINUX` | `CIBW_TEST_GROUPS_PYODIDE`

#### Examples

!!! tab examples "Environment variables"

```yaml
# Will cause the wheel to be installed with these groups of dependencies
CIBW_TEST_GROUPS: "test qt"
```

Separate multiple items with a space.

!!! tab examples "pyproject.toml"

```toml
[tool.cibuildwheel]
# Will cause the wheel to be installed with these groups of dependencies
test-groups = ["test", "qt"]
```

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

### `CIBW_TEST_SKIP` {: #test-skip}
> Skip running tests on some builds
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ dependencies = [
"bashlex!=0.13",
"bracex",
"certifi",
"dependency-groups>=1.2",
"filelock",
"packaging>=20.9",
"platformdirs",
Expand Down
33 changes: 33 additions & 0 deletions test/test_testing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import inspect
import os
import subprocess
import textwrap
Expand Down Expand Up @@ -114,6 +115,38 @@ def test_extras_require(tmp_path):
assert set(actual_wheels) == set(expected_wheels)


def test_dependency_groups(tmp_path):
group_project = project_with_a_test.copy()
group_project.files["pyproject.toml"] = inspect.cleandoc("""
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[dependency-groups]
dev = ["pytest"]
""")

project_dir = tmp_path / "project"
group_project.generate(project_dir)

# build and test the wheels
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_TEST_GROUPS": "dev",
# the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} {{project}}/test",
"CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || pytest {project}/test",
},
single_python=True,
)

# also check that we got the right wheels
expected_wheels = utils.expected_wheels("spam", "0.1.0", single_python=True)
assert set(actual_wheels) == set(expected_wheels)


project_with_a_failing_test = test_projects.new_c_project()
project_with_a_failing_test.files["test/spam_test.py"] = r"""
from unittest import TestCase
Expand Down
8 changes: 8 additions & 0 deletions unit_test/options_toml_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
test-command = "pyproject"
test-requires = "something"
test-extras = ["one", "two"]
test-groups = ["three", "four"]
manylinux-x86_64-image = "manylinux1"
Expand Down Expand Up @@ -60,6 +61,7 @@ def test_simple_settings(tmp_path, platform, fname):
== 'THING="OTHER" FOO="BAR"'
)
assert options_reader.get("test-extras", option_format=ListFormat(",")) == "one,two"
assert options_reader.get("test-groups", option_format=ListFormat(" ")) == "three four"

assert options_reader.get("manylinux-x86_64-image") == "manylinux1"
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
Expand All @@ -85,7 +87,9 @@ def test_envvar_override(tmp_path, platform):
"CIBW_MANYLINUX_X86_64_IMAGE": "manylinux_2_24",
"CIBW_TEST_COMMAND": "mytest",
"CIBW_TEST_REQUIRES": "docs",
"CIBW_TEST_GROUPS": "mgroup two",
"CIBW_TEST_REQUIRES_LINUX": "scod",
"CIBW_TEST_GROUPS_LINUX": "lgroup",
},
)

Expand All @@ -99,6 +103,10 @@ def test_envvar_override(tmp_path, platform):
options_reader.get("test-requires", option_format=ListFormat(" "))
== {"windows": "docs", "macos": "docs", "linux": "scod"}[platform]
)
assert (
options_reader.get("test-groups", option_format=ListFormat(" "))
== {"windows": "mgroup two", "macos": "mgroup two", "linux": "lgroup"}[platform]
)
assert options_reader.get("test-command") == "mytest"


Expand Down
Loading

0 comments on commit cc1d977

Please sign in to comment.