How to Add Progress Bars & Spinners to CLIs in Python

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)

        Leave a Comment

        Your email address will not be published. Required fields are marked *