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
+11
-9
@@ -166,6 +166,7 @@ def build_in_container(
|
||||
container: OCIContainer,
|
||||
container_project_path: PurePath,
|
||||
container_package_dir: PurePath,
|
||||
local_tmp_dir: Path,
|
||||
) -> None:
|
||||
container_output_dir = PurePosixPath("/output")
|
||||
|
||||
@@ -199,22 +200,22 @@ def build_in_container(
|
||||
|
||||
for config in platform_configs:
|
||||
log.build_start(config.identifier)
|
||||
local_identifier_tmp_dir = local_tmp_dir / config.identifier
|
||||
build_options = options.build_options(config.identifier)
|
||||
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
|
||||
use_uv = build_frontend.name == "build[uv]"
|
||||
pip = ["uv", "pip"] if use_uv else ["pip"]
|
||||
|
||||
dependency_constraint_flags: list[PathOrStr] = []
|
||||
|
||||
log.step("Setting up build environment...")
|
||||
|
||||
if build_options.dependency_constraints:
|
||||
constraints_file = build_options.dependency_constraints.get_for_python_version(
|
||||
config.version
|
||||
)
|
||||
dependency_constraint_flags: list[PathOrStr] = []
|
||||
local_constraints_file = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version,
|
||||
tmp_dir=local_identifier_tmp_dir,
|
||||
)
|
||||
if local_constraints_file:
|
||||
container_constraints_file = PurePosixPath("/constraints.txt")
|
||||
|
||||
container.copy_into(constraints_file, container_constraints_file)
|
||||
container.copy_into(local_constraints_file, container_constraints_file)
|
||||
dependency_constraint_flags = ["-c", container_constraints_file]
|
||||
|
||||
env = container.get_environment()
|
||||
@@ -426,7 +427,7 @@ def build_in_container(
|
||||
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(
|
||||
options.globals.build_selector, options.globals.architectures
|
||||
)
|
||||
@@ -480,6 +481,7 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
|
||||
container=container,
|
||||
container_project_path=container_project_path,
|
||||
container_package_dir=container_package_dir,
|
||||
local_tmp_dir=tmp_path,
|
||||
)
|
||||
|
||||
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_universal2 = config.identifier.endswith("universal2")
|
||||
|
||||
dependency_constraint_flags: Sequence[PathOrStr] = []
|
||||
if build_options.dependency_constraints:
|
||||
dependency_constraint_flags = [
|
||||
"-c",
|
||||
build_options.dependency_constraints.get_for_python_version(config.version),
|
||||
]
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version, tmp_dir=identifier_tmp_dir
|
||||
)
|
||||
dependency_constraint_flags: Sequence[PathOrStr] = (
|
||||
["-c", constraints_path] if constraints_path else []
|
||||
)
|
||||
|
||||
base_python, env = setup_python(
|
||||
identifier_tmp_dir / "build",
|
||||
@@ -463,12 +463,9 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
build_env = env.copy()
|
||||
if not use_uv:
|
||||
build_env["VIRTUALENV_PIP"] = pip_version
|
||||
if build_options.dependency_constraints:
|
||||
constraint_path = build_options.dependency_constraints.get_for_python_version(
|
||||
config.version
|
||||
)
|
||||
if constraints_path:
|
||||
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":
|
||||
|
||||
+12
-10
@@ -97,7 +97,7 @@ class BuildOptions:
|
||||
repair_command: str
|
||||
manylinux_images: dict[str, str] | None
|
||||
musllinux_images: dict[str, str] | None
|
||||
dependency_constraints: DependencyConstraints | None
|
||||
dependency_constraints: DependencyConstraints
|
||||
test_command: str | None
|
||||
before_test: str | None
|
||||
test_sources: list[str]
|
||||
@@ -693,7 +693,6 @@ class Options:
|
||||
"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=" && "))
|
||||
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
|
||||
test_sources = shlex.split(
|
||||
@@ -739,15 +738,18 @@ class Options:
|
||||
with contextlib.suppress(KeyError):
|
||||
environment.add(env_var_name, self.env[env_var_name], prepend=True)
|
||||
|
||||
if dependency_versions == "pinned":
|
||||
dependency_constraints: DependencyConstraints | None = (
|
||||
DependencyConstraints.with_defaults()
|
||||
dependency_versions_str = self.reader.get(
|
||||
"dependency-versions",
|
||||
env_plat=True,
|
||||
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
|
||||
)
|
||||
try:
|
||||
dependency_constraints = DependencyConstraints.from_config_string(
|
||||
dependency_versions_str
|
||||
)
|
||||
elif dependency_versions == "latest":
|
||||
dependency_constraints = None
|
||||
else:
|
||||
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:
|
||||
test_extras = f"[{test_extras}]"
|
||||
|
||||
@@ -265,12 +265,12 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
built_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(
|
||||
config.version, variant="pyodide"
|
||||
)
|
||||
dependency_constraint_flags = ["-c", constraints_path]
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version, variant="pyodide", tmp_dir=identifier_tmp_dir
|
||||
)
|
||||
dependency_constraint_flags: Sequence[PathOrStr] = (
|
||||
["-c", constraints_path] if constraints_path else []
|
||||
)
|
||||
|
||||
env = setup_python(
|
||||
identifier_tmp_dir / "build",
|
||||
@@ -319,7 +319,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
build_env = env.copy()
|
||||
if build_options.dependency_constraints:
|
||||
if constraints_path:
|
||||
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
|
||||
build_env["VIRTUALENV_PIP"] = pip_version
|
||||
call(
|
||||
|
||||
@@ -233,7 +233,45 @@
|
||||
"dependency-versions": {
|
||||
"default": "pinned",
|
||||
"description": "Specify how cibuildwheel controls the versions of the tools it uses",
|
||||
"type": "string",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "CIBW_DEPENDENCY_VERSIONS"
|
||||
},
|
||||
"enable": {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import shlex
|
||||
from collections.abc import Mapping, MutableMapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path, PurePath
|
||||
from typing import Any, Literal, Self, TypeVar
|
||||
|
||||
@@ -6,47 +8,109 @@ from packaging.utils import parse_wheel_filename
|
||||
|
||||
from . import resources
|
||||
from .cmd import call
|
||||
from .helpers import parse_key_value_string, unwrap
|
||||
|
||||
|
||||
@dataclass()
|
||||
class DependencyConstraints:
|
||||
def __init__(self, base_file_path: Path):
|
||||
assert base_file_path.exists()
|
||||
self.base_file_path = base_file_path.resolve()
|
||||
base_file_path: Path | None = None
|
||||
packages: list[str] = field(default_factory=list)
|
||||
|
||||
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
|
||||
def with_defaults(cls) -> Self:
|
||||
def pinned(cls) -> Self:
|
||||
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(
|
||||
self, version: str, *, variant: Literal["python", "pyodide"] = "python"
|
||||
) -> Path:
|
||||
version_parts = version.split(".")
|
||||
self, *, version: str, variant: Literal["python", "pyodide"] = "python", tmp_dir: Path
|
||||
) -> Path | None:
|
||||
if self.packages:
|
||||
constraint_file = tmp_dir / "constraints.txt"
|
||||
constraint_file.write_text("\n".join(self.packages))
|
||||
return constraint_file
|
||||
|
||||
# try to find a version-specific dependency file e.g. if
|
||||
# ./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_name = specific_stem + self.base_file_path.suffix
|
||||
specific_file_path = self.base_file_path.with_name(specific_name)
|
||||
if self.base_file_path is not None:
|
||||
version_parts = version.split(".")
|
||||
|
||||
if specific_file_path.exists():
|
||||
return specific_file_path
|
||||
else:
|
||||
return self.base_file_path
|
||||
# try to find a version-specific dependency file e.g. if
|
||||
# ./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_name = specific_stem + self.base_file_path.suffix
|
||||
specific_file_path = self.base_file_path.with_name(specific_name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.base_file_path!r})"
|
||||
if specific_file_path.exists():
|
||||
return specific_file_path
|
||||
else:
|
||||
return self.base_file_path
|
||||
|
||||
def __eq__(self, o: object) -> bool:
|
||||
if not isinstance(o, DependencyConstraints):
|
||||
return False
|
||||
|
||||
return self.base_file_path == o.base_file_path
|
||||
return None
|
||||
|
||||
def options_summary(self) -> Any:
|
||||
if self == DependencyConstraints.with_defaults():
|
||||
if self == DependencyConstraints.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
|
||||
else:
|
||||
return "latest"
|
||||
|
||||
|
||||
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"
|
||||
repaired_wheel_dir = identifier_tmp_dir / "repaired_wheel"
|
||||
|
||||
dependency_constraint_flags: Sequence[PathOrStr] = []
|
||||
if build_options.dependency_constraints:
|
||||
dependency_constraint_flags = [
|
||||
"-c",
|
||||
build_options.dependency_constraints.get_for_python_version(config.version),
|
||||
]
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
version=config.version,
|
||||
tmp_dir=identifier_tmp_dir,
|
||||
)
|
||||
dependency_constraint_flags: Sequence[PathOrStr] = (
|
||||
["-c", constraints_path] if constraints_path else []
|
||||
)
|
||||
|
||||
# install Python
|
||||
base_python, env = setup_python(
|
||||
@@ -411,10 +412,7 @@ def build(options: Options, tmp_path: Path) -> None:
|
||||
if not use_uv:
|
||||
build_env["VIRTUALENV_PIP"] = pip_version
|
||||
|
||||
if build_options.dependency_constraints:
|
||||
constraints_path = build_options.dependency_constraints.get_for_python_version(
|
||||
config.version
|
||||
)
|
||||
if constraints_path:
|
||||
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
|
||||
|
||||
if build_frontend.name == "pip":
|
||||
|
||||
Reference in New Issue
Block a user