Skip to content

feat: add protocol for override hooks#1126

Open
tiran wants to merge 1 commit into
python-wheel-build:mainfrom
tiran:override-protocol
Open

feat: add protocol for override hooks#1126
tiran wants to merge 1 commit into
python-wheel-build:mainfrom
tiran:override-protocol

Conversation

@tiran
Copy link
Copy Markdown
Collaborator

@tiran tiran commented May 7, 2026

Pull Request Description

What

OverrideHookProtocol documents the interface for per-package override hooks and provides runtime validation of hook signatures via check_signature(). The lint command now checks override hook signatures.

Why

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a typed Protocol-based framework for override hooks with runtime signature validation. The OverrideHookProtocol class defines standard hook methods (wheel/sdist building, source downloading, dependency resolution, etc.), each annotated with a default implementation location. Three classmethods enable discovery (list_hooks), default loading (get_default), and signature validation (check_signature). The lint command integrates this by validating each discovered extension's hook functions against their Protocol signatures, logging errors for mismatches. Tests cover the protocol's core methods and validate all default implementations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add protocol for override hooks' directly and clearly describes the main addition—a new Protocol for override hooks—which aligns with the primary changeset across overrides.py, lint.py, and tests.
Description check ✅ Passed The description explains what OverrideHookProtocol does and mentions the lint command integration, both of which are core aspects of the changeset, and notes CONTRIBUTING.md compliance.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@mergify mergify Bot added the ci label May 7, 2026
OverrideHookProtocol documents the interface for per-package override
hooks and provides runtime validation of hook signatures via
check_signature(). The lint command now checks override hook signatures.

Co-Authored-By: Claude <claude@anthropic.com>
Signed-off-by: Christian Heimes <cheimes@redhat.com>
@tiran tiran force-pushed the override-protocol branch from be670dd to fa5bc5c Compare May 7, 2026 15:32
@tiran tiran marked this pull request as ready for review May 8, 2026 07:58
@tiran tiran requested a review from a team as a code owner May 8, 2026 07:58
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

57-69: 💤 Low value

test_check_signature_matching duplicates coverage already provided by the parametrized test.

test_protocol_signature_matches_default already validates check_signature for every hook (including build_wheel) against its real default. This test adds value only if it's explicitly checking a hand-crafted override (not the default). Consider targeting an intentionally different-but-valid function to make the intent distinct, or remove it to reduce duplication.

🤖 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_overrides.py` around lines 57 - 69, test_check_signature_matching
currently duplicates coverage from test_protocol_signature_matches_default;
either remove this redundant test or change it to exercise an explicit override
function that differs from the real default but still matches the protocol.
Concretely, update test_check_signature_matching to define a custom override
(e.g., a purposely different-but-valid signature function for build_wheel with
distinct parameter names or default values) and call
overrides.OverrideHookProtocol.check_signature(your_custom_build_wheel) to
assert the protocol accepts a non-default override, or simply delete
test_check_signature_matching if no distinct behavior is needed.
🤖 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/overrides.py`:
- Around line 181-189: get_default currently lets a raw AttributeError bubble up
if the default function named by obj.fromager_default is missing; update
get_default (the classmethod named get_default and the use of
obj.fromager_default -> module_name, func_name) to catch AttributeError around
getattr(module, func_name) and raise a KeyError referencing the hook_name and
the missing func_name/module_name (include the original exception as the
__cause__ with "from") so callers get clear context about which hook/default is
broken.

In `@tests/test_overrides.py`:
- Around line 46-49: Remove the fragile hardcoded count check in
test_list_hooks: in the function test_list_hooks (which calls
overrides.OverrideHookProtocol.list_hooks) delete the assertion assert
len(hooks) == 13 and instead assert that hooks is non-empty (e.g., assert hooks)
and that there are no duplicates (e.g., assert len(set(hooks)) == len(hooks));
keep the isinstance(hooks, list) check and rely on the existing parametrized
test_protocol_signature_matches_default for per-hook behavior coverage.

---

Nitpick comments:
In `@tests/test_overrides.py`:
- Around line 57-69: test_check_signature_matching currently duplicates coverage
from test_protocol_signature_matches_default; either remove this redundant test
or change it to exercise an explicit override function that differs from the
real default but still matches the protocol. Concretely, update
test_check_signature_matching to define a custom override (e.g., a purposely
different-but-valid signature function for build_wheel with distinct parameter
names or default values) and call
overrides.OverrideHookProtocol.check_signature(your_custom_build_wheel) to
assert the protocol accepts a non-default override, or simply delete
test_check_signature_matching if no distinct behavior is needed.
🪄 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: 783ce5ce-b387-4c9a-b237-19475396a74b

📥 Commits

Reviewing files that changed from the base of the PR and between 5fd5d20 and fa5bc5c.

📒 Files selected for processing (3)
  • src/fromager/commands/lint.py
  • src/fromager/overrides.py
  • tests/test_overrides.py

Comment thread src/fromager/overrides.py
Comment on lines +181 to +189
@classmethod
def get_default(cls, hook_name: str) -> typing.Callable[..., typing.Any]:
"""Return the default function object for a hook name."""
obj = getattr(cls, hook_name, None)
if obj is None or not hasattr(obj, "fromager_default"):
raise KeyError(hook_name)
module_name, func_name = obj.fromager_default
module = importlib.import_module(module_name)
return typing.cast(typing.Callable[..., typing.Any], getattr(module, func_name))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

get_default silently propagates AttributeError with no context.

If func_name doesn't exist in the imported module (e.g., typo in _default_hook args), getattr(module, func_name) raises a bare AttributeError with no mention of which hook is broken.

🛡️ Proposed fix
     module = importlib.import_module(module_name)
-    return typing.cast(typing.Callable[..., typing.Any], getattr(module, func_name))
+    func = getattr(module, func_name, None)
+    if func is None:
+        raise AttributeError(
+            f"Hook {hook_name!r}: no attribute {func_name!r} in {module_name!r}"
+        ) from None
+    return typing.cast(typing.Callable[..., typing.Any], func)
🤖 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/overrides.py` around lines 181 - 189, get_default currently lets
a raw AttributeError bubble up if the default function named by
obj.fromager_default is missing; update get_default (the classmethod named
get_default and the use of obj.fromager_default -> module_name, func_name) to
catch AttributeError around getattr(module, func_name) and raise a KeyError
referencing the hook_name and the missing func_name/module_name (include the
original exception as the __cause__ with "from") so callers get clear context
about which hook/default is broken.

Comment thread tests/test_overrides.py
Comment on lines +46 to +49
def test_list_hooks() -> None:
hooks = overrides.OverrideHookProtocol.list_hooks()
assert isinstance(hooks, list)
assert len(hooks) == 13
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hardcoded hook count breaks silently when hooks are added or removed.

assert len(hooks) == 13 will fail on every hook addition/removal without communicating which hook was added or removed. The parametrized test_protocol_signature_matches_default already exercises every hook end-to-end; this test adds little value in its current form.

♻️ Proposed fix
 def test_list_hooks() -> None:
     hooks = overrides.OverrideHookProtocol.list_hooks()
     assert isinstance(hooks, list)
-    assert len(hooks) == 13
+    # Spot-check a stable subset; the parametrized test validates all hooks.
+    assert "build_wheel" in hooks
+    assert "download_source" in hooks
+    assert len(hooks) > 0
🤖 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_overrides.py` around lines 46 - 49, Remove the fragile hardcoded
count check in test_list_hooks: in the function test_list_hooks (which calls
overrides.OverrideHookProtocol.list_hooks) delete the assertion assert
len(hooks) == 13 and instead assert that hooks is non-empty (e.g., assert hooks)
and that there are no duplicates (e.g., assert len(set(hooks)) == len(hooks));
keep the isinstance(hooks, list) check and rely on the existing parametrized
test_protocol_signature_matches_default for per-hook behavior coverage.

Comment thread src/fromager/overrides.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Another way to do this would be to have fromager define a type so we could pass fewer individual arguments to hooks. For example, the build hooks both take the same arguments. We could define a type and pass 1 value to the hook. Then when we add fields, the hooks don't have to change if they don't need the value.

Maybe the methods that manipulate the source (download, prepare, etc.) could be standardized to take a single type, too?

How many different types would we need?

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.

2 participants