Skip to content

Commit 504cbf5

Browse files
committed
feat(graph): Improve why command output
The `graph why` command is a tool for developers to understand the dependency graph. This change improves its output to be more useful and clearer. The output now includes the requirement specifier from the parent package along with the parent package name. This makes it easier to understand not just which package is pulling in a dependency, but which version or range of versions it requires. The output for top-level dependencies specified on the command line or in a requirements file is also improved to clearly label them as such. Cycle detection is included to prevent infinite loops and make it clear where circular dependencies exist. Signed-off-by: Doug Hellmann <dhellmann@redhat.com>
1 parent 493ceb4 commit 504cbf5

File tree

2 files changed

+40
-8
lines changed

2 files changed

+40
-8
lines changed

src/fromager/commands/bootstrap.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,9 +374,14 @@ def write_constraints_file(
374374
output.write(f"{dep_name}=={node.version}\n")
375375

376376
for dep_name in conflicting_deps:
377-
logger.error("finding why %s was being used", dep_name)
378377
for node in graph.get_nodes_by_name(dep_name):
379-
find_why(graph, node, -1, 1, [])
378+
find_why(
379+
graph=graph,
380+
node=node,
381+
max_depth=-1,
382+
depth=0,
383+
req_type=[],
384+
)
380385

381386
return ret
382387

src/fromager/commands/graph.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,7 @@ def why(
205205
if version:
206206
package_nodes = [node for node in package_nodes if node.version in version]
207207
for node in package_nodes:
208-
print(f"\n{node.key}")
209-
find_why(graph, node, depth, 1, requirement_type)
208+
find_why(graph, node, depth, 0, requirement_type)
210209

211210

212211
def find_why(
@@ -215,22 +214,50 @@ def find_why(
215214
max_depth: int,
216215
depth: int,
217216
req_type: list[RequirementType],
218-
):
217+
seen: set[str] | None = None,
218+
) -> None:
219+
if seen is None:
220+
seen = set()
221+
222+
if node.key in seen:
223+
print(f"{' ' * depth} * {node.key} has a cycle")
224+
return
225+
226+
# Print the name of the package we are asking about. We do this here because
227+
# we might be invoked for multiple packages and we want the format to be
228+
# consistent.
229+
if depth == 0:
230+
print(f"\n{node.key}")
231+
232+
seen = set([node.key]).union(seen)
219233
all_skipped = True
220234
is_toplevel = False
221235
for parent in node.parents:
236+
# Show the toplevel dependencies regardless of the req_type because they
237+
# are the ones that are actually installed and may influence other
238+
# dependencies.
222239
if parent.destination_node.key == ROOT:
223240
is_toplevel = True
224-
print(f" * {node.key} is a toplevel dependency")
241+
print(
242+
f"{' ' * depth} * {node.key} is a toplevel dependency with req {parent.req}"
243+
)
225244
continue
245+
# Skip dependencies that don't match the req_type.
226246
if req_type and parent.req_type not in req_type:
227247
continue
228248
all_skipped = False
229249
print(
230-
f"{' ' * depth} * is an {parent.req_type} dependency of {parent.destination_node.key} with req {parent.req}"
250+
f"{' ' * depth} * {node.key} is an {parent.req_type} dependency of {parent.destination_node.key} with req {parent.req}"
231251
)
232252
if max_depth and (max_depth == -1 or depth <= max_depth):
233-
find_why(graph, parent.destination_node, max_depth, depth + 1, [])
253+
find_why(
254+
graph=graph,
255+
node=parent.destination_node,
256+
max_depth=max_depth,
257+
depth=depth + 1,
258+
req_type=req_type,
259+
seen=seen,
260+
)
234261

235262
if all_skipped and not is_toplevel:
236263
print(

0 commit comments

Comments
 (0)