Skip to Content

Introducing python-cli-ui

Posted on 4 mins read Tags: Python

For quite some time I’ve been adding a file called ui.py in some of the Python projects I was working on.

Since I believe in the rule of three and I already have three different projects using it, I’ve decided to share it to the world.

Feel free to take a look at the github page.

What it does

Let’s start with a screen shot:

ui demo

Coloring

It’s a module to write colorful command line interfaces in Python.

It’s a tiny wrapper on top of colorama, with a (IMHO) nicer API than crayons or lazyme.

Here’s an example:

ui.info(ui.red, "Error", ui.reset, ui.bold, file_path, ui.reset, "not found")

This will print the word ‘Error’ in red, the file path in bold, and the ‘not found’ normally.

The API follows the behavior of the print() function: by default, tokens are separated by spaces, \n is added at the end, and you can specify your own sep and end keywords if you need to.

Displaying enumerations

It also allows to display items of a list, taking care of “off-by-one” errors and aligning the numbers nicely (note the leading space for 1/12)

>>> months = ["January", "February", ..., "December"]
>>> for i, month in enumerate(months):
>>>     ui.info_count(i, 12, month)
* ( 1/12) January
* ( 2/12) February
...
* (12/12) December

Indenting

>>> first_name = "John"
>>> last_name = "Doe"
>>> adress = """\
ACME Inc.
795 E Dragram
Tucson AZ 85705
USA
"""
>>> ui.info("People")
>>> ui.info(ui.tabs(1), first_name, last_name)
>>> ui.info(ui.tabs(1), "Adress:")
>>> ui.info(ui.indent(2, adress))
People:
  John Doe
  Adress:
    795 E Dragram
    Tucson AZ 85705
    USA

Unicode goodness

You can define your own suite of characters, which will get a color, a unicode and an ASCII representation (so that it works on Windows too.)

For instance:

check  = UnicodeSequence(green, "✓", "ok")

ui.info("Success!", ui.check)

On Linux:

unicode check

On Windows:

ascii check

Semantics

The module also contains “high-level” methods such as info_1, info_2, info_3, so that you can write:

ui.info_1("Making some tea")
...
ui.info_2("Boiling water")
...
ui.info_3("Turning kettle on")
...
ui.info_1("Done")

Which will be rendered as:

python ui cli example

This allows you to group your messages in a coherent way. Here the main message is ‘Making some tea’. ‘Boiling water’ is a sub-task of making the tea, and ‘Turning the kettle on’ is a sub-task of the boiling water process.

In the same vein, warning, error and fatal methods are provided for when things go wrong. (The last one calls sys.exit(), hence the name)

There’s also a debug function, for messages you are only interested when debugging: you can control the verbosity using the CONFIG global dictionary.

ui.CONFIG['quiet'] = True
ui.info("this is some info") # won't get printed
ui.CONFIG['verbose'] = True
ui.debug("A debug message") # will get printed

Interaction

Arbitrary string with a default

>>> domain = ui.ask_string("Please enter your domain name", default="example.com")
>>> print("You chose:", domain)
> Please enter your domain name (example.com)
(nothing)
> You chose example.com

>>> domain = ui.ask_string("Please enter your domain name", default="example.com")
>>> print("You chose:", domain)
> Please enter your domain name (example.com)
foobar.com
> You chose foobar.com

Boolean choice

Note how the prompt goes from Y/n (y uppercase), to y/N (n uppercase) depending on the default value:

>>> with_sugar = ui.ask_yes_no("With sugar?", default=True)
> "With sugar ? (Y/n)
n
> False

>>> with_cream = ui.ask_yes_no("With cream?", default=False)
> "With cream? (y/N)
(nothing)
> False

Choice in a list

Note how the user is stuck in a loop until he enters a valid answer, and how the first item is selected by default:

>>> choices = ["apple", "orange", "banana"]
>>> answer = ui.ask_choice("Select a fruit:", choices)
> Select a fruit:
1. apple (default)
2. orange
3. banana
> foobar
Please enter a number between 1 and 3
> 4
Please enter a number between 1 and 3
> 2
>>> print(answer)
oranges

Other goodies

A timer

@ui.timer("Doing foo")
def foo():
     # something that takes time

>>> foo()
... # output of the foo method
Doing foo took 3min 13s

Works also in a with statement:

with ui.timer("making foobar"):
    foo()
    bar()

Did you mean?

>>> commands = ["install", "remove"]
>>> user_input = input()
intall
>>> ui.did_you_mean("No such command", user_input, choices)
No such command.
Did you mean: install?

A pytest fixture

You can also write tests to assert that a certain message matching a regexp was emitted.

def say_hello(name):
    ui.info("Hello", name)

def test_say_hello(messages):
    say_hello("John")
    assert messages.find(r"Hello\w+John")

Parting words

Well, I hope you’ll find this module useful.

It is available as python-cli-ui on pypi

Cheers!


Thanks for reading this far :)

I'd love to hear what you have to say, so please feel free to leave a comment below, or read the feedback page for more ways to get in touch with me.

Note that to get notified when new articles are published, you can either:

Cheers!