Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support test-groups #2063

Merged
merged 5 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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