4

There are numerous existing questions regarding the display of progress bars in the terminal while a Python script executes, but every one of them is based on a loop where you perform an operation and then update the progress graphic.

Unfortunately, the function whose progress I want to show--or at least a spinner object to show that it's working--is a black-box that I can't (at least really, really shouldn't) alter. Essentially, what I want to do is:

#pseudocode input
print('Loading')
spinner.begin()
blackbox() #a few thousand operations happen in here
spinner.end()
print('Finished')

#pseudocode output
Loading.
Loading..
Loading...
Loading.
Loading..
Loading...
Finished

Although ideally that would be an animation of the ellipsis instead of printing multiple lines. Before I can even start building silly ascii animations though, there's the main hurdle:

Is there a way to run spinner and blackbox() at the same time? Alternately, is there a hack to pause blackbox(), regardless of its content, every few hundred milliseconds, update the spinner graphic, and then resume where it left off?

I've tried this with the progress module but had no luck... I couldn't even get the example code to work, it just hung up after I started iterating until I Ctrl+C'd out.

Steve R
  • 131
  • 2
  • 11

3 Answers3

5

I like using alive_progress for this.

alive_bar spinner

from typing import ContextManager, Optional
from alive_progress import alive_bar

def spinner(title: Optional[str] = None) -> ContextManager:
    """
    Context manager to display a spinner while a long-running process is running.

    Usage:
        with spinner("Fetching data..."):
            fetch_data()

    Args:
        title: The title of the spinner. If None, no title will be displayed.
    """
    return alive_bar(monitor=None, stats=None, title=title)

To install: pip install alive-progress

crypdick
  • 16,152
  • 7
  • 51
  • 74
  • 1
    Just found this library and it's amazing. I was originally using [click-spinner](https://github.com/click-contrib/click-spinner), but this is much better. Appreciate you giving this a shoutout! – takanuva15 May 18 '23 at 19:43
2

Threads is probably the easiest way to make this work. Here is a vastly simplified version that should get the point across. I wasn't sure whether you actually have the spinner function or not, so I made my own.

import threading
import time

def blackbox():
    time.sleep(10)

thread = threading.Thread(target=blackbox)
thread.start()

eli_count = 0
while thread.is_alive():
    print('Loading', '.'*(eli_count+1), ' '*(2-eli_count), end='\r')
    eli_count = (eli_count + 1) % 3
    time.sleep(0.1)
thread.join()
print('Done      ')

So, while blackbox runs, the loading message is updated periodically. Once it finishes, the thread is joined and the loading message is replaced with a completed message.

FamousJameous
  • 1,565
  • 11
  • 25
  • Great, that's exactly what I was looking for. Is there much of a performance hit from running multiple threads? It takes a fairly long time as is. – Steve R Jun 30 '17 at 17:50
  • The blackbox function should run about the same in a thread. It will have to share the CPU core with the main thread, but since the main thread is hardly doing anything (at least in my example), that shouldn't affect it much. If there is a lot of CPU work happening in your main thread while the blackbox is running, you could see a performance impact. – FamousJameous Jun 30 '17 at 17:55
  • Okay, sounds good; I won't be doing much more than your example in the primary thread. Although if `blackbox()` is a method of some class, can I mess with other attributes of the class while `blackbox()` is running on another thread? – Steve R Jun 30 '17 at 17:57
  • That could be dangerous. If `blackbox` doesn't use those attributes, you can probably get away with it, otherwise you might want to look at protecting those attributes with locks. – FamousJameous Jun 30 '17 at 18:01
  • Alright, I think I'll just leave well enough alone, then. All the other operations take <1 second to run, it's just `blackbox()` that's the time hog. Thanks again. – Steve R Jun 30 '17 at 18:02
1

You probably want to use threads (import threading). Have spinner.begin() start a thread that prints your messages, then let your blackbox run, and then have spinner.end() send a finish message to the thread using a Queue (from Queue import Queue) or something, join() the thread and keep doing whatever it is you do.

As a design choice, hide the prints somewhere deeper, not in the same block of code as the begin and end calls.

Re.po
  • 214
  • 1
  • 7