Skip to content

feat(graph): add graph check command for pre-rebuild conflict detection - #999

Open
stmcgovern wants to merge 4 commits into
python-wheel-build:mainfrom
stmcgovern:graph-validate-v1
Open

stmcgovern wants to merge 4 commits into
python-wheel-build:mainfrom
stmcgovern:graph-validate-v1

Conversation

@stmcgovern

@stmcgovern stmcgovern commented Mar 31, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request Description

What

Add a pre-rebuild check that answers two questions in seconds:

  1. Is this graph structurally sound? (well-formed, acyclic)
  2. How many wheels are extra? (collapsible vs required)

The conflict classification uses the same specifier.filter logic as show_explain_duplicates and write_constraints_file. It is conservative: collapsible here guarantees write_constraints_file succeeds.

Self-loops (e.g. safetensors[numpy]) are reported as warnings, not failures — they are common in extras and harmless for install deps. Real cycles (e.g. haystack-ai ↔ haystack-experimental) cause failure.

Supports --json for CI integration and --constraints for pin output.

Why

Closes: #998

@stmcgovern
stmcgovern requested a review from a team as a code owner March 31, 2026 15:04
@coderabbitai

coderabbitai Bot commented Mar 31, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@stmcgovern, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5ea8d079-c723-4b09-bb12-1a01b7b6c06b

📥 Commits

Reviewing files that changed from the base of the PR and between 59d2482 and bf193be.

📒 Files selected for processing (1)
  • src/fromager/commands/graph.py
📝 Walkthrough

Walkthrough

This change adds a new fromager graph check subcommand that reads graph.json, checks for dangling edges and cycles in the raw graph data, and classifies multi-version packages as collapsible or required after building a DependencyGraph. The command supports human-readable, JSON, and constraints output, and exits with status 1 when dangling edges, cycles, or required conflicts are present. Tests cover the validation helpers and the CLI behavior across success, failure, and edge cases.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: adding a graph check command for pre-rebuild conflict detection.
Description check ✅ Passed The description is directly about the new pre-rebuild graph check and its output modes.
Linked Issues check ✅ Passed The PR implements the linked issue's graph check, structural validation, cycle handling, conflict classification, and exit codes.
Out of Scope Changes check ✅ Passed The changes shown are focused on the requested graph check command and its tests.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mergify mergify Bot added the ci label Mar 31, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/fromager/commands/graph.py (2)

1075-1100: Consider making --json and --constraints mutually exclusive.

If both flags are specified, --constraints silently takes precedence. This could surprise users.

Option: Add a click mutex constraint
`@click.option`(
    "--json",
    "as_json",
    is_flag=True,
    default=False,
    help="Output results as JSON.",
)
`@click.option`(
    "--constraints",
    "as_constraints",
    is_flag=True,
    default=False,
    help="Output collapsible pins in constraints.txt format.",
)

You could add validation at the start of check():

