Skip to content

Add .rx.awaiting to report whether an expression is still resolving - #1176

Open
philippjfr wants to merge 1 commit into
mainfrom
rx-awaiting
Open

Add .rx.awaiting to report whether an expression is still resolving#1176
philippjfr wants to merge 1 commit into
mainfrom
rx-awaiting

Conversation

@philippjfr

Copy link
Copy Markdown
Member

Problem

Since #1173 an rx node whose asynchronous operation is in flight reports param.Undefined
rather than the value it computed from inputs that have since been superseded. That fixed the
stale value, but it left no way to ask a reactive expression whether anything feeding it is
still in flight.

API

reactive_ops.awaiting is a read-only property on the .rx namespace:

>>> expr = param.rx(1).rx.pipe(slow_double) + 1
>>> expr.rx.value is param.Undefined
True
>>> expr.rx.awaiting
True

It is a plain bool, not a reactive expression, which makes it the only member of the
namespace that is not composable (apart from .rx.value).

Changes

reactive_ops.awaiting walks the graph and asks each node whether it is settling:

return any(node._settling for node in reactive._upstream())

rx._upstream() yields the node and everything it derives its value from. Inputs reach a node
by three routes and all of them have to be followed, because an operation is only as settled
as the nodes feeding it:

  • the _prev chain of the pipeline the node belongs to,
  • the _shared input it was cloned from when a pipeline branches,
  • and any rx passed as an argument to one of its operations, which
    _iter_rx finds by descending the same containers resolve_value does (list, tuple, set,
    dict keys and values, slice components).

The walk is iterative with an id()-keyed seen-set, because reusing an input clones it and
links the clones through _shared, so the graph is a DAG rather than a chain and a naive walk
would revisit nodes.

rx._settling is self._awaiting or self._awaiting_ref, i.e. either of the two resolution
mechanisms. rx._awaiting_ref checks the node's internal parameters against
Parameters._awaiting_ref from #1175, covering the case where the asynchronous callable is
held on a parameter rather than applied as an operation. Both are private; _awaiting keeps
its existing meaning and awaiting is the only public addition.

Design notes

Reading it does not schedule anything. awaiting deliberately does not touch _obj or
otherwise resolve the node, so an expression whose value has never been requested reports
False rather than True. "Nothing has been asked for yet" is not the same as "a result is
pending", and a property that started work as a side effect of being read would be a trap in
a loading indicator.

Generators settle per emission. A generator or async generator ref settles on each value
it yields rather than on exhaustion, so awaiting is True only until the next value
arrives. A spinner driven by it stops between emissions instead of spinning until the stream
ends, which is the useful reading for a streaming source.

_skipped was not usable for this. It conflates "the async result is not in yet" with
"an operation raised Skip", which are precisely the two states awaiting exists to tell
apart.

Docs

Adds a .rx.awaiting section to the Reactive_Expressions.ipynb user guide, contrasting it
with .rx.updating(): updating() tracks a synchronous computation, while an asynchronous
operation outlives the update that scheduled it. The list of special methods on .rx closes
by saying each entry returns a reactive expression, so awaiting is mentioned after it with
the difference called out rather than being added to the list.

The example uses a single top-level await in one cell so a docs build is deterministic
rather than depending on when the notebook's kernel gets around to a background task.

AI Disclosure

Written with the assistance of Claude Opus 5.

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.13043% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.97%. Comparing base (83c4dca) to head (3f614b9).

Files with missing lines Patch % Lines
param/reactive.py 89.13% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1176      +/-   ##
==========================================
+ Coverage   86.90%   86.97%   +0.07%     
==========================================
  Files           9        9              
  Lines        5398     5444      +46     
==========================================
+ Hits         4691     4735      +44     
- Misses        707      709       +2     

☔ View full report in Codecov by Harness.
📢 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.

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