Skip to content

Commit dd7009b

Browse files
smoparthclaude
andcommitted
feat(cli): support multiple constraints files via repeated -c flag
Add `multiple=True` to the global `-c`/`--constraints-file` Click option so users can pass multiple constraint sources (local files or remote URIs). WorkContext now loads all provided files in sequence, merging constraints via the combining logic added in #1125. The `pip_constraint_args` property writes a single merged constraints file for uv instead of passing the raw input file. Closes: #1096 Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Shanmukh Pawan <smoparth@redhat.com>
1 parent b30aae7 commit dd7009b

4 files changed

Lines changed: 72 additions & 22 deletions

File tree

src/fromager/__main__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@
134134
"-c",
135135
"--constraints-file",
136136
type=str,
137-
help="location of the constraints file",
137+
multiple=True,
138+
help="location of constraint file(s), may be repeated",
138139
)
139140
@click.option(
140141
"--cleanup/--no-cleanup",
@@ -177,7 +178,7 @@ def main(
177178
patches_dir: pathlib.Path,
178179
settings_file: pathlib.Path,
179180
settings_dir: pathlib.Path,
180-
constraints_file: str,
181+
constraints_file: tuple[str, ...],
181182
cleanup: bool,
182183
variant: str,
183184
jobs: int | None,
@@ -247,7 +248,7 @@ def main(
247248
logger.info(f"variant: {variant}")
248249
logger.info(f"patches dir: {patches_dir}")
249250
logger.info(f"maximum concurrent jobs: {jobs}")
250-
logger.info(f"constraints file: {constraints_file}")
251+
logger.info(f"constraints file(s): {constraints_file}")
251252
logger.info(f"network isolation: {network_isolation}")
252253
if build_wheel_server_url:
253254
logger.info(f"external build wheel server: {build_wheel_server_url}")

src/fromager/context.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
dependency_graph,
2020
external_commands,
2121
packagesettings,
22-
request_session,
2322
)
2423

2524
if typing.TYPE_CHECKING:
@@ -36,7 +35,7 @@ class WorkContext:
3635
def __init__(
3736
self,
3837
active_settings: packagesettings.Settings | None,
39-
constraints_file: str | None,
38+
constraints_file: str | tuple[str, ...] | None,
4039
patches_dir: pathlib.Path,
4140
sdists_repo: pathlib.Path,
4241
wheels_repo: pathlib.Path,
@@ -59,13 +58,16 @@ def __init__(
5958
max_jobs=max_jobs,
6059
)
6160
self.settings = active_settings
62-
self.input_constraints_uri: str | None
6361
self.constraints = constraints.Constraints()
64-
if constraints_file is not None:
65-
self.input_constraints_uri = constraints_file
66-
self.constraints.load_constraints_file(constraints_file)
67-
else:
68-
self.input_constraints_uri = None
62+
self.input_constraints_uris: list[str] = []
63+
if constraints_file:
64+
if isinstance(constraints_file, str):
65+
files: tuple[str, ...] = (constraints_file,)
66+
else:
67+
files = constraints_file
68+
for cf in files:
69+
self.input_constraints_uris.append(cf)
70+
self.constraints.load_constraints_file(cf)
6971
self.sdists_repo = pathlib.Path(sdists_repo).resolve()
7072
self.sdists_downloads = self.sdists_repo / "downloads"
7173
self.sdists_builds = self.sdists_repo / "builds"
@@ -135,16 +137,17 @@ def pip_wheel_server_args(self) -> list[str]:
135137

136138
@property
137139
def pip_constraint_args(self) -> list[str]:
138-
if not self.input_constraints_uri:
140+
if not self.input_constraints_uris:
139141
return []
140142

141-
if self.input_constraints_uri.startswith(("https://", "http://", "file://")):
142-
path_to_constraints_file = self.work_dir / "input-constraints.txt"
143-
if not path_to_constraints_file.exists():
144-
response = request_session.session.get(self.input_constraints_uri)
145-
path_to_constraints_file.write_text(response.text)
146-
else:
147-
path_to_constraints_file = pathlib.Path(self.input_constraints_uri)
143+
path_to_constraints_file = self.work_dir / "input-constraints.txt"
144+
if not path_to_constraints_file.exists():
145+
lines: list[str] = []
146+
for constraint_name in self.constraints:
147+
req = self.constraints.get_constraint(constraint_name)
148+
if req is not None:
149+
lines.append(f"{req}\n")
150+
path_to_constraints_file.write_text("".join(lines))
148151

149152
path_to_constraints_file = path_to_constraints_file.absolute()
150153
return ["--constraint", os.fspath(path_to_constraints_file)]

tests/test_cli.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,34 @@ def test_output_dir_overridden_by_explicit_flags(
9696
assert not (out / "sdists-repo").exists()
9797

9898

99+
def test_multiple_constraints_files(
100+
tmp_path: pathlib.Path, cli_runner: CliRunner
101+
) -> None:
102+
"""Multiple -c flags are accepted and constraints are merged."""
103+
file1 = tmp_path / "base.txt"
104+
file1.write_text("numpy>=1.24\n")
105+
file2 = tmp_path / "extra.txt"
106+
file2.write_text("numpy<2.0\n")
107+
108+
out = tmp_path / "output"
109+
out.mkdir()
110+
111+
result = cli_runner.invoke(
112+
fromager,
113+
[
114+
"-O",
115+
str(out),
116+
"-c",
117+
str(file1),
118+
"-c",
119+
str(file2),
120+
"canonicalize",
121+
"some-package",
122+
],
123+
)
124+
assert result.exit_code == 0, result.output
125+
126+
99127
KNOWN_COMMANDS: set[str] = {
100128
"bootstrap",
101129
"bootstrap-parallel",

tests/test_context.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
def _make_context(
1212
tmp_path: pathlib.Path,
13-
constraints_file: str | None = None,
13+
constraints_file: str | tuple[str, ...] | None = None,
1414
wheel_server_url: str = "",
1515
cleanup: bool = True,
1616
) -> context.WorkContext:
@@ -43,16 +43,34 @@ def _all_setup_dirs(ctx: context.WorkContext) -> list[pathlib.Path]:
4343

4444
def test_pip_constraints_args(tmp_path: pathlib.Path) -> None:
4545
constraints_file = tmp_path / "constraints.txt"
46-
constraints_file.write_text("\n") # the file has to exist
46+
constraints_file.write_text("numpy>=1.24\n")
4747
ctx = _make_context(tmp_path, constraints_file=str(constraints_file))
4848
ctx.setup()
49-
assert ["--constraint", os.fspath(constraints_file)] == ctx.pip_constraint_args
49+
merged_path = ctx.work_dir / "input-constraints.txt"
50+
assert ["--constraint", os.fspath(merged_path)] == ctx.pip_constraint_args
51+
assert merged_path.exists()
52+
assert "numpy>=1.24" in merged_path.read_text()
5053

5154
ctx = _make_context(tmp_path)
5255
ctx.setup()
5356
assert [] == ctx.pip_constraint_args
5457

5558

59+
def test_pip_constraints_args_multiple_files(tmp_path: pathlib.Path) -> None:
60+
file1 = tmp_path / "base.txt"
61+
file1.write_text("numpy>=1.24\n")
62+
file2 = tmp_path / "security.txt"
63+
file2.write_text("numpy<2.0\nrequests>=2.28\n")
64+
65+
ctx = _make_context(tmp_path, constraints_file=(str(file1), str(file2)))
66+
ctx.setup()
67+
merged_path = ctx.work_dir / "input-constraints.txt"
68+
assert ["--constraint", os.fspath(merged_path)] == ctx.pip_constraint_args
69+
content = merged_path.read_text()
70+
assert "numpy" in content
71+
assert "requests" in content
72+
73+
5674
def test_setup_creates_directories(tmp_path: pathlib.Path) -> None:
5775
ctx = _make_context(tmp_path)
5876
ctx.setup()

0 commit comments

Comments
 (0)