if as_json and as_constraints:
    raise click.UsageError("--json and --constraints are mutually exclusive")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/commands/graph.py` around lines 1075 - 1100, The check() command
accepts both --json (as_json) and --constraints (as_constraints) but currently
lets --constraints silently win; add an explicit mutual-exclusion check at the
start of the check() function to detect if both as_json and as_constraints are
True and raise a click.UsageError (with a clear message like "--json and
--constraints are mutually exclusive"). This change should be implemented inside
the check() function body that receives wkctx, graph_file, as_json,
as_constraints so any callers get an immediate, user-facing error instead of
surprising behavior.

1113-1129: Avoid reading the graph file twice.

The graph JSON is loaded at line 1115, then re-read at line 1127 via from_file. Use from_dict(graph_dict) instead.

Proposed fix
     if not wf_issues:
-        graph = DependencyGraph.from_file(graph_file)
+        graph = DependencyGraph.from_dict(graph_dict)
         entries = _classify_conflicts(graph)
         n_nodes = len(graph) - 1  # exclude root
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fromager/commands/graph.py` around lines 1113 - 1129, The code currently
loads graph_file into graph_dict and later re-reads the file via
DependencyGraph.from_file; replace that second read with
DependencyGraph.from_dict(graph_dict) to avoid double I/O: after verifying
wf_issues and acyclicity (using _check_well_formed and _check_acyclicity), call
DependencyGraph.from_dict(graph_dict) (not from_file) to build the graph used by
_classify_conflicts and to compute n_nodes, keeping the existing logic for
entries = _classify_conflicts(graph) and n_nodes = len(graph) - 1.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/fromager/commands/graph.py`:
- Around line 1075-1100: The check() command accepts both --json (as_json) and
--constraints (as_constraints) but currently lets --constraints silently win;
add an explicit mutual-exclusion check at the start of the check() function to
detect if both as_json and as_constraints are True and raise a click.UsageError
(with a clear message like "--json and --constraints are mutually exclusive").
This change should be implemented inside the check() function body that receives
wkctx, graph_file, as_json, as_constraints so any callers get an immediate,
user-facing error instead of surprising behavior.
- Around line 1113-1129: The code currently loads graph_file into graph_dict and
later re-reads the file via DependencyGraph.from_file; replace that second read
with DependencyGraph.from_dict(graph_dict) to avoid double I/O: after verifying
wf_issues and acyclicity (using _check_well_formed and _check_acyclicity), call
DependencyGraph.from_dict(graph_dict) (not from_file) to build the graph used by
_classify_conflicts and to compute n_nodes, keeping the existing logic for
entries = _classify_conflicts(graph) and n_nodes = len(graph) - 1.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 59054cbc-aa40-420f-b03f-7f71ab591faa

📥 Commits

Reviewing files that changed from the base of the PR and between aec9c9c and f5396e0.

📒 Files selected for processing (2)
  • src/fromager/commands/graph.py
  • tests/test_graph_validate.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_graph_check.py (1)

347-360: Consider adding assertion for build_efficiency field.

The JSON output includes build_efficiency with total_wheels and extra_builds per the implementation, but no test verifies this field. Minor gap.

assert "build_efficiency" in data
assert "total_wheels" in data["build_efficiency"]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_graph_check.py` around lines 347 - 360, The test_clean_graph_json
test is missing assertions for the build_efficiency section of the output;
update the test (in test_clean_graph_json) to assert that "build_efficiency"
exists in the parsed data dict and that it contains the expected keys (e.g.,
"total_wheels" and "extra_builds") and optionally that those values are
present/not-None (use the local variable data to check keys and simple
truthiness or type). Ensure assertions reference
data["build_efficiency"]["total_wheels"] and
data["build_efficiency"]["extra_builds"] so the test will fail if those fields
are absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/test_graph_check.py`:
- Around line 347-360: The test_clean_graph_json test is missing assertions for
the build_efficiency section of the output; update the test (in
test_clean_graph_json) to assert that "build_efficiency" exists in the parsed
data dict and that it contains the expected keys (e.g., "total_wheels" and
"extra_builds") and optionally that those values are present/not-None (use the
local variable data to check keys and simple truthiness or type). Ensure
assertions reference data["build_efficiency"]["total_wheels"] and
data["build_efficiency"]["extra_builds"] so the test will fail if those fields
are absent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 923cb1b7-9918-481e-aa54-06d541398075

📥 Commits

Reviewing files that changed from the base of the PR and between 926e0f6 and 8679050.

📒 Files selected for processing (1)
  • tests/test_graph_check.py

…tion

Add a pre-rebuild check that answers two questions in seconds:

1. Is this graph structurally sound? (well-formed, acyclic)
2. How many wheels are extra? (collapsible vs required)

The conflict classification uses the same specifier.filter logic as
show_explain_duplicates and write_constraints_file. It is conservative:
collapsible here guarantees write_constraints_file succeeds.

Self-loops (e.g. safetensors[numpy]) are reported as warnings, not
failures — they are common in extras and harmless for install deps.
Real cycles (e.g. haystack-ai ↔ haystack-experimental) cause failure.

Supports --json for CI integration and --constraints for pin output.

Closes: python-wheel-build#998
…e file read

Address review feedback: raise UsageError when both output flags are
passed, and use from_dict(graph_dict) instead of re-reading the file.
Align test filename with the command name per CONTRIBUTING.md convention.
@stmcgovern
stmcgovern force-pushed the graph-validate-v1 branch from 8679050 to 59d2482 Compare July 2, 2026 15:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/fromager/commands/graph.py (1)

1086-1114: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Recursive DFS may hit Python's recursion limit on deep dependency chains.

dfs() recurses per edge traversed; depth equals the longest dependency chain. Fromager targets deep AI/ML dependency trees, so a sufficiently long chain could raise RecursionError on what's meant to be a fast, reliable pre-rebuild gate. Consider converting to an explicit stack-based DFS.

♻️ Iterative alternative (sketch)
def dfs(start: str) -> None:
    stack = [(start, iter(graph_dict.get(start, {}).get("edges", [])))]
    color[start] = in_stack
    path.append(start)
    while stack:
        key, edges = stack[-1]
        advanced = False
        for edge in edges:
            tgt = edge["key"]
            if tgt == key:
                continue
            if tgt not in color:
                continue
            if color[tgt] == in_stack:
                cycle_start = path.index(tgt)
                cycles.append(f"CYCLE: {' -> '.join(path[cycle_start:] + [tgt])}")
            elif color[tgt] == unvisited:
                color[tgt] = in_stack
                path.append(tgt)
                stack.append((tgt, iter(graph_dict.get(tgt, {}).get("edges", []))))
                advanced = True
                break
        if not advanced:
            path.pop()
            color[key] = done
            stack.pop()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/fromager/commands/graph.py` around lines 1086 - 1114, The cycle-detection
