How to know when and Output() Widget becomes non-empty?

Desired Interface:

There is an accordion of many Output member widgets. Each one has a title. In the beginning all are collapsed. It is expected that the user will probably examine at most one, or perhaps two, at a time.

Problem:

Each Output member widget may have text in it, or it might be empty. Hence, the situation is like a shell game for the user. The user has to expand each and check to find the one with text. This is no good.

Desired Solution:

When a member Output widget becomes non-empty, an asterisk is prepended to the title. When the member Output widget is cleared, the asterisk is removed from the corresponding title.

This would work fine.

This implementation did not work:

After an member Output widget was written, a function was called to change the corresponding title.

This fails because there are function calls to library routines in with blocks. Unexpected error messages are showing up asynchronously in member Output blocks. These are not recognized as writes to the blocks, so the asterisk does not end up in the title.

So then callbacks?

Consider this blocks of code:

w = widgets.Output()
w2 = widgets.Output()
def msg(a):
  with w2:
    print("msg")
def disp(a):
  with w2:
    print("disp")
w.on_msg(msg)
w.on_displayed(disp)
display(w2)

When w is displayed, disp is displayed in w2. That is all well and good, but that doesn’t say when text has been written to an Output, it just says when w is displayed.

The msg callback never occurs. I had hoped that indicated that the Output had received a message.

So bottom line, how can the program know when an Output becomes non-empty? … are these callback registration methods documented somewhere? When I search the readthedocs doc, it does not find them.

The next thing to try will be a polling loop that checks the out.get_state() on each member Output. Apart from the latency, that will work. But am I missing the right way to do this?

Here is a hack, in case someone else runs into this. Perhaps someone knows a better way to to do this. I am only using this as a standard out/error catcher. I don’t know if it would work in a more general case:

import ipywidgets
W = ipywidgets
L = W.Layout
import asyncio
from threading import Thread

class Output(W.Output):
  def __init__(self ,*args):
    super().__init__(*args)
    self.was_empty = True
    self.on_became_not_empty_fs = []
    self.interval = 1
    self.check_became_not_empty_loop_task = asyncio.create_task(self._check_became_not_empty_loop())
    
  def __del__(self):
    self.check_became_not_empty_loop_task.cancel()
    
  def _check_became_not_empty(self):
    now_empty = self.empty()
    if not now_empty and self.was_empty:
      for f in self.on_became_not_empty_fs: 
        thread = Thread(target=f)
        thread.start()
    self.was_empty = now_empty
      
  # asyncio uses green threads, so using threads here doesn't accmplish much.  It does make the sleep timer for loop delay more accurate.
  async def _check_became_not_empty_loop(self):
    while True: 
      self._check_became_not_empty()
      await asyncio.sleep(self.interval)   
        
  def empty(self):
    return len(self.outputs) == 0 

  def on_became_not_empty(self ,f):
    self.on_became_not_empty_fs.append(f)
    
  def clear_output(self):
    self._check_became_not_empty() # it might have happened since we last polled
    self.was_empty = True
    super().clear_output()
1 Like