Skip to content

Commit 870a1dd

Browse files
committed
Implement inline package constraints
1 parent 69f8175 commit 870a1dd

File tree

10 files changed

+171
-47
lines changed

10 files changed

+171
-47
lines changed

cibuildwheel/linux.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ def build_in_container(
168168
container: OCIContainer,
169169
container_project_path: PurePath,
170170
container_package_dir: PurePath,
171+
local_tmp_dir: Path,
171172
) -> None:
172173
container_output_dir = PurePosixPath("/output")
173174

@@ -201,6 +202,7 @@ def build_in_container(
201202

202203
for config in platform_configs:
203204
log.build_start(config.identifier)
205+
local_identifier_tmp_dir = local_tmp_dir / config.identifier
204206
build_options = options.build_options(config.identifier)
205207
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
206208
use_uv = build_frontend.name == "build[uv]" and Version(config.version) >= Version("3.8")
@@ -211,12 +213,13 @@ def build_in_container(
211213
log.step("Setting up build environment...")
212214

213215
if build_options.dependency_constraints:
214-
constraints_file = build_options.dependency_constraints.get_for_python_version(
215-
config.version
216+
local_constraints_file = build_options.dependency_constraints.get_for_python_version(
217+
version=config.version,
218+
tmp_dir=local_identifier_tmp_dir,
216219
)
217220
container_constraints_file = PurePosixPath("/constraints.txt")
218221

219-
container.copy_into(constraints_file, container_constraints_file)
222+
container.copy_into(local_constraints_file, container_constraints_file)
220223
dependency_constraint_flags = ["-c", container_constraints_file]
221224

222225
env = container.get_environment()
@@ -428,7 +431,7 @@ def build_in_container(
428431
log.step_end()
429432

430433

431-
def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
434+
def build(options: Options, tmp_path: Path) -> None:
432435
python_configurations = get_python_configurations(
433436
options.globals.build_selector, options.globals.architectures
434437
)
@@ -482,6 +485,7 @@ def build(options: Options, tmp_path: Path) -> None: # noqa: ARG001
482485
container=container,
483486
container_project_path=container_project_path,
484487
container_package_dir=container_package_dir,
488+
local_tmp_dir=tmp_path,
485489
)
486490

487491
except subprocess.CalledProcessError as error:

cibuildwheel/macos.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,9 @@ def build(options: Options, tmp_path: Path) -> None:
436436
if build_options.dependency_constraints:
437437
dependency_constraint_flags = [
438438
"-c",
439-
build_options.dependency_constraints.get_for_python_version(config.version),
439+
build_options.dependency_constraints.get_for_python_version(
440+
version=config.version, tmp_dir=identifier_tmp_dir
441+
),
440442
]
441443

442444
base_python, env = setup_python(
@@ -476,7 +478,7 @@ def build(options: Options, tmp_path: Path) -> None:
476478
build_env["VIRTUALENV_PIP"] = pip_version
477479
if build_options.dependency_constraints:
478480
constraint_path = build_options.dependency_constraints.get_for_python_version(
479-
config.version
481+
version=config.version, tmp_dir=identifier_tmp_dir
480482
)
481483
combine_constraints(
482484
build_env, constraint_path, identifier_tmp_dir if use_uv else None

cibuildwheel/options.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,6 @@ def build_options(self, identifier: str | None) -> BuildOptions:
690690
"config-settings", option_format=ShlexTableFormat(sep=" ", pair_sep="=")
691691
)
692692

693-
dependency_versions = self.reader.get("dependency-versions")
694693
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
695694
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
696695
test_sources = shlex.split(
@@ -736,15 +735,18 @@ def build_options(self, identifier: str | None) -> BuildOptions:
736735
with contextlib.suppress(KeyError):
737736
environment.add(env_var_name, self.env[env_var_name], prepend=True)
738737

739-
if dependency_versions == "pinned":
740-
dependency_constraints: None | (
741-
DependencyConstraints
742-
) = DependencyConstraints.with_defaults()
743-
elif dependency_versions == "latest":
744-
dependency_constraints = None
745-
else:
746-
dependency_versions_path = Path(dependency_versions)
747-
dependency_constraints = DependencyConstraints(dependency_versions_path)
738+
dependency_versions_str = self.reader.get(
739+
"dependency-versions",
740+
env_plat=True,
741+
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
742+
)
743+
try:
744+
dependency_constraints = DependencyConstraints.from_config_string(
745+
dependency_versions_str
746+
)
747+
except ValueError as e:
748+
msg = f"Failed to parse dependency versions. {e}"
749+
raise errors.ConfigurationError(msg) from e
748750

749751
if test_extras:
750752
test_extras = f"[{test_extras}]"

cibuildwheel/pyodide.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def build(options: Options, tmp_path: Path) -> None:
270270
dependency_constraint_flags: Sequence[PathOrStr] = []
271271
if build_options.dependency_constraints:
272272
constraints_path = build_options.dependency_constraints.get_for_python_version(
273-
config.version, variant="pyodide"
273+
version=config.version, variant="pyodide", tmp_dir=identifier_tmp_dir
274274
)
275275
dependency_constraint_flags = ["-c", constraints_path]
276276

cibuildwheel/util/packaging.py

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,75 @@
11
from __future__ import annotations
22

3+
import shlex
34
from collections.abc import Mapping, MutableMapping, Sequence
5+
from dataclasses import dataclass
46
from pathlib import Path, PurePath
57
from typing import Any, Literal, TypeVar
68

79
from packaging.utils import parse_wheel_filename
810

911
from . import resources
1012
from .cmd import call
13+
from .helpers import parse_key_value_string
1114

1215

16+
@dataclass()
1317
class DependencyConstraints:
14-
def __init__(self, base_file_path: Path):
15-
assert base_file_path.exists()
16-
self.base_file_path = base_file_path.resolve()
18+
base_file_path: Path | None = None
19+
packages: list[str] | None = None
20+
21+
def __post_init__(self) -> None:
22+
if self.packages is not None and self.base_file_path is not None:
23+
msg = "Cannot specify both a file and packages in the dependency constraints"
24+
raise ValueError(msg)
25+
26+
if self.base_file_path is not None:
27+
assert self.base_file_path.exists()
28+
self.base_file_path = self.base_file_path.resolve()
1729

1830
@staticmethod
1931
def with_defaults() -> DependencyConstraints:
2032
return DependencyConstraints(base_file_path=resources.CONSTRAINTS)
2133

34+
@staticmethod
35+
def from_config_string(config_string: str) -> DependencyConstraints | None:
36+
config_dict = parse_key_value_string(config_string, ["file"], ["packages"])
37+
file_or_keywords = config_dict.get("file")
38+
packages = config_dict.get("packages")
39+
40+
if file_or_keywords and packages:
41+
msg = "Cannot specify both a file and packages in dependency-versions"
42+
raise ValueError(msg)
43+
44+
if packages:
45+
return DependencyConstraints(packages=packages)
46+
47+
if file_or_keywords and len(file_or_keywords) > 1:
48+
msg = "Only one file or keyword can be specified in dependency-versions"
49+
raise ValueError(msg)
50+
51+
file_or_keyword = file_or_keywords[0] if file_or_keywords else None
52+
53+
if file_or_keyword == "latest":
54+
return None
55+
56+
if file_or_keyword == "pinned" or not file_or_keyword:
57+
return DependencyConstraints.with_defaults()
58+
59+
return DependencyConstraints(base_file_path=Path(file_or_keyword))
60+
2261
def get_for_python_version(
23-
self, version: str, *, variant: Literal["python", "pyodide"] = "python"
62+
self, *, version: str, variant: Literal["python", "pyodide"] = "python", tmp_dir: Path
2463
) -> Path:
64+
if self.packages:
65+
constraint_file = tmp_dir / "constraints.txt"
66+
constraint_file.write_text("\n".join(self.packages))
67+
return constraint_file
68+
69+
assert (
70+
self.base_file_path is not None
71+
), "DependencyConstraints should have either a file or packages"
72+
2573
version_parts = version.split(".")
2674

2775
# try to find a version-specific dependency file e.g. if
@@ -35,19 +83,15 @@ def get_for_python_version(
3583
else:
3684
return self.base_file_path
3785

38-
def __repr__(self) -> str:
39-
return f"{self.__class__.__name__}({self.base_file_path!r})"
40-
41-
def __eq__(self, o: object) -> bool:
42-
if not isinstance(o, DependencyConstraints):
43-
return False
44-
45-
return self.base_file_path == o.base_file_path
46-
4786
def options_summary(self) -> Any:
4887
if self == DependencyConstraints.with_defaults():
4988
return "pinned"
89+
elif self.packages:
90+
return {"packages": " ".join(shlex.quote(p) for p in self.packages)}
5091
else:
92+
assert (
93+
self.base_file_path is not None
94+
), "DependencyConstraints should have either a file or packages"
5195
return self.base_file_path.name
5296

5397

cibuildwheel/windows.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,10 @@ def build(options: Options, tmp_path: Path) -> None:
373373
if build_options.dependency_constraints:
374374
dependency_constraint_flags = [
375375
"-c",
376-
build_options.dependency_constraints.get_for_python_version(config.version),
376+
build_options.dependency_constraints.get_for_python_version(
377+
version=config.version,
378+
tmp_dir=identifier_tmp_dir,
379+
),
377380
]
378381

379382
# install Python
@@ -418,7 +421,8 @@ def build(options: Options, tmp_path: Path) -> None:
418421

419422
if build_options.dependency_constraints:
420423
constraints_path = build_options.dependency_constraints.get_for_python_version(
421-
config.version
424+
version=config.version,
425+
tmp_dir=identifier_tmp_dir,
422426
)
423427
combine_constraints(build_env, constraints_path, identifier_tmp_dir)
424428

docs/options.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,10 @@ Platform-specific environment variables are also available:<br/>
14061406

14071407
# Choose a specific pyodide-build version
14081408
CIBW_DEPENDENCY_VERSIONS_PYODIDE: "packages: pyodide-build==0.29.1"
1409+
1410+
# Use shell-style quoting around spaces in paths or package specifiers
1411+
CIBW_DEPENDENCY_VERSIONS: "'./constraints file.txt'"
1412+
CIBW_DEPENDENCY_VERSIONS: "packages: 'pip >=16.0.0, !=17'"
14091413
```
14101414

14111415
!!! tab examples "pyproject.toml"
@@ -1419,7 +1423,7 @@ Platform-specific environment variables are also available:<br/>
14191423
dependency-versions = "latest"
14201424

14211425
# Use your own pip constraints file
1422-
dependency-versions = "./constraints.txt"
1426+
dependency-versions = { file = "./constraints.txt" }
14231427

14241428
# Specify requirements inline
14251429
dependency-versions = { packages = ["auditwheel==6.2.0"] }

test/test_dependency_versions.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import platform
44
import re
5+
import shlex
56
import textwrap
67
from pathlib import Path
78

@@ -96,7 +97,8 @@ def test_pinned_versions(tmp_path, python_version, build_frontend_env_nouv):
9697
assert set(actual_wheels) == set(expected_wheels)
9798

9899

99-
def test_dependency_constraints_file(tmp_path, build_frontend_env_nouv):
100+
@pytest.mark.parametrize("method", ["inline", "file"])
101+
def test_dependency_constraints(method, tmp_path, build_frontend_env_nouv):
100102
if utils.platform == "linux":
101103
pytest.skip("linux doesn't pin individual tool versions, it pins manylinux images instead")
102104

@@ -108,15 +110,24 @@ def test_dependency_constraints_file(tmp_path, build_frontend_env_nouv):
108110
"delocate": "0.10.3",
109111
}
110112

111-
constraints_file = tmp_path / "constraints file.txt"
112-
constraints_file.write_text(
113-
textwrap.dedent(
114-
"""
115-
pip=={pip}
116-
delocate=={delocate}
117-
""".format(**tool_versions)
113+
if method == "file":
114+
constraints_file = tmp_path / "constraints file.txt"
115+
constraints_file.write_text(
116+
textwrap.dedent(
117+
"""
118+
pip=={pip}
119+
delocate=={delocate}
120+
""".format(**tool_versions)
121+
)
118122
)
119-
)
123+
dependency_version_option = shlex.quote(str(constraints_file))
124+
elif method == "inline":
125+
dependency_version_option = "packages: " + " ".join(
126+
f"{k}=={v}" for k, v in tool_versions.items()
127+
)
128+
else:
129+
msg = f"Unknown method: {method}"
130+
raise ValueError(msg)
120131

121132
build_environment = {}
122133

@@ -131,7 +142,7 @@ def test_dependency_constraints_file(tmp_path, build_frontend_env_nouv):
131142
project_dir,
132143
add_env={
133144
"CIBW_ENVIRONMENT": cibw_environment_option,
134-
"CIBW_DEPENDENCY_VERSIONS": str(constraints_file),
145+
"CIBW_DEPENDENCY_VERSIONS": dependency_version_option,
135146
"CIBW_SKIP": "cp36-*",
136147
**build_frontend_env_nouv,
137148
},

unit_test/dependency_constraints_test.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,32 @@
55
from cibuildwheel.util.packaging import DependencyConstraints
66

77

8-
def test_defaults():
8+
def test_defaults(tmp_path: Path) -> None:
99
dependency_constraints = DependencyConstraints.with_defaults()
1010

1111
project_root = Path(__file__).parents[1]
1212
resources_dir = project_root / "cibuildwheel" / "resources"
1313

14+
assert dependency_constraints.base_file_path
1415
assert dependency_constraints.base_file_path.samefile(resources_dir / "constraints.txt")
15-
assert dependency_constraints.get_for_python_version("3.99").samefile(
16+
assert dependency_constraints.get_for_python_version(version="3.99", tmp_dir=tmp_path).samefile(
1617
resources_dir / "constraints.txt"
1718
)
18-
assert dependency_constraints.get_for_python_version("3.9").samefile(
19+
assert dependency_constraints.get_for_python_version(version="3.9", tmp_dir=tmp_path).samefile(
1920
resources_dir / "constraints-python39.txt"
2021
)
21-
assert dependency_constraints.get_for_python_version("3.6").samefile(
22+
assert dependency_constraints.get_for_python_version(version="3.6", tmp_dir=tmp_path).samefile(
2223
resources_dir / "constraints-python36.txt"
2324
)
25+
26+
27+
def test_inline_packages(tmp_path: Path) -> None:
28+
dependency_constraints = DependencyConstraints(
29+
base_file_path=None,
30+
packages=["foo==1.2.3", "bar==4.5.6"],
31+
)
32+
33+
constraint_file = dependency_constraints.get_for_python_version(version="x.x", tmp_dir=tmp_path)
34+
constraints_file_contents = constraint_file.read_text()
35+
36+
assert constraints_file_contents == "foo==1.2.3\nbar==4.5.6"

0 commit comments

Comments
 (0)