CLIs – Command Line Interfaces tools are very useful for most developers and system engineers. In most cases it is expected for it to be pretty responsive. However sometimes a task behind a CLI command may be time demanding and in order not to keep users happy, we mostly use spinners and progress bars to indicate that the process is on going.
In this tutorial we will explore some ways to add progress bars and spinners to your python CLI tools. We will be using Click as our CLI library of choice. Let us begin
Click – Progress Bar
Click provides its own progress bar functionality that integrates well with Click commands. We can use Click’s progress bar as below
import time
import click
@cli.command()
@click.argument("file", type=click.Path(exists=True))
def analyze_file(file):
"""Analyze this file."""
with open(file, "r") as f:
content = f.read()
with click.progressbar(length=100, label="Generating analysis") as bar:
# Simulate progress
for i in range(100):
time.sleep(0.1) # Simulate some work
bar.update(1)
click.secho(f"Analysing results for {file}:", fg="cyan")
click.echo(suggestions)
This approach provides a simple progress bar that fits well with Click’s design
Use a spinner instead of a progress bar
We can also use a spinner and threading for this as shown below
import time
import threading
import click
@cli.command()
@click.argument("file", type=click.Path(exists=True))
def analyze_file(file):
"""Analyze this file."""
with open(file, "r") as f:
content = f.read()
def spinner():
spinner = "|/-\\"
i = 0
while not done.is_set():
click.echo(f"\rGenerating analysis {spinner[i % len(spinner)]}", nl=False)
time.sleep(0.1)
i += 1
done = threading.Event()
thread = threading.Thread(target=spinner)
thread.start()
try:
analzsis_results = some_fxn(file)
finally:
done.set()
thread.join()
click.echo("\r", nl=False) # Clear the spinner
click.secho(f"Analysing results for {file}:", fg="cyan")
click.echo(suggestions)
This approach provides visual feedback without implying a specific progress percentage[5].
Using Tqdm
Tqdm is a popular and powerful progressbar and spinner library that you can use, you could do something like this:
from tqdm import tqdm
import time
import click
@cli.command()
@click.argument("file", type=click.Path(exists=True))
def analyze_file(file):
"""Analyze this file."""
with open(file, "r") as f:
content = f.read()
with tqdm(total=100, desc="Generating analysis", ncols=70) as pbar:
# Simulate progress
for i in range(100):
time.sleep(0.1) # Simulate some work
pbar.update(1)
suggestions = some_fxn(file)
click.secho(f"Analysing results for {file}:", fg="cyan")
click.echo(suggestions)
However, be aware that using tqdm might introduce some overhead and could potentially slow down your code if not used carefully.
Reusable Spinner Decorator
First, let’s create a utility function for the spinner in a format of a decorator. This allows us to reuse it many times.
import threading
import time
import click
def spinner(message="Processing"):
def spin():
spinner_chars = "|/-\\"
i = 0
while not done.is_set():
click.echo(f"\r{message} {spinner_chars[i % len(spinner_chars)]}", nl=False)
time.sleep(0.1)
i += 1
done = threading.Event()
thread = threading.Thread(target=spin)
thread.start()
return done, thread
def stop_spinner(done, thread):
done.set()
thread.join()
click.echo("\r", nl=False) # Clear the spinner
Now, we can create a decorator to easily apply this spinner to any command.
import functools
def with_spinner(message="Processing"):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
done, thread = spinner(message)
try:
result = func(*args, **kwargs)
finally:
stop_spinner(done, thread)
return result
return wrapper
return decorator
You can use this decorator in your main CLI file. Here’s how you can apply it to multiple commands:
from utils import with_spinner
@cli.command()
@click.argument("file", type=click.Path(exists=True))
@with_spinner("Analyzing code")
def analyze(file):
"""Analyze code quality and complexity"""
analysis = devx.analyze_code(file)
click.secho(f"Analysis for {file}:", fg="cyan")
click.echo(analysis)
@cli.command()
@click.argument("file", type=click.Path(exists=True))
@with_spinner("Explaining code")
def explain(file):
"""Explain the code in a file."""
explanation = devx.explain_code(file)
click.secho(f"Explanation for {file}:", fg="cyan")
click.echo(explanation)
By converting it to a decorator, the spinner can be easily applied to any command by using the @with_spinner
decorator, making it more reusable, cleaner and consistent with user experience
This approach provides a clean, reusable solution for adding spinners to your Click commands, enhancing the user experience for long-running operations
I hope this was useful.
Happy Coding
Jesus Saves
By Jesse E.Agbe(JCharis)