DFS in the graph-building logic uses recursion and can overflow on long
dependency chains. Replace the recursive dfs() used in the cycle detection block
with an explicit stack-based traversal, preserving the existing
color/path/cycles behavior while avoiding RecursionError; update the
implementation around dfs, path, and the graph_dict edge walk accordingly.
tests/test_graph_check.py (1)

332-575: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No test for missing/invalid graph file.

The CLI test class covers structural and conflict scenarios well, but nothing exercises graph check <nonexistent-file> or a malformed-JSON file — the path where open()/json.load() currently produce a raw traceback (see companion comment in src/fromager/commands/graph.py). Once that's fixed with ClickPath(exists=True)/JSON error handling, a test asserting a clean click.UsageError/ClickException would lock in the behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_graph_check.py` around lines 332 - 575, Add coverage in
TestCheckCLI for invalid input paths: exercise graph check with a nonexistent
file and with a malformed JSON file. The current tests cover only valid graphs,
so add cases that invoke fromager with a bad path or bad JSON and assert a clean
ClickException/UsageError instead of a traceback. Use the existing helpers
_write_graph, cli_runner.invoke, and the graph check command invocation to keep
the new tests aligned with the other TestCheckCLI scenarios.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/fromager/commands/graph.py`:
- Around line 1344-1372: The check command’s graph-file argument currently
bypasses the same missing-file validation used elsewhere, so a bad path falls
through to open(graph_file) and json.load with a traceback. Update the check CLI
in graph.py to use the existing Click path validation pattern used by
find_best_fit, ideally clickext.ClickPath(exists=True), so missing files produce
a clean Click error before parsing. Keep the change localized to the check
function and its graph-file argument definition.

---

Nitpick comments:
In `@src/fromager/commands/graph.py`:
- Around line 1086-1114: The cycle-detection DFS in the graph-building logic
uses recursion and can overflow on long dependency chains. Replace the recursive
dfs() used in the cycle detection block with an explicit stack-based traversal,
preserving the existing color/path/cycles behavior while avoiding
RecursionError; update the implementation around dfs, path, and the graph_dict
edge walk accordingly.

In `@tests/test_graph_check.py`:
- Around line 332-575: Add coverage in TestCheckCLI for invalid input paths:
exercise graph check with a nonexistent file and with a malformed JSON file. The
current tests cover only valid graphs, so add cases that invoke fromager with a
bad path or bad JSON and assert a clean ClickException/UsageError instead of a
traceback. Use the existing helpers _write_graph, cli_runner.invoke, and the
graph check command invocation to keep the new tests aligned with the other
TestCheckCLI scenarios.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d5ac1689-4344-48eb-b50a-0ae6316b5a07

📥 Commits

Reviewing files that changed from the base of the PR and between 8679050 and 59d2482.

📒 Files selected for processing (2)
  • src/fromager/commands/graph.py
  • tests/test_graph_check.py

Comment thread src/fromager/commands/graph.py Outdated
Use `clickext.ClickPath(exists=True)` for the graph-file argument,
matching the pattern used by `find-best-fit` in the same file. Wrap
`json.load` with a `JSONDecodeError` handler for malformed input.

Addresses CodeRabbit review feedback on PR python-wheel-build#999.

Signed-off-by: Sean McGovern <smcgover@redhat.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RFE] Add graph check command for pre-rebuild conflict detection

1 participant