Inline dependency-versions syntax (#2122)
* Write docs for inline dependency-versions * Change the `inline` keyword to `packages` for better readability * Implement inline package constraints * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add schema for TOML properties * Change the parsing of the option to parse filenames as-is * Add a unit test for table-parsing of the option * Remove unneeded shlex.quote on the dependency-version test * Tidy-ups, comments, docs fixes * Add test for empty packages option value * Fix empty packages scenario And, remove some optionals to reduce the problem space --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
parent
f5e502aa88
commit
25f2d3f313
@@ -104,6 +104,23 @@ properties:
|
|||||||
dependency-versions:
|
dependency-versions:
|
||||||
default: pinned
|
default: pinned
|
||||||
description: Specify how cibuildwheel controls the versions of the tools it uses
|
description: Specify how cibuildwheel controls the versions of the tools it uses
|
||||||
|
oneOf:
|
||||||
|
- enum: [pinned, latest]
|
||||||
|
- type: string
|
||||||
|
description: Path to a file containing dependency versions, or inline package specifications, starting with "packages:"
|
||||||
|
not:
|
||||||
|
enum: [pinned, latest]
|
||||||
|
- type: object
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
file:
|
||||||
|
type: string
|
||||||
|
- type: object
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
packages:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
type: string
|
type: string
|
||||||
enable:
|
enable:
|
||||||
description: Enable or disable certain builds.
|
description: Enable or disable certain builds.
|
||||||
|
|||||||
+10
-8
@@ -166,6 +166,7 @@ def build_in_container(
|
|||||||
container: OCIContainer,
|
container: OCIContainer,
|
||||||
container_project_path: PurePath,
|
container_project_path: PurePath,
|
||||||
container_package_dir: PurePath,
|
container_package_dir: PurePath,
|
||||||
|
local_tmp_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
container_output_dir = PurePosixPath("/output")
|
container_output_dir = PurePosixPath("/output")
|
||||||
|
|
||||||
@@ -199,22 +200,22 @@ def build_in_container(
|
|||||||
|
|
||||||
for config in platform_configs:
|
for config in platform_configs:
|
||||||
log.build_start(config.identifier)
|
log.build_start(config.identifier)
|
||||||
|
local_identifier_tmp_dir = local_tmp_dir / config.identifier
|
||||||
build_options = options.build_options(config.identifier)
|
build_options = options.build_options(config.identifier)
|
||||||
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
|
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
|
||||||
use_uv = build_frontend.name == "build[uv]"
|
use_uv = build_frontend.name == "build[uv]"
|
||||||
pip = ["uv", "pip"] if use_uv else ["pip"]
|
pip = ["uv", "pip"] if use_uv else ["pip"]
|
||||||
|
|
||||||
dependency_constraint_flags: list[PathOrStr] = []
|
|
||||||
|
|
||||||
log.step("Setting up build environment...")
|
log.step("Setting up build environment...")
|
||||||
|
|
||||||
if build_options.dependency_constraints:
|
dependency_constraint_flags: list[PathOrStr] = []
|
||||||
constraints_file = build_options.dependency_constraints.get_for_python_version(
|
local_constraints_file = build_options.dependency_constraints.get_for_python_version(
|
||||||
config.version
|
version=config.version,
|
||||||
|
tmp_dir=local_identifier_tmp_dir,
|
||||||
)
|
)
|
||||||
|
if local_constraints_file:
|
||||||
container_constraints_file = PurePosixPath("/constraints.txt")
|
container_constraints_file = PurePosixPath("/constraints.txt")
|
||||||
|
container.copy_into(local_constraints_file, container_constraints_file)
|
||||||
container.copy_into(constraints_file, container_constraints_file)
|
|
||||||
dependency_constraint_flags = ["-c", container_constraints_file]
|
dependency_constraint_flags = ["-c", container_constraints_file]
|
||||||
|
|
||||||
env = container.get_environment()
|
env = container.get_environment()
|
||||||
@@ -426,7 +427,7 @@ def build_in_container(
|
|||||||
log.step_end()
|
log.step_end()
|
||||||
|
|
||||||
|
|
||||||
def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
|
def build(options: Options, tmp_path: Path) -> None:
|
||||||
python_configurations = get_python_configurations(
|
python_configurations = get_python_configurations(
|
||||||
options.globals.build_selector, options.globals.architectures
|
options.globals.build_selector, options.globals.architectures
|
||||||
)
|
)
|
||||||
@@ -480,6 +481,7 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
|
|||||||
container=container,
|
container=container,
|
||||||
container_project_path=container_project_path,
|
container_project_path=container_project_path,
|
||||||
container_package_dir=container_package_dir,
|
container_package_dir=container_package_dir,
|
||||||
|
local_tmp_dir=tmp_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
except subprocess.CalledProcessError as error:
|
except subprocess.CalledProcessError as error:
|
||||||
|
|||||||
+8
-11
@@ -421,12 +421,12 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
config_is_arm64 = config.identifier.endswith("arm64")
|
config_is_arm64 = config.identifier.endswith("arm64")
|
||||||
config_is_universal2 = config.identifier.endswith("universal2")
|
config_is_universal2 = config.identifier.endswith("universal2")
|
||||||
|
|
||||||
dependency_constraint_flags: Sequence[PathOrStr] = []
|
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||||
if build_options.dependency_constraints:
|
version=config.version, tmp_dir=identifier_tmp_dir
|
||||||
dependency_constraint_flags = [
|
)
|
||||||
"-c",
|
dependency_constraint_flags: Sequence[PathOrStr] = (
|
||||||
build_options.dependency_constraints.get_for_python_version(config.version),
|
["-c", constraints_path] if constraints_path else []
|
||||||
]
|
)
|
||||||
|
|
||||||
base_python, env = setup_python(
|
base_python, env = setup_python(
|
||||||
identifier_tmp_dir / "build",
|
identifier_tmp_dir / "build",
|
||||||
@@ -463,12 +463,9 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
build_env = env.copy()
|
build_env = env.copy()
|
||||||
if not use_uv:
|
if not use_uv:
|
||||||
build_env["VIRTUALENV_PIP"] = pip_version
|
build_env["VIRTUALENV_PIP"] = pip_version
|
||||||
if build_options.dependency_constraints:
|
if constraints_path:
|
||||||
constraint_path = build_options.dependency_constraints.get_for_python_version(
|
|
||||||
config.version
|
|
||||||
)
|
|
||||||
combine_constraints(
|
combine_constraints(
|
||||||
build_env, constraint_path, identifier_tmp_dir if use_uv else None
|
build_env, constraints_path, identifier_tmp_dir if use_uv else None
|
||||||
)
|
)
|
||||||
|
|
||||||
if build_frontend.name == "pip":
|
if build_frontend.name == "pip":
|
||||||
|
|||||||
+12
-10
@@ -97,7 +97,7 @@ class BuildOptions:
|
|||||||
repair_command: str
|
repair_command: str
|
||||||
manylinux_images: dict[str, str] | None
|
manylinux_images: dict[str, str] | None
|
||||||
musllinux_images: dict[str, str] | None
|
musllinux_images: dict[str, str] | None
|
||||||
dependency_constraints: DependencyConstraints | None
|
dependency_constraints: DependencyConstraints
|
||||||
test_command: str | None
|
test_command: str | None
|
||||||
before_test: str | None
|
before_test: str | None
|
||||||
test_sources: list[str]
|
test_sources: list[str]
|
||||||
@@ -693,7 +693,6 @@ class Options:
|
|||||||
"config-settings", option_format=ShlexTableFormat(sep=" ", pair_sep="=")
|
"config-settings", option_format=ShlexTableFormat(sep=" ", pair_sep="=")
|
||||||
)
|
)
|
||||||
|
|
||||||
dependency_versions = self.reader.get("dependency-versions")
|
|
||||||
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
|
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
|
||||||
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
|
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
|
||||||
test_sources = shlex.split(
|
test_sources = shlex.split(
|
||||||
@@ -739,15 +738,18 @@ class Options:
|
|||||||
with contextlib.suppress(KeyError):
|
with contextlib.suppress(KeyError):
|
||||||
environment.add(env_var_name, self.env[env_var_name], prepend=True)
|
environment.add(env_var_name, self.env[env_var_name], prepend=True)
|
||||||
|
|
||||||
if dependency_versions == "pinned":
|
dependency_versions_str = self.reader.get(
|
||||||
dependency_constraints: DependencyConstraints | None = (
|
"dependency-versions",
|
||||||
DependencyConstraints.with_defaults()
|
env_plat=True,
|
||||||
|
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
|
||||||
)
|
)
|
||||||
elif dependency_versions == "latest":
|
try:
|
||||||
dependency_constraints = None
|
dependency_constraints = DependencyConstraints.from_config_string(
|
||||||
else:
|
dependency_versions_str
|
||||||
dependency_versions_path = Path(dependency_versions)
|
)
|
||||||
dependency_constraints = DependencyConstraints(dependency_versions_path)
|
except (ValueError, OSError) as e:
|
||||||
|
msg = f"Failed to parse dependency versions. {e}"
|
||||||
|
raise errors.ConfigurationError(msg) from e
|
||||||
|
|
||||||
if test_extras:
|
if test_extras:
|
||||||
test_extras = f"[{test_extras}]"
|
test_extras = f"[{test_extras}]"
|
||||||
|
|||||||
@@ -265,12 +265,12 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
built_wheel_dir.mkdir()
|
built_wheel_dir.mkdir()
|
||||||
repaired_wheel_dir.mkdir()
|
repaired_wheel_dir.mkdir()
|
||||||
|
|
||||||
dependency_constraint_flags: Sequence[PathOrStr] = []
|
|
||||||
if build_options.dependency_constraints:
|
|
||||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||||
config.version, variant="pyodide"
|
version=config.version, variant="pyodide", tmp_dir=identifier_tmp_dir
|
||||||
|
)
|
||||||
|
dependency_constraint_flags: Sequence[PathOrStr] = (
|
||||||
|
["-c", constraints_path] if constraints_path else []
|
||||||
)
|
)
|
||||||
dependency_constraint_flags = ["-c", constraints_path]
|
|
||||||
|
|
||||||
env = setup_python(
|
env = setup_python(
|
||||||
identifier_tmp_dir / "build",
|
identifier_tmp_dir / "build",
|
||||||
@@ -319,7 +319,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
build_env = env.copy()
|
build_env = env.copy()
|
||||||
if build_options.dependency_constraints:
|
if constraints_path:
|
||||||
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
|
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
|
||||||
build_env["VIRTUALENV_PIP"] = pip_version
|
build_env["VIRTUALENV_PIP"] = pip_version
|
||||||
call(
|
call(
|
||||||
|
|||||||
@@ -233,7 +233,45 @@
|
|||||||
"dependency-versions": {
|
"dependency-versions": {
|
||||||
"default": "pinned",
|
"default": "pinned",
|
||||||
"description": "Specify how cibuildwheel controls the versions of the tools it uses",
|
"description": "Specify how cibuildwheel controls the versions of the tools it uses",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"pinned",
|
||||||
|
"latest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
"description": "Path to a file containing dependency versions, or inline package specifications, starting with \"packages:\"",
|
||||||
|
"not": {
|
||||||
|
"enum": [
|
||||||
|
"pinned",
|
||||||
|
"latest"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"file": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"packages": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"title": "CIBW_DEPENDENCY_VERSIONS"
|
"title": "CIBW_DEPENDENCY_VERSIONS"
|
||||||
},
|
},
|
||||||
"enable": {
|
"enable": {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import shlex
|
||||||
from collections.abc import Mapping, MutableMapping, Sequence
|
from collections.abc import Mapping, MutableMapping, Sequence
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
from typing import Any, Literal, Self, TypeVar
|
from typing import Any, Literal, Self, TypeVar
|
||||||
|
|
||||||
@@ -6,25 +8,90 @@ from packaging.utils import parse_wheel_filename
|
|||||||
|
|
||||||
from . import resources
|
from . import resources
|
||||||
from .cmd import call
|
from .cmd import call
|
||||||
|
from .helpers import parse_key_value_string, unwrap
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass()
|
||||||
class DependencyConstraints:
|
class DependencyConstraints:
|
||||||
def __init__(self, base_file_path: Path):
|
base_file_path: Path | None = None
|
||||||
assert base_file_path.exists()
|
packages: list[str] = field(default_factory=list)
|
||||||
self.base_file_path = base_file_path.resolve()
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if self.packages and self.base_file_path is not None:
|
||||||
|
msg = "Cannot specify both a file and packages in the dependency constraints"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
if self.base_file_path is not None:
|
||||||
|
if not self.base_file_path.exists():
|
||||||
|
msg = f"Dependency constraints file not found: {self.base_file_path}"
|
||||||
|
raise FileNotFoundError(msg)
|
||||||
|
self.base_file_path = self.base_file_path.resolve()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def with_defaults(cls) -> Self:
|
def pinned(cls) -> Self:
|
||||||
return cls(base_file_path=resources.CONSTRAINTS)
|
return cls(base_file_path=resources.CONSTRAINTS)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def latest(cls) -> Self:
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_config_string(cls, config_string: str) -> Self:
|
||||||
|
if config_string == "pinned":
|
||||||
|
return cls.pinned()
|
||||||
|
|
||||||
|
if config_string == "latest" or not config_string:
|
||||||
|
return cls.latest()
|
||||||
|
|
||||||
|
if config_string.startswith(("file:", "packages:")):
|
||||||
|
# we only do the table-style parsing if it looks like a table,
|
||||||
|
# because this option used to be only a file path. We don't want
|
||||||
|
# to break existing configurations, whose file paths might include
|
||||||
|
# special characters like ':' or ' ', which would require quoting
|
||||||
|
# if they were to be passed as a parse_key_value_string positional
|
||||||
|
# argument.
|
||||||
|
return cls.from_table_style_config_string(config_string)
|
||||||
|
|
||||||
|
return cls(base_file_path=Path(config_string))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_table_style_config_string(cls, config_string: str) -> Self:
|
||||||
|
config_dict = parse_key_value_string(config_string, kw_arg_names=["file", "packages"])
|
||||||
|
files = config_dict.get("file")
|
||||||
|
packages = config_dict.get("packages") or []
|
||||||
|
|
||||||
|
if files and packages:
|
||||||
|
msg = "Cannot specify both a file and packages in dependency-versions"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
if files:
|
||||||
|
if len(files) > 1:
|
||||||
|
msg = unwrap("""
|
||||||
|
Only one file can be specified in dependency-versions.
|
||||||
|
If you intended to pass only one, perhaps you need to quote the path?
|
||||||
|
""")
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
return cls(base_file_path=Path(files[0]))
|
||||||
|
|
||||||
|
return cls(packages=packages)
|
||||||
|
|
||||||
def get_for_python_version(
|
def get_for_python_version(
|
||||||
self, version: str, *, variant: Literal["python", "pyodide"] = "python"
|
self, *, version: str, variant: Literal["python", "pyodide"] = "python", tmp_dir: Path
|
||||||
) -> Path:
|
) -> Path | None:
|
||||||
|
if self.packages:
|
||||||
|
constraint_file = tmp_dir / "constraints.txt"
|
||||||
|
constraint_file.write_text("\n".join(self.packages))
|
||||||
|
return constraint_file
|
||||||
|
|
||||||
|
if self.base_file_path is not None:
|
||||||
version_parts = version.split(".")
|
version_parts = version.split(".")
|
||||||
|
|
||||||
# try to find a version-specific dependency file e.g. if
|
# try to find a version-specific dependency file e.g. if
|
||||||
# ./constraints.txt is the base, look for ./constraints-python36.txt
|
# ./constraints.txt is the base, look for ./constraints-python36.txt
|
||||||
specific_stem = self.base_file_path.stem + f"-{variant}{version_parts[0]}{version_parts[1]}"
|
specific_stem = (
|
||||||
|
self.base_file_path.stem + f"-{variant}{version_parts[0]}{version_parts[1]}"
|
||||||
|
)
|
||||||
specific_name = specific_stem + self.base_file_path.suffix
|
specific_name = specific_stem + self.base_file_path.suffix
|
||||||
specific_file_path = self.base_file_path.with_name(specific_name)
|
specific_file_path = self.base_file_path.with_name(specific_name)
|
||||||
|
|
||||||
@@ -33,20 +100,17 @@ class DependencyConstraints:
|
|||||||
else:
|
else:
|
||||||
return self.base_file_path
|
return self.base_file_path
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
return None
|
||||||
return f"{self.__class__.__name__}({self.base_file_path!r})"
|
|
||||||
|
|
||||||
def __eq__(self, o: object) -> bool:
|
|
||||||
if not isinstance(o, DependencyConstraints):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self.base_file_path == o.base_file_path
|
|
||||||
|
|
||||||
def options_summary(self) -> Any:
|
def options_summary(self) -> Any:
|
||||||
if self == DependencyConstraints.with_defaults():
|
if self == DependencyConstraints.pinned():
|
||||||
return "pinned"
|
return "pinned"
|
||||||
else:
|
elif self.packages:
|
||||||
|
return {"packages": " ".join(shlex.quote(p) for p in self.packages)}
|
||||||
|
elif self.base_file_path is not None:
|
||||||
return self.base_file_path.name
|
return self.base_file_path.name
|
||||||
|
else:
|
||||||
|
return "latest"
|
||||||
|
|
||||||
|
|
||||||
def get_pip_version(env: Mapping[str, str]) -> str:
|
def get_pip_version(env: Mapping[str, str]) -> str:
|
||||||
|
|||||||
+8
-10
@@ -364,12 +364,13 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
built_wheel_dir = identifier_tmp_dir / "built_wheel"
|
built_wheel_dir = identifier_tmp_dir / "built_wheel"
|
||||||
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
|
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
|
||||||
|
|
||||||
dependency_constraint_flags: Sequence[PathOrStr] = []
|
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||||
if build_options.dependency_constraints:
|
version=config.version,
|
||||||
dependency_constraint_flags = [
|
tmp_dir=identifier_tmp_dir,
|
||||||
"-c",
|
)
|
||||||
build_options.dependency_constraints.get_for_python_version(config.version),
|
dependency_constraint_flags: Sequence[PathOrStr] = (
|
||||||
]
|
["-c", constraints_path] if constraints_path else []
|
||||||
|
)
|
||||||
|
|
||||||
# install Python
|
# install Python
|
||||||
base_python, env = setup_python(
|
base_python, env = setup_python(
|
||||||
@@ -411,10 +412,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
if not use_uv:
|
if not use_uv:
|
||||||
build_env["VIRTUALENV_PIP"] = pip_version
|
build_env["VIRTUALENV_PIP"] = pip_version
|
||||||
|
|
||||||
if build_options.dependency_constraints:
|
if constraints_path:
|
||||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
|
||||||
config.version
|
|
||||||
)
|
|
||||||
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
|
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
|
||||||
|
|
||||||
if build_frontend.name == "pip":
|
if build_frontend.name == "pip":
|
||||||
|
|||||||
+22
-4
@@ -1356,9 +1356,10 @@ Options can be supplied after the name.
|
|||||||
|
|
||||||
|
|
||||||
### `CIBW_DEPENDENCY_VERSIONS` {: #dependency-versions}
|
### `CIBW_DEPENDENCY_VERSIONS` {: #dependency-versions}
|
||||||
> Specify how cibuildwheel controls the versions of the tools it uses
|
|
||||||
|
|
||||||
Options: `pinned` `latest` `<your constraints file>`
|
> Control the versions of the tools cibuildwheel uses
|
||||||
|
|
||||||
|
Options: `pinned` `latest` `packages: SPECIFIER...` `<your constraints file>`
|
||||||
|
|
||||||
Default: `pinned`
|
Default: `pinned`
|
||||||
|
|
||||||
@@ -1373,7 +1374,8 @@ fixes that can't wait for a new cibuildwheel release.
|
|||||||
|
|
||||||
To control the versions of dependencies yourself, you can supply a [pip
|
To control the versions of dependencies yourself, you can supply a [pip
|
||||||
constraints](https://pip.pypa.io/en/stable/user_guide/#constraints-files) file
|
constraints](https://pip.pypa.io/en/stable/user_guide/#constraints-files) file
|
||||||
here and it will be used instead.
|
here and it will be used instead. Alternatively, you can list constraint
|
||||||
|
specifiers inline with the `packages: SPECIFIER...` syntax.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
If you need different dependencies for each python version, provide them
|
If you need different dependencies for each python version, provide them
|
||||||
@@ -1404,6 +1406,15 @@ Platform-specific environment variables are also available:<br/>
|
|||||||
|
|
||||||
# Use your own pip constraints file
|
# Use your own pip constraints file
|
||||||
CIBW_DEPENDENCY_VERSIONS: ./constraints.txt
|
CIBW_DEPENDENCY_VERSIONS: ./constraints.txt
|
||||||
|
|
||||||
|
# Specify requirements inline
|
||||||
|
CIBW_DEPENDENCY_VERSIONS: "packages: auditwheel==6.2.0"
|
||||||
|
|
||||||
|
# Choose a specific pyodide-build version
|
||||||
|
CIBW_DEPENDENCY_VERSIONS_PYODIDE: "packages: pyodide-build==0.29.1"
|
||||||
|
|
||||||
|
# Use shell-style quoting around spaces package specifiers
|
||||||
|
CIBW_DEPENDENCY_VERSIONS: "packages: 'pip >=16.0.0, !=17'"
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! tab examples "pyproject.toml"
|
!!! tab examples "pyproject.toml"
|
||||||
@@ -1417,7 +1428,14 @@ Platform-specific environment variables are also available:<br/>
|
|||||||
dependency-versions = "latest"
|
dependency-versions = "latest"
|
||||||
|
|
||||||
# Use your own pip constraints file
|
# Use your own pip constraints file
|
||||||
dependency-versions = "./constraints.txt"
|
dependency-versions = { file = "./constraints.txt" }
|
||||||
|
|
||||||
|
# Specify requirements inline
|
||||||
|
dependency-versions = { packages = ["auditwheel==6.2.0"] }
|
||||||
|
|
||||||
|
[tool.cibuildwheel.pyodide]
|
||||||
|
# Choose a specific pyodide-build version
|
||||||
|
dependency-versions = { packages = ["pyodide-build==0.29.1"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ def test_pinned_versions(tmp_path, python_version, build_frontend_env_nouv):
|
|||||||
assert set(actual_wheels) == set(expected_wheels)
|
assert set(actual_wheels) == set(expected_wheels)
|
||||||
|
|
||||||
|
|
||||||
def test_dependency_constraints_file(tmp_path, build_frontend_env_nouv):
|
@pytest.mark.parametrize("method", ["inline", "file"])
|
||||||
|
def test_dependency_constraints(method, tmp_path, build_frontend_env_nouv):
|
||||||
if utils.platform == "linux":
|
if utils.platform == "linux":
|
||||||
pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead")
|
pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead")
|
||||||
|
|
||||||
@@ -103,6 +104,7 @@ def test_dependency_constraints_file(tmp_path, build_frontend_env_nouv):
|
|||||||
"delocate": "0.10.3",
|
"delocate": "0.10.3",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if method == "file":
|
||||||
constraints_file = tmp_path / "constraints file.txt"
|
constraints_file = tmp_path / "constraints file.txt"
|
||||||
constraints_file.write_text(
|
constraints_file.write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
@@ -112,6 +114,14 @@ def test_dependency_constraints_file(tmp_path, build_frontend_env_nouv):
|
|||||||
""".format(**tool_versions)
|
""".format(**tool_versions)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
dependency_version_option = str(constraints_file)
|
||||||
|
elif method == "inline":
|
||||||
|
dependency_version_option = "packages: " + " ".join(
|
||||||
|
f"{k}=={v}" for k, v in tool_versions.items()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
msg = f"Unknown method: {method}"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
build_environment = {}
|
build_environment = {}
|
||||||
|
|
||||||
@@ -126,7 +136,7 @@ def test_dependency_constraints_file(tmp_path, build_frontend_env_nouv):
|
|||||||
project_dir,
|
project_dir,
|
||||||
add_env={
|
add_env={
|
||||||
"CIBW_ENVIRONMENT": cibw_environment_option,
|
"CIBW_ENVIRONMENT": cibw_environment_option,
|
||||||
"CIBW_DEPENDENCY_VERSIONS": str(constraints_file),
|
"CIBW_DEPENDENCY_VERSIONS": dependency_version_option,
|
||||||
**build_frontend_env_nouv,
|
**build_frontend_env_nouv,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +1,55 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from cibuildwheel.util.packaging import DependencyConstraints
|
from cibuildwheel.util.packaging import DependencyConstraints
|
||||||
|
|
||||||
|
|
||||||
def test_defaults():
|
def test_defaults(tmp_path: Path) -> None:
|
||||||
dependency_constraints = DependencyConstraints.with_defaults()
|
dependency_constraints = DependencyConstraints.pinned()
|
||||||
|
|
||||||
project_root = Path(__file__).parents[1]
|
project_root = Path(__file__).parents[1]
|
||||||
resources_dir = project_root / "cibuildwheel" / "resources"
|
resources_dir = project_root / "cibuildwheel" / "resources"
|
||||||
|
|
||||||
|
assert dependency_constraints.base_file_path
|
||||||
assert dependency_constraints.base_file_path.samefile(resources_dir / "constraints.txt")
|
assert dependency_constraints.base_file_path.samefile(resources_dir / "constraints.txt")
|
||||||
assert dependency_constraints.get_for_python_version("3.99").samefile(
|
|
||||||
resources_dir / "constraints.txt"
|
constraints_file = dependency_constraints.get_for_python_version(
|
||||||
|
version="3.99", tmp_dir=tmp_path
|
||||||
)
|
)
|
||||||
assert dependency_constraints.get_for_python_version("3.9").samefile(
|
assert constraints_file
|
||||||
resources_dir / "constraints-python39.txt"
|
assert constraints_file.samefile(resources_dir / "constraints.txt")
|
||||||
|
|
||||||
|
constraints_file = dependency_constraints.get_for_python_version(
|
||||||
|
version="3.9", tmp_dir=tmp_path
|
||||||
)
|
)
|
||||||
assert dependency_constraints.get_for_python_version("3.13").samefile(
|
assert constraints_file
|
||||||
resources_dir / "constraints-python313.txt"
|
assert constraints_file.samefile(resources_dir / "constraints-python39.txt")
|
||||||
|
|
||||||
|
constraints_file = dependency_constraints.get_for_python_version(
|
||||||
|
version="3.13", tmp_dir=tmp_path
|
||||||
)
|
)
|
||||||
|
assert constraints_file
|
||||||
|
assert constraints_file.samefile(resources_dir / "constraints-python313.txt")
|
||||||
|
|
||||||
|
|
||||||
|
def test_inline_packages(tmp_path: Path) -> None:
|
||||||
|
dependency_constraints = DependencyConstraints(
|
||||||
|
base_file_path=None,
|
||||||
|
packages=["foo==1.2.3", "bar==4.5.6"],
|
||||||
|
)
|
||||||
|
|
||||||
|
constraint_file = dependency_constraints.get_for_python_version(version="x.x", tmp_dir=tmp_path)
|
||||||
|
assert constraint_file
|
||||||
|
constraints_file_contents = constraint_file.read_text()
|
||||||
|
|
||||||
|
assert constraints_file_contents == "foo==1.2.3\nbar==4.5.6"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("config_string", ["", "latest", "packages:"])
|
||||||
|
def test_empty_constraints(config_string: str) -> None:
|
||||||
|
dependency_constraints = DependencyConstraints.from_config_string(config_string)
|
||||||
|
|
||||||
|
assert not dependency_constraints.packages
|
||||||
|
assert not dependency_constraints.base_file_path
|
||||||
|
assert dependency_constraints == DependencyConstraints.latest()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from cibuildwheel.frontend import _split_config_settings
|
|||||||
from cibuildwheel.options import BuildOptions, _get_pinned_container_images
|
from cibuildwheel.options import BuildOptions, _get_pinned_container_images
|
||||||
from cibuildwheel.selector import BuildSelector, EnableGroup
|
from cibuildwheel.selector import BuildSelector, EnableGroup
|
||||||
from cibuildwheel.util import resources
|
from cibuildwheel.util import resources
|
||||||
|
from cibuildwheel.util.packaging import DependencyConstraints
|
||||||
|
|
||||||
# CIBW_PLATFORM is tested in main_platform_test.py
|
# CIBW_PLATFORM is tested in main_platform_test.py
|
||||||
|
|
||||||
@@ -342,6 +343,43 @@ def test_before_all(before_all, platform_specific, platform, intercepted_build_a
|
|||||||
assert build_options.before_all == (before_all or "")
|
assert build_options.before_all == (before_all or "")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"dependency_versions",
|
||||||
|
[None, "pinned", "latest", "FILE", "packages: pip==21.0.0"],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize("platform_specific", [False, True])
|
||||||
|
def test_dependency_versions(
|
||||||
|
dependency_versions, platform_specific, platform, intercepted_build_args, monkeypatch, tmp_path
|
||||||
|
):
|
||||||
|
option_value = dependency_versions
|
||||||
|
|
||||||
|
if dependency_versions == "FILE":
|
||||||
|
constraints_file = tmp_path / "constraints.txt"
|
||||||
|
constraints_file.write_text("foo==1.2.3\nbar==4.5.6")
|
||||||
|
option_value = str(constraints_file)
|
||||||
|
|
||||||
|
if option_value is not None:
|
||||||
|
if platform_specific:
|
||||||
|
monkeypatch.setenv("CIBW_DEPENDENCY_VERSIONS_" + platform.upper(), option_value)
|
||||||
|
monkeypatch.setenv("CIBW_DEPENDENCY_VERSIONS", "overwritten")
|
||||||
|
else:
|
||||||
|
monkeypatch.setenv("CIBW_DEPENDENCY_VERSIONS", option_value)
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
build_options: BuildOptions = intercepted_build_args.args[0].build_options(identifier=None)
|
||||||
|
dependency_constraints = build_options.dependency_constraints
|
||||||
|
if dependency_versions is None or dependency_versions == "pinned":
|
||||||
|
assert dependency_constraints == DependencyConstraints.pinned()
|
||||||
|
elif dependency_versions == "latest":
|
||||||
|
assert dependency_constraints == DependencyConstraints.latest()
|
||||||
|
elif dependency_versions == "FILE":
|
||||||
|
assert dependency_constraints.base_file_path
|
||||||
|
assert dependency_constraints.base_file_path.samefile(Path(option_value))
|
||||||
|
elif dependency_versions.startswith("packages:"):
|
||||||
|
assert dependency_constraints.packages == ["pip==21.0.0"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("method", ["unset", "command_line", "env_var"])
|
@pytest.mark.parametrize("method", ["unset", "command_line", "env_var"])
|
||||||
def test_debug_traceback(monkeypatch, method, capfd):
|
def test_debug_traceback(monkeypatch, method, capfd):
|
||||||
if method == "command_line":
|
if method == "command_line":
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ from cibuildwheel.options import (
|
|||||||
_get_pinned_container_images,
|
_get_pinned_container_images,
|
||||||
)
|
)
|
||||||
from cibuildwheel.selector import EnableGroup
|
from cibuildwheel.selector import EnableGroup
|
||||||
|
from cibuildwheel.util import resources
|
||||||
|
from cibuildwheel.util.packaging import DependencyConstraints
|
||||||
|
|
||||||
PYPROJECT_1 = """
|
PYPROJECT_1 = """
|
||||||
[tool.cibuildwheel]
|
[tool.cibuildwheel]
|
||||||
@@ -468,3 +470,62 @@ def test_free_threaded_support(
|
|||||||
assert EnableGroup.CPythonFreeThreading in options.globals.build_selector.enable
|
assert EnableGroup.CPythonFreeThreading in options.globals.build_selector.enable
|
||||||
else:
|
else:
|
||||||
assert EnableGroup.CPythonFreeThreading not in options.globals.build_selector.enable
|
assert EnableGroup.CPythonFreeThreading not in options.globals.build_selector.enable
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("toml_assignment", "base_file_path", "packages"),
|
||||||
|
[
|
||||||
|
("", resources.CONSTRAINTS, []),
|
||||||
|
("dependency-versions = 'pinned'", resources.CONSTRAINTS, []),
|
||||||
|
("dependency-versions = 'latest'", None, []),
|
||||||
|
("dependency-versions = 'constraints file.txt'", Path("constraints file.txt"), []),
|
||||||
|
(
|
||||||
|
"dependency-versions = \"file:'constraints file.txt'\"",
|
||||||
|
Path("constraints file.txt"),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dependency-versions = {file = 'constraints file.txt'}",
|
||||||
|
Path("constraints file.txt"),
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"dependency-versions = 'packages: foo==1.2.3 bar==4.5.6'",
|
||||||
|
None,
|
||||||
|
["foo==1.2.3", "bar==4.5.6"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_dependency_versions_toml(
|
||||||
|
tmp_path: Path,
|
||||||
|
toml_assignment: str,
|
||||||
|
base_file_path: Path | None,
|
||||||
|
packages: list[str] | None,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
args = CommandLineArguments.defaults()
|
||||||
|
args.package_dir = tmp_path
|
||||||
|
|
||||||
|
(tmp_path / "constraints file.txt").write_text("")
|
||||||
|
monkeypatch.chdir(tmp_path)
|
||||||
|
|
||||||
|
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
||||||
|
pyproject_toml.write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
f"""\
|
||||||
|
[tool.cibuildwheel]
|
||||||
|
{toml_assignment}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
options = Options(platform="linux", command_line_arguments=args, env={})
|
||||||
|
parsed_dependency_constraints = options.build_options(None).dependency_constraints
|
||||||
|
if base_file_path is None and packages is None:
|
||||||
|
assert parsed_dependency_constraints == DependencyConstraints.latest()
|
||||||
|
else:
|
||||||
|
if parsed_dependency_constraints.base_file_path and base_file_path:
|
||||||
|
assert parsed_dependency_constraints.base_file_path.samefile(base_file_path)
|
||||||
|
else:
|
||||||
|
assert parsed_dependency_constraints.base_file_path == base_file_path
|
||||||
|
assert parsed_dependency_constraints.packages == packages
|
||||||
|
|||||||
Reference in New Issue
Block a user