Skip to content

Commit bf50cab

Browse files
authored
Merge pull request #816 from dhellmann/save-constraints-to-graph
feat: add constraints to graph files
2 parents ca87a27 + 871c59b commit bf50cab

File tree

7 files changed

+221
-5
lines changed

7 files changed

+221
-5
lines changed

src/fromager/bootstrapper.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,9 @@ def _add_to_graph(
939939

940940
_, parent_req, parent_version = self.why[-1] if self.why else (None, None, None)
941941
pbi = self.ctx.package_build_info(req)
942+
# Get the constraint rule if any
943+
constraint_req = self.ctx.constraints.get_constraint(req.name)
944+
constraint = str(constraint_req) if constraint_req else ""
942945
# Update the dependency graph after we determine that this requirement is
943946
# useful but before we determine if it is redundant so that we capture all
944947
# edges to use for building a valid constraints file.
@@ -950,6 +953,7 @@ def _add_to_graph(
950953
req_version=req_version,
951954
download_url=download_url,
952955
pre_built=pbi.pre_built,
956+
constraint=constraint,
953957
)
954958
self.ctx.write_to_graph_to_file()
955959

src/fromager/commands/bootstrap.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ def bootstrap(
169169
req_type=RequirementType.TOP_LEVEL,
170170
)
171171
logger.info("%s resolves to %s", req, version)
172+
# Get the constraint rule if any
173+
constraint_req = wkctx.constraints.get_constraint(req.name)
174+
constraint = str(constraint_req) if constraint_req else ""
172175
wkctx.dependency_graph.add_dependency(
173176
parent_name=None,
174177
parent_version=None,
@@ -177,6 +180,7 @@ def bootstrap(
177180
req_version=version,
178181
download_url=source_url,
179182
pre_built=pbi.pre_built,
183+
constraint=constraint,
180184
)
181185
requirement_ctxvar.reset(token)
182186

src/fromager/commands/graph.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,11 @@ def show_explain_duplicates(graph: DependencyGraph) -> None:
237237
usable_versions: dict[str, list[str]] = {}
238238
user_counter: int = 0
239239

240-
print(f"\n{dep_name}")
240+
# Get the constraint from the first node (all versions have the same constraint)
241+
constraint_info = (
242+
f" (constraint: {nodes[0].constraint})" if nodes[0].constraint else ""
243+
)
244+
print(f"\n{dep_name}{constraint_info}")
241245
for node in sorted(nodes, key=lambda x: x.version):
242246
print(f" {node.version}")
243247

@@ -327,7 +331,8 @@ def find_why(
327331
# we might be invoked for multiple packages and we want the format to be
328332
# consistent.
329333
if depth == 0:
330-
print(f"\n{node.key}")
334+
constraint_info = f" (constraint: {node.constraint})" if node.constraint else ""
335+
print(f"\n{node.key}{constraint_info}")
331336

332337
seen = set([node.key]).union(seen)
333338
all_skipped = True
@@ -338,16 +343,25 @@ def find_why(
338343
# dependencies.
339344
if parent.destination_node.key == ROOT:
340345
is_toplevel = True
346+
# Show constraint for top-level dependencies
347+
constraint_info = (
348+
f" (constraint: {node.constraint})" if node.constraint else ""
349+
)
341350
print(
342-
f"{' ' * depth} * {node.key} is a toplevel dependency with req {parent.req}"
351+
f"{' ' * depth} * {node.key}{constraint_info} is a toplevel dependency with req {parent.req}"
343352
)
344353
continue
345354
# Skip dependencies that don't match the req_type.
346355
if req_type and parent.req_type not in req_type:
347356
continue
348357
all_skipped = False
358+
parent_constraint = (
359+
f" (constraint: {parent.destination_node.constraint})"
360+
if parent.destination_node.constraint
361+
else ""
362+
)
349363
print(
350-
f"{' ' * depth} * {node.key} is an {parent.req_type} dependency of {parent.destination_node.key} with req {parent.req}"
364+
f"{' ' * depth} * {node.key} is an {parent.req_type} dependency of {parent.destination_node.key}{parent_constraint} with req {parent.req}"
351365
)
352366
if max_depth and (max_depth == -1 or depth <= max_depth):
353367
find_why(

src/fromager/dependency_graph.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class DependencyNodeDict(typing.TypedDict):
2929
canonicalized_name: str
3030
version: str
3131
pre_built: bool
32+
constraint: str
3233
edges: list[DependencyEdgeDict]
3334

3435

@@ -38,6 +39,7 @@ class DependencyNode:
3839
version: Version
3940
download_url: str = dataclasses.field(default="", compare=False)
4041
pre_built: bool = dataclasses.field(default=False, compare=False)
42+
constraint: str = dataclasses.field(default="", compare=False)
4143
# additional fields
4244
key: str = dataclasses.field(init=False, compare=False, repr=False)
4345
parents: list[DependencyEdge] = dataclasses.field(
@@ -85,6 +87,7 @@ def to_dict(self) -> DependencyNodeDict:
8587
"pre_built": self.pre_built,
8688
"version": str(self.version),
8789
"canonicalized_name": str(self.canonicalized_name),
90+
"constraint": self.constraint,
8891
"edges": [edge.to_dict() for edge in self.children],
8992
}
9093

@@ -175,6 +178,7 @@ def from_dict(
175178
req_version=Version(destination_node_dict["version"]),
176179
download_url=destination_node_dict["download_url"],
177180
pre_built=destination_node_dict["pre_built"],
181+
constraint=destination_node_dict.get("constraint", ""),
178182
)
179183
stack.append(edge_dict["key"])
180184
visited.add(curr_key)
@@ -207,12 +211,14 @@ def _add_node(
207211
version: Version,
208212
download_url: str,
209213
pre_built: bool,
214+
constraint: str,
210215
):
211216
new_node = DependencyNode(
212217
canonicalized_name=req_name,
213218
version=version,
214219
download_url=download_url,
215220
pre_built=pre_built,
221+
constraint=constraint,
216222
)
217223
# check if a node with that key already exists. if it does then use that
218224
node = self.nodes.get(new_node.key, new_node)
@@ -229,6 +235,7 @@ def add_dependency(
229235
req_version: Version,
230236
download_url: str = "",
231237
pre_built: bool = False,
238+
constraint: str = "",
232239
) -> None:
233240
logger.debug(
234241
"recording %s dependency %s%s -> %s==%s",
@@ -244,6 +251,7 @@ def add_dependency(
244251
version=req_version,
245252
download_url=download_url,
246253
pre_built=pre_built,
254+
constraint=constraint,
247255
)
248256

249257
parent_key = ROOT if parent_name is None else f"{parent_name}=={parent_version}"

tests/test_dependency_graph.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def test_dependencynode_dataclass():
4848
assert a.key == "a==1.0"
4949
assert (
5050
repr(a)
51-
== "DependencyNode(canonicalized_name='a', version=<Version('1.0')>, download_url='', pre_built=False)"
51+
== "DependencyNode(canonicalized_name='a', version=<Version('1.0')>, download_url='', pre_built=False, constraint='')"
5252
)
5353
with pytest.raises(dataclasses.FrozenInstanceError):
5454
a.version = Version("2.0")

tests/test_graph.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
"pre_built": False,
1212
"version": "0",
1313
"canonicalized_name": "",
14+
"constraint": "",
1415
"edges": [{"key": "a==2.0", "req_type": "install", "req": "a==2.0"}],
1516
},
1617
"a==2.0": {
1718
"download_url": "url",
1819
"pre_built": False,
1920
"version": "2.0",
2021
"canonicalized_name": "a",
22+
"constraint": "",
2123
"edges": [
2224
{"key": "b==3.0", "req_type": "build-system", "req": "b==3.0"},
2325
{"key": "c==4.0", "req_type": "build-backend", "req": "c==4.0"},
@@ -28,6 +30,7 @@
2830
"pre_built": False,
2931
"version": "3.0",
3032
"canonicalized_name": "b",
33+
"constraint": "",
3134
"edges": [
3235
{"key": "c==4.0", "req_type": "build-sdist", "req": "c<=4.0"},
3336
],
@@ -37,6 +40,7 @@
3740
"pre_built": False,
3841
"version": "4.0",
3942
"canonicalized_name": "c",
43+
"constraint": "",
4044
"edges": [],
4145
},
4246
}

tests/test_graph_commands.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Test graph command functions that display constraint information."""
2+
3+
from packaging.requirements import Requirement
4+
from packaging.utils import canonicalize_name
5+
from packaging.version import Version
6+
7+
from fromager import dependency_graph
8+
from fromager.commands.graph import find_why, show_explain_duplicates
9+
from fromager.requirements_file import RequirementType
10+
11+
12+
def test_show_explain_duplicates_with_constraints(capsys):
13+
"""Test that explain_duplicates shows constraint information."""
14+
# Create a graph with duplicate dependencies that have constraints
15+
graph = dependency_graph.DependencyGraph()
16+
17+
# Add top-level package
18+
graph.add_dependency(
19+
parent_name=None,
20+
parent_version=None,
21+
req_type=RequirementType.TOP_LEVEL,
22+
req=Requirement("package-a"),
23+
req_version=Version("1.0.0"),
24+
download_url="https://example.com/package-a-1.0.0.tar.gz",
25+
)
26+
27+
# Add package-b version 1.0.0 as dependency of package-a with constraint
28+
graph.add_dependency(
29+
parent_name=canonicalize_name("package-a"),
30+
parent_version=Version("1.0.0"),
31+
req_type=RequirementType.INSTALL,
32+
req=Requirement("package-b>=1.0"),
33+
req_version=Version("1.0.0"),
34+
download_url="https://example.com/package-b-1.0.0.tar.gz",
35+
constraint="package-b>=1.0,<2.0",
36+
)
37+
38+
# Add another top-level package
39+
graph.add_dependency(
40+
parent_name=None,
41+
parent_version=None,
42+
req_type=RequirementType.TOP_LEVEL,
43+
req=Requirement("package-c"),
44+
req_version=Version("1.0.0"),
45+
download_url="https://example.com/package-c-1.0.0.tar.gz",
46+
)
47+
48+
# Add package-b version 2.0.0 as dependency of package-c without constraint
49+
graph.add_dependency(
50+
parent_name=canonicalize_name("package-c"),
51+
parent_version=Version("1.0.0"),
52+
req_type=RequirementType.INSTALL,
53+
req=Requirement("package-b>=2.0"),
54+
req_version=Version("2.0.0"),
55+
download_url="https://example.com/package-b-2.0.0.tar.gz",
56+
constraint="",
57+
)
58+
59+
# Run the command
60+
show_explain_duplicates(graph)
61+
62+
# Capture output
63+
captured = capsys.readouterr()
64+
65+
# Verify constraint is shown at the package name level, not per-version
66+
assert "package-b (constraint: package-b>=1.0,<2.0)" in captured.out
67+
# Versions should be shown without constraint info
68+
assert " 1.0.0\n" in captured.out
69+
assert " 2.0.0\n" in captured.out
70+
# Version lines should not have constraint info
71+
assert "1.0.0 (constraint:" not in captured.out
72+
assert "2.0.0 (constraint:" not in captured.out
73+
74+
75+
def test_find_why_with_constraints(capsys):
76+
"""Test that why command shows constraint information."""
77+
# Create a graph with constraints
78+
graph = dependency_graph.DependencyGraph()
79+
80+
# Add top-level package with constraint
81+
graph.add_dependency(
82+
parent_name=None,
83+
parent_version=None,
84+
req_type=RequirementType.TOP_LEVEL,
85+
req=Requirement("parent-pkg"),
86+
req_version=Version("1.0.0"),
87+
download_url="https://example.com/parent-pkg-1.0.0.tar.gz",
88+
constraint="parent-pkg==1.0.0",
89+
)
90+
91+
# Add child dependency with its own constraint
92+
graph.add_dependency(
93+
parent_name=canonicalize_name("parent-pkg"),
94+
parent_version=Version("1.0.0"),
95+
req_type=RequirementType.INSTALL,
96+
req=Requirement("child-pkg>=1.0"),
97+
req_version=Version("1.5.0"),
98+
download_url="https://example.com/child-pkg-1.5.0.tar.gz",
99+
constraint="child-pkg>=1.0,<2.0",
100+
)
101+
102+
# Find why child-pkg is included
103+
child_node = graph.nodes["child-pkg==1.5.0"]
104+
find_why(graph, child_node, 1, 0, [])
105+
106+
# Capture output
107+
captured = capsys.readouterr()
108+
109+
# Verify constraint is shown for the child package at depth 0
110+
assert "child-pkg==1.5.0 (constraint: child-pkg>=1.0,<2.0)" in captured.out
111+
# Verify constraint is shown for the parent when showing the dependency relationship
112+
assert "(constraint: parent-pkg==1.0.0)" in captured.out
113+
114+
115+
def test_find_why_toplevel_with_constraint(capsys):
116+
"""Test that why command shows constraint for top-level dependencies."""
117+
# Create a graph with a top-level package that has a constraint
118+
graph = dependency_graph.DependencyGraph()
119+
120+
# Add top-level package with constraint
121+
graph.add_dependency(
122+
parent_name=None,
123+
parent_version=None,
124+
req_type=RequirementType.TOP_LEVEL,
125+
req=Requirement("toplevel-pkg"),
126+
req_version=Version("2.0.0"),
127+
download_url="https://example.com/toplevel-pkg-2.0.0.tar.gz",
128+
constraint="toplevel-pkg>=2.0,<3.0",
129+
)
130+
131+
# Find why toplevel-pkg is included
132+
node = graph.nodes["toplevel-pkg==2.0.0"]
133+
find_why(graph, node, 0, 0, [])
134+
135+
# Capture output
136+
captured = capsys.readouterr()
137+
138+
# Verify constraint is shown at depth 0
139+
assert "toplevel-pkg==2.0.0 (constraint: toplevel-pkg>=2.0,<3.0)" in captured.out
140+
# Verify constraint is shown when identifying it as a top-level dependency
141+
assert (
142+
"toplevel-pkg==2.0.0 (constraint: toplevel-pkg>=2.0,<3.0) is a toplevel dependency"
143+
in captured.out
144+
)
145+
146+
147+
def test_find_why_without_constraints(capsys):
148+
"""Test that why command works when no constraints are present."""
149+
# Create a graph without constraints
150+
graph = dependency_graph.DependencyGraph()
151+
152+
# Add top-level package without constraint
153+
graph.add_dependency(
154+
parent_name=None,
155+
parent_version=None,
156+
req_type=RequirementType.TOP_LEVEL,
157+
req=Requirement("simple-pkg"),
158+
req_version=Version("1.0.0"),
159+
download_url="https://example.com/simple-pkg-1.0.0.tar.gz",
160+
)
161+
162+
# Add child dependency without constraint
163+
graph.add_dependency(
164+
parent_name=canonicalize_name("simple-pkg"),
165+
parent_version=Version("1.0.0"),
166+
req_type=RequirementType.INSTALL,
167+
req=Requirement("simple-child"),
168+
req_version=Version("2.0.0"),
169+
download_url="https://example.com/simple-child-2.0.0.tar.gz",
170+
)
171+
172+
# Find why simple-child is included
173+
child_node = graph.nodes["simple-child==2.0.0"]
174+
find_why(graph, child_node, 1, 0, [])
175+
176+
# Capture output
177+
captured = capsys.readouterr()
178+
179+
# Verify no constraint info is shown
180+
assert "(constraint:" not in captured.out
181+
assert "simple-child==2.0.0" in captured.out
182+
assert "simple-pkg==1.0.0" in captured.out

0 commit comments

Comments
 (0)