For an open-source tool without a web frontend, one of the main ways users will interact with ZenML is our command-line interface or CLI. I recently worked on an effort to improve the visual experience of anyone using our CLI by integrating the popular open-source library,
rich, into the code base. (This was part of our latest release, which adds a continuous deployment functionality to ZenML!)
The items that follow are what I consider to be low-hanging fruit for any CLI that is written in Python. You may already have custom solutions or use specific packages that offer certain features. It might be worth considering just getting all of that CLI and terminal goodness from
rich, however, given that it does so much for you with relatively little dependency bloating that you perhaps might expect.
Let’s cover the important one first 😉:
rich offers full support for emojis in your CLI interfaces. I’m being slightly flippant here, but only slightly. You may be familiar with emojis as used in chat apps such as the winking face above, but there are hundreds of other, potentially more useful, emojis that you might want to use.
For the ZenML CLI, we went with a ✅ tick emoji to indicate that an integration was installed when listing the available and supported integrations. We also chose a 👉 pointing hand emoji to indicate which component or stack was currently activated among the various configurations that we allow you to construct. Nothing too fancy in either case, but I think they’re more useful and communicative as a user than the other options (like an asterisk, for example). (You’ll see examples of how we used them below.)
You can view a list of all the supported emojis by running
python -m rich.emoji (after
Our CLI allows users to view information about the examples we provide to showcase how ZenML works (and how it can be used). Each example already contains a markdown
README.md file with information about the implementation, installation instructions and so on.
We didn’t want to duplicate work that had already gone into creating those information sheets, so we used them to allow the user to learn about the examples. A simple
zenml example info mlflow_tracking was used to output the raw text of the markdown file. For obvious reasons, this wasn’t satisfactory from a usability perspective.
rich, we have a way to parse the raw markdown markup and display it as a rich document. What’s more, we use the
pager which gives a familiar interface to anyone interacting with the info document. (In fact, it was searching for an option to handle this markdown parsing that first saw us discover
rich and all the other things it does.)
Errors are often where the rubber meets the road in software projects. When you’re developing you want those error messages to be informative, clear and not some kind of runic message you have to decode.
rich, you get a complete redesign of how tracebacks are displayed, one that I have found far more useful when trying to understand why a particular code change has caused an error. Moreover, you have the option to have local variables displayed alongside the stack trace message, all neatly boxed up to make it clear what you’re looking at.
Enabling this as the default way to display Python tracebacks is as simple as adding the following to somewhere that always gets loaded:
from rich.traceback import install install(show_locals=True)
print()gets a makeover
Just like tracebacks in
rich are better than the Python defaults, you also have a better
pprint — though it doesn’t have the colors.)
We don’t actually use any
rich our users get access to it for their own purposes, be it debugging or otherwise.
I have been using
rich ever since I first saw it used. Like most things in this post, it is a convenience function that offers a better default to standard Python ways of inspecting an object. See the above illustration of what the output looks like. If you pass in
methods=True you’ll see what methods can be called on that object. If you pass in
docs=True you can read the docstrings for that object.
inspect in their own pipelines.
When someone tells you that they upgraded their CLI tool, spinners are what you expect. Who doesn’t love a good spinner!? We added only one (when you call
zenml init) but probably will use more as our tool grows.
You can get a good idea of the kinds of spinners available by running
python -m rich.status which will output a sort of demo with some spinners. Adding this into your code is painless with a simple context manager:
with console.status("Doing really important work…"): # do something here
The next step up from a spinner is a progress bar. You get these with
rich and they’re easy to set up:
from rich.progress import track some_iterable =  for n in track(range(len(some_iterable)), description="Doing important things…"): # do something here with the iterable's values
Not only do these progress bars offer a visual indication of your progress,
rich also does some background calculations and it suggests an approximate time until completion (based on how quickly you move through the elements).
We use all sorts of tables in our CLI. We display the integrations you have installed, the stacks you have set up, the examples available for download and so on.
A clear table is an easy win to make it easier for the user to interact with CLI output. You can get much, much more with advanced
rich tabular composition features, but probably you don’t need anything complicated. You just need a table with lines where previously you didn’t have that.
If you have an application of more than minimal complexity you will likely want to have different variations of how you output to the terminal. Maybe you want specific colors for warning or error messages, or there’s a particular style that should only be used in certain situations. For all that,
rich offers a, well, rich API and set of functionality that allows you to output pretty much everything you’d want to the terminal.
Check out the docs for the full details, but
rich will handle any kind of styling and colors that you want to include, justification and alignment within the boundaries of the terminal, soft wrapping, and so on.
import logging from rich.logging import RichHandler # an example of how to set up a rich logger (from the rich docs) FORMAT = "%(message)s" logging.basicConfig( level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] ) log = logging.getLogger("rich") log.info("Hello, World!")
We haven’t fully committed to this yet in the ZenML CLI, but if you want all
rich’s goodies in all of your CLI output, use the
rich logging handler. Simply set the
RichHandler as (one of your) logging handlers when you’re configuring your
In this way, you’ll get access to everything that
rich offers, except now it’s in your logs. You probably want to be careful with this, especially if logs are in any sense mission-critical, since the console markup might cause issues when reviewing those logs at a later date. It’s nevertheless a full-featured way of handling your logging output and if you don’t already have a custom setup, this is probably worth checking out.
Let us know if you end up using these tips and the
rich library to spruce up your CLI! Get the latest version of ZenML to use all our latest richified CLI goodness.
Alex Strick van Linschoten is a Machine Learning Engineer at ZenML.