Source code for m4opt.utils.console

"""Utilities for console applications and text user interfaces.

Use the :meth:`progress` and :meth:`status` methods to create live feedback
for a nested series of tasks. The elapsed time is shown for each task, along
with whether it completed successfully or failed due to an exception being
raised.

Examples
--------

.. code:: python

    from m4opt.utils.console import progress, status
    from time import sleep
    with progress():
        with status("Prepare the dough"):
            with status("Proof the yeast"):
                pass  # do some work here
            with status("Mix the wet and dry ingredients"):
                pass  # do some work here
            with status("Knead for 10 minutes"):
                pass  # do some work here
            with status("Let rise for 1 hour"):
                pass  # do some work here
        with status("Preheat oven to 500° F"):
            pass  # do some work here
        with status("Assemble the pizza"):
            with status("Roll out the dough"):
                pass  # do some work here
            with status("Top with sauce and cheese"):
                pass  # do some work here
            with status("Add your favorite toppings"):
                pass  # do some work here
        with status("Bake until golden brown"):
            pass
        with status("Serve to your hungry guests"):
            raise RuntimeError("Sorry, I ate it all")

.. code:: text

    ✓ Prepare the dough                 0:00:00
      ✓ Proof the yeast                 0:00:00
      ✓ Mix the wet and dry ingredients 0:00:00
      ✓ Knead for 10 minutes            0:00:00
      ✓ Let rise for 1 hour             0:00:00
    ✓ Preheat oven to 500° F            0:00:00
    ✓ Assemble the pizza                0:00:00
      ✓ Roll out the dough              0:00:00
      ✓ Top with sauce and cheese       0:00:00
      ✓ Add your favorite toppings      0:00:00
    ✓ Bake until golden brown           0:00:00
    ✗ Serve to your hungry guests       0:00:00
    Traceback (most recent call last):
      File "/Users/lpsinger/src/m4opt/test.py", line 24, in <module>
        raise RuntimeError("Sorry, I ate it all")
    RuntimeError: Sorry, I ate it all
"""

from contextlib import contextmanager

import rich.console
from rich.progress import Progress, SpinnerColumn, TimeElapsedColumn
from rich.text import Text

__all__ = ("progress", "status")

_progress = None
_depth = 0
_max_depth = 2
_is_jupyter = rich.console._is_jupyter()


[docs] @contextmanager def progress(): """Context manager to create a live display for showing status of tasks. If there is already an active progress display, this method will return it instead of creating a new one. """ global _progress if not _is_jupyter and _progress is None: with Progress( IndentedSpinnerColumn(finished_text="[bar.finished]✓"), TimeElapsedColumn() ) as new_progress: _progress = new_progress try: yield _progress finally: _progress = None else: yield _progress
class IndentedSpinnerColumn(SpinnerColumn): def render(self, task): return ( Text(task.fields["depth"] * " ") + ( Text("✗", style="red") if task.fields["failed"] else super().render(task) ) + Text(f" {task.description}", style=None if task.completed else "bold") )
[docs] @contextmanager def status(description: str): """Context manager to track the runtime of a task.""" global _depth if _is_jupyter or _depth >= _max_depth: yield else: with progress() as pg: task = pg.add_task(description, total=1, depth=_depth, failed=False) _depth += 1 try: yield except: pg.update(task, failed=True) raise else: pg.update(task, advance=1, completed=True) finally: _depth -= 1
if __name__ == "__main__": from time import sleep for roman_numeral in ["I", "II", "III"]: with status(f"Task {roman_numeral}"): for letter in ["A", "B", "C"]: with status(f"Task {letter}"): for number in ["1", "2", "3"]: if roman_numeral == "III" and letter == "B" and number == "1": raise RuntimeError("Failed") sleep(1)