Skip to content

Commit aac361c

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 aac361c

File tree

2 files changed

+37
-8
lines changed

2 files changed

+37
-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: 30 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,47 @@ def find_why(
215214
max_depth: int,
216215
depth: int,
217216
req_type: list[RequirementType],
218-
):
217+
seen: set[str] = set(),
218+
) -> None:
219+
if node.key in seen:
220+
print(f"{' ' * depth} * {node.key} has a cycle")
221+
return
222+
223+
# Print the name of the package we are asking about. We do this here because
224+
# we might be invoked for multiple packages and we want the format to be
225+
# consistent.
226+
if depth == 0:
227+
print(f"\n{node.key}")
228+
229+
seen = set([node.key]).union(seen)
219230
all_skipped = True
220231
is_toplevel = False
221232
for parent in node.parents:
233+
# Show the toplevel dependencies regardless of the req_type because they
234+
# are the ones that are actually installed and may influence other
235+
# dependencies.
222236
if parent.destination_node.key == ROOT:
223237
is_toplevel = True
224-
print(f" * {node.key} is a toplevel dependency")
238+
print(
239+
f"{' ' * depth} * {node.key} is a toplevel dependency with req {parent.req}"
240+
)
225241
continue
242+
# Skip dependencies that don't match the req_type.
226243
if req_type and parent.req_type not in req_type:
227244
continue
228245
all_skipped = False
229246
print(
230-
f"{' ' * depth} * is an {parent.req_type} dependency of {parent.destination_node.key} with req {parent.req}"
247+
f"{' ' * depth} * {node.key} is an {parent.req_type} dependency of {parent.destination_node.key} with req {parent.req}"
231248
)
232249
if max_depth and (max_depth == -1 or depth <= max_depth):
233-
find_why(graph, parent.destination_node, max_depth, depth + 1, [])
250+
find_why(
251+
graph=graph,
252+
node=parent.destination_node,
253+
max_depth=max_depth,
254+
depth=depth + 1,
255+
req_type=req_type,
256+
seen=seen,
257+
)
234258

235259
if all_skipped and not is_toplevel:
236260
print(

0 commit comments

Comments
 (0)