Skip to content

Conversation

@jan-janssen
Copy link
Member

@jan-janssen jan-janssen commented Sep 14, 2025

Summary by CodeRabbit

  • New Features

    • Attributes and non-private operations now automatically forward to the underlying future for a simpler, more intuitive experience.
  • Breaking Changes

    • result() now returns only the selected item (or None) instead of the full underlying result, which may break callers expecting the previous shape.
    • Several explicit wrapper methods on the public surface have been removed.
  • Chores

    • Public API streamlined by relying on dynamic forwarding rather than redundant delegation.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 14, 2025

Walkthrough

Refactors FutureSelector in executorlib/standalone/select.py to proxy an inner future via dynamic attribute forwarding, removes explicit delegation methods, adjusts constructor order, and changes result() to return a selected item from the inner future’s result. split_future and get_item_from_future remain unchanged in interface and behavior.

Changes

Cohort / File(s) Summary of edits
FutureSelector proxy refactor
executorlib/standalone/select.py
Converted FutureSelector to a proxy wrapper around an inner future with __getattr__/__setattr__ forwarding; removed explicit delegation methods (cancel, cancelled, running, done, add_done_callback, exception, set_running_or_notify_cancel, set_result, set_exception); reordered initialization; modified result(timeout) to return inner_result[self._selector] when inner result is not None; split_future and get_item_from_future unchanged but now return the updated FutureSelector.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant FS as FutureSelector
  participant IF as InnerFuture

  Client->>FS: result(timeout)
  FS->>IF: result(timeout)
  IF-->>FS: inner_result
  alt inner_result is not None
    FS-->>Client: inner_result[selector]
  else
    FS-->>Client: None
  end

  Note over Client,FS: Attribute access/assignment forwarding
  Client->>FS: get/set non-private attribute
  FS->>IF: forward via __getattr__/__setattr__
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I nibble code like clover sprigs, so neat,
A future split, a proxy light on feet.
Methods hop away, attributes glide—
Selector picks the sweetest bite inside.
In burrows of async, I cheer and beam:
One inner carrot, sliced to dream. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title concisely and accurately describes the primary change: converting FutureSelector toward a more generic/proxy interface for concurrent.futures.Future; it is a single clear sentence and aligns with the changes in the diff (removal of explicit forwarders and addition of dynamic attribute forwarding).
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch clean_up_future_selector

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


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.

@codecov
Copy link

codecov bot commented Sep 14, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.16%. Comparing base (0edc7c3) to head (10c7f68).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #824      +/-   ##
==========================================
- Coverage   98.17%   98.16%   -0.02%     
==========================================
  Files          34       34              
  Lines        1698     1686      -12     
==========================================
- Hits         1667     1655      -12     
  Misses         31       31              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
executorlib/standalone/select.py (1)

23-28: Result should rely on wrapper state, not call inner.result() directly

Directly calling the inner future bypasses the wrapper’s timeout/callback semantics. With the chaining above, this should delegate to the parent implementation.

Apply this diff:

-    def result(self, timeout: Optional[float] = None) -> Any:
-        result = self._future.result(timeout=timeout)
-        if result is not None:
-            return result[self._selector]
-        else:
-            return None
+    def result(self, timeout: Optional[float] = None) -> Any:
+        return super().result(timeout=timeout)
🧹 Nitpick comments (1)
executorlib/standalone/select.py (1)

9-9: Initialize and chain correctly in init

Call super().init first and immediately attach the propagation callback; this avoids any transient inconsistent state. Covered by the diff in my other comment.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0edc7c3 and 03c3dd2.

📒 Files selected for processing (1)
  • executorlib/standalone/select.py (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (15)
  • GitHub Check: unittest_openmpi (ubuntu-latest, 3.13)
  • GitHub Check: unittest_old
  • GitHub Check: unittest_openmpi (ubuntu-22.04-arm, 3.13)
  • GitHub Check: unittest_openmpi (ubuntu-latest, 3.11)
  • GitHub Check: unittest_openmpi (macos-latest, 3.13)
  • GitHub Check: unittest_openmpi (ubuntu-24.04-arm, 3.13)
  • GitHub Check: unittest_openmpi (ubuntu-latest, 3.12)
  • GitHub Check: unittest_mpich (macos-latest, 3.13)
  • GitHub Check: unittest_flux_openmpi
  • GitHub Check: unittest_win
  • GitHub Check: unittest_flux_mpich
  • GitHub Check: unittest_slurm_mpich
  • GitHub Check: benchmark (ubuntu-latest, 3.13, .ci_support/environment-openmpi.yml)
  • GitHub Check: notebooks_integration
  • GitHub Check: benchmark (ubuntu-latest, 3.13, .ci_support/environment-mpich.yml)

Comment on lines 11 to 15
def __getattr__(self, attr: str):
if attr in ["_future", "_selector"]:
return super().__getattribute__(attr)
else:
return getattr(self._future, attr)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

getattr doesn’t delegate Future methods; cancel/done/exception act on the wrapper, not the inner future

Because these methods exist on the superclass, getattr is never invoked. The wrapper never transitions state; add_done_callback/done/cancel semantics are broken.

Apply this diff to chain inner→wrapper completion and rely on the wrapper’s own Future state (no generic getattr/setattr needed):

-from concurrent.futures import Future
+from concurrent.futures import Future, CancelledError
@@
-class FutureSelector(Future):
-    def __init__(self, future: Future, selector: int | str):
-        self._future = future
-        self._selector = selector
-        super().__init__()
-
-    def __getattr__(self, attr: str):
-        if attr in ["_future", "_selector"]:
-            return super().__getattribute__(attr)
-        else:
-            return getattr(self._future, attr)
+class FutureSelector(Future):
+    def __init__(self, future: Future, selector: int | str):
+        super().__init__()
+        self._future = future
+        self._selector = selector
+        # Propagate completion/cancel/exception from inner -> wrapper
+        def _propagate(src: Future):
+            if src.cancelled():
+                Future.cancel(self)  # mark wrapper as cancelled
+                return
+            try:
+                res = src.result()
+                val = res[self._selector] if res is not None else None
+                Future.set_result(self, val)
+            except BaseException as e:
+                Future.set_exception(self, e)
+        self._future.add_done_callback(_propagate)
+
+    # Propagate cancel from wrapper -> inner
+    def cancel(self) -> bool:
+        ok = self._future.cancel()
+        if ok:
+            Future.cancel(self)
+        return ok
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __getattr__(self, attr: str):
if attr in ["_future", "_selector"]:
return super().__getattribute__(attr)
else:
return getattr(self._future, attr)
from concurrent.futures import Future, CancelledError
class FutureSelector(Future):
def __init__(self, future: Future, selector: int | str):
super().__init__()
self._future = future
self._selector = selector
# Propagate completion/cancel/exception from inner -> wrapper
def _propagate(src: Future):
if src.cancelled():
Future.cancel(self) # mark wrapper as cancelled
return
try:
res = src.result()
val = res[self._selector] if res is not None else None
Future.set_result(self, val)
except BaseException as e:
Future.set_exception(self, e)
self._future.add_done_callback(_propagate)
# Propagate cancel from wrapper -> inner
def cancel(self) -> bool:
ok = self._future.cancel()
if ok:
Future.cancel(self)
return ok
🤖 Prompt for AI Agents
In executorlib/standalone/select.py around lines 11-15, the current __getattr__
delegates all attributes to the inner future which fails for methods that exist
on the Future superclass (cancel/done/exception/add_done_callback), so implement
explicit chaining: remove the generic __getattr__ delegation for Future methods
and instead register a done callback on the inner future that transfers
result/exception/cancellation to the wrapper via the wrapper's own
set_result/set_exception/cancel methods; override cancel to cancel the inner
future and set the wrapper cancelled state, and for
result/done/exception/add_done_callback rely on the wrapper's Future
implementation (no generic getattr/setattr) so wrapper state reflects inner
future completion.

Comment on lines +17 to +21
def __setattr__(self, name: str, value: Any):
if name in ["_future", "_selector"]:
super().__setattr__(name, value)
else:
setattr(self._future, name, value)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove setattr: it corrupts Future internals

This forwards assignments like _state/_condition to the inner future, breaking the wrapper’s own Future state and callback machinery.

Apply this diff to drop it:

-    def __setattr__(self, name: str, value: Any):
-        if name in ["_future", "_selector"]:
-            super().__setattr__(name, value)
-        else:
-            setattr(self._future, name, value)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def __setattr__(self, name: str, value: Any):
if name in ["_future", "_selector"]:
super().__setattr__(name, value)
else:
setattr(self._future, name, value)
🤖 Prompt for AI Agents
In executorlib/standalone/select.py around lines 17 to 21, the custom
__setattr__ forwards all attribute assignments (including _state/_condition) to
the wrapped Future which corrupts the inner Future's internals and breaks
callback/state machinery; remove this __setattr__ method entirely so attribute
assignments operate normally on the wrapper object and let explicit attribute
forwarding be handled only where safe and necessary.

Copy link
Contributor

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
executorlib/standalone/select.py (1)

20-25: Drop the custom result(); rely on the wrapper’s Future.result

Once propagation sets the wrapper’s result, overriding result() is unnecessary and bypasses the wrapper’s callback chain.

Apply this diff to remove it:

-    def result(self, timeout: Optional[float] = None) -> Any:
-        result = self._future.result(timeout=timeout)
-        if result is not None:
-            return result[self._selector]
-        else:
-            return None
♻️ Duplicate comments (1)
executorlib/standalone/select.py (1)

9-18: Fix wrapper state corruption: drop getattr/setattr, propagate completion via callback, and override cancel

Blanket delegation breaks Future semantics and corrupts the wrapper’s internal state; this also mirrors prior feedback.

Apply this diff to make the wrapper a proper Future that mirrors the inner one’s completion/cancel/exception and still returns the selected item:

-from concurrent.futures import Future
-from typing import Any, Optional
+from concurrent.futures import Future
@@
 class FutureSelector(Future):
     def __init__(self, future: Future, selector: int | str):
-        self._future = future
-        self._selector = selector
-        super().__init__()
+        super().__init__()
+        self._future = future
+        self._selector = selector
+        # Propagate inner completion -> wrapper state
+        def _propagate(src: Future):
+            if src.cancelled():
+                Future.cancel(self)
+                return
+            try:
+                res = src.result()
+                val = res[self._selector] if res is not None else None
+                Future.set_result(self, val)
+            except BaseException as e:
+                Future.set_exception(self, e)
+        future.add_done_callback(_propagate)
 
-    def __getattr__(self, attr: str) -> Any:
-        return getattr(self._future, attr)
-
-    def __setattr__(self, name: str, value: Any):
-        if name in ["_future", "_selector"]:
-            super().__setattr__(name, value)
-        else:
-            setattr(self._future, name, value)
+    def cancel(self) -> bool:
+        ok = self._future.cancel()
+        if ok:
+            Future.cancel(self)
+        return ok
🧹 Nitpick comments (2)
executorlib/standalone/select.py (2)

2-2: Clean up unused typing imports

After removing getattr/setattr/result, the typing imports are unused.

-from typing import Any, Optional

20-25: Optional: friendlier error for bad selector

If the inner result isn’t indexable or the key is missing, callers get TypeError/IndexError/KeyError. Consider re-raising with context.

If you want, I can draft a small helper to map these to a consistent ValueError with details.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03c3dd2 and 10c7f68.

📒 Files selected for processing (1)
  • executorlib/standalone/select.py (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: unittest_mpich (ubuntu-latest, 3.11)
  • GitHub Check: unittest_openmpi (ubuntu-24.04-arm, 3.13)
  • GitHub Check: unittest_openmpi (macos-latest, 3.13)
  • GitHub Check: unittest_mpich (macos-latest, 3.13)
  • GitHub Check: unittest_openmpi (ubuntu-22.04-arm, 3.13)
  • GitHub Check: unittest_openmpi (ubuntu-latest, 3.11)
  • GitHub Check: unittest_openmpi (ubuntu-latest, 3.12)
  • GitHub Check: unittest_openmpi (ubuntu-latest, 3.13)
  • GitHub Check: unittest_win
  • GitHub Check: notebooks_integration
  • GitHub Check: minimal
  • GitHub Check: unittest_slurm_mpich
  • GitHub Check: benchmark (ubuntu-latest, 3.13, .ci_support/environment-mpich.yml)
  • GitHub Check: unittest_flux_openmpi
  • GitHub Check: benchmark (ubuntu-latest, 3.13, .ci_support/environment-openmpi.yml)
  • GitHub Check: notebooks
  • GitHub Check: unittest_flux_mpich

Comment on lines 5 to +9
class FutureSelector(Future):
def __init__(self, future: Future, selector: int | str):
super().__init__()
self._future = future
self._selector = selector
super().__init__()
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Sanity checks before merge

  • Ensure callbacks added to the wrapper receive the wrapper (not the inner) after the propagation change.
  • Verify cancel() on the wrapper cancels the inner and marks the wrapper cancelled.

Run a quick repo scan for potential coupling to the previous delegation behavior:

Also applies to: 28-39, 42-55


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Call sites creating FutureSelector or using helpers
rg -nC2 -P '\b(FutureSelector|split_future|get_item_from_future)\s*\('

# Places adding callbacks to these futures
rg -nC2 -P '\.add_done_callback\s*\('

# Potential direct access to inner future fields (should be avoided)
rg -nC2 -P '\._future\b'

Length of output: 12653


Sanity checks before merge — fix FutureSelector delegation

  • Callbacks: add_done_callback must register callbacks on the wrapper so callbacks receive the wrapper. Current FutureSelector (init/getattr/setattr) forwards Future internals to the inner future, causing wrapper.add_done_callback to attach to the inner and callbacks to be invoked with the inner. See executorlib/standalone/select.py (FutureSelector init/getattr/setattr).
  • Cancel: wrapper.cancel() must cancel the inner and mark the wrapper cancelled (tests expect this). See tests/test_standalone_select.py (fs2.cancel / fs2.cancelled assertions around lines 68–71).
  • Coupling: executorlib/task_scheduler/interactive/dependency_plot.py directly reads arg._future and arg._selector (≈lines 39 and 122); update those call sites to use a public API or preserve the wrapper internals if you change delegation.
🤖 Prompt for AI Agents
In executorlib/standalone/select.py around lines 5–9, FutureSelector currently
forwards Future internals to the inner future which causes add_done_callback to
attach to the inner, prevents the wrapper from reflecting cancellation, and
breaks coupling with code that reads _future/_selector; modify FutureSelector so
add_done_callback registers callbacks on the wrapper (so callbacks receive the
wrapper), implement cancel() to call cancel() on the inner future and mark the
wrapper as cancelled (and cancelled() should reflect that), stop blindly
delegating attribute access of future internals (remove or limit
__getattr__/__setattr__ passthrough) or if you must keep internal fields,
preserve _future and _selector as attributes and update
executorlib/task_scheduler/interactive/dependency_plot.py to use public
accessors (e.g., .future and .selector) or read the preserved _future/_selector
directly; ensure wrapper forwards result/exception/state queries to the inner
but keeps control of callback registration and cancellation state.

@jan-janssen jan-janssen merged commit 91dc206 into main Sep 14, 2025
87 of 91 checks passed
@jan-janssen jan-janssen deleted the clean_up_future_selector branch September 14, 2025 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant