Python CLI UI

Tools for nice user interfaces in the terminal.

Installation

cli-ui is available on Pypi. and is compatible with Python 3.7 and higher.

Note for Windows users

cli_ui tries to be smart and disables color output when it would create weird strings to appear on screen, and this works pretty well on non-Windows platforms (because the behavior of the isatty() function be trusted there).

On Windows, the behavior of isatty() cannot be trusted (it may return False on Mintty for instance), so coloring is disabled unless you opt-in by calling setup() with color=always.

API

Configuration

cli_ui.setup(*, verbose: bool = False, quiet: bool = False, color: str = 'auto', title: str = 'auto', timestamp: bool = False) None

Configure behavior of message functions.

Parameters
  • verbose – Whether debug() messages should get printed

  • quiet – Hide every message except warning(), error(), and fatal()

  • color – Choices: ‘auto’, ‘always’, or ‘never’. Whether to color output. By default (‘auto’), only use color when output is a terminal.

  • title – Ditto for setting terminal title

  • timestamp – Whether to prefix every message with a time stamp

>>> cli_ui.debug("this will not be printed")
<nothing>
>>> cli_ui.setup(verbose=True)
>>> cli_ui.debug("this will be printed")
this will be printed

Constants

You can use any of these constants as a token in the following functions:

  • Effects:

    • bold

    • faint

    • standout

    • underline

    • blink

    • overline

    • reset

  • Colors:

    • black

    • blue

    • brown

    • darkblue

    • darkgray

    • darkgreen

    • darkred

    • darkyellow

    • fuchsia

    • green

    • lightgray

    • purple

    • red

    • teal

    • turquoise

    • white

    • yellow

Note

Each of these constants is an instance of Color.

  • Sequence of Unicode characters:

    • check: ✓ (green, replaced by ‘ok’ on Windows)

    • cross: ❌ (red, replaced by ‘ko’ on Windows)

    • ellipsis: … (no color, replaced by ‘…’ on Windows)

    You can create your own colored sequences using UnicodeSequence:

class cli_ui.UnicodeSequence(color: cli_ui.Color, as_unicode: str, as_ascii: str)

Represent a sequence containing a color followed by a Unicode symbol

>>> up_arrow = cli_ui.UnicodeSequence(cli_ui.blue, "↑", "+")
>>> cli_ui.info(up_arrow, "2 commits")
↑ 2 commits # on Linux
+ 2 commits # on Windows

Alternatively, if you do not want to force a color, you can use Symbol:

class cli_ui.Symbol(as_unicode: str, as_ascii: str)
>>> heart = cli_ui.Symbol("❤", "<3")
>>> cli_ui.info("Thanks for using cli-ui", heart)
Thanks for using cli-ui ❤  # on Linux
Thanks for using cli-ui <3  # on Windows

Informative messages

cli_ui.info(*tokens: Any, **kwargs: Any) None

Print an informative message

Parameters
  • tokens – list of ui constants or strings, like (cli_ui.red, 'this is an error')

  • sep – separator, defaults to ' '

  • end – token to place at the end, defaults to '\n'

  • fileobj – file-like object to print the output, defaults to sys.stdout

  • update_title – whether to update the title of the terminal window

>>> cli_ui.info("this is", cli_ui.red, "red")
This is red

Functions below take the same arguments as the info() function

cli_ui.info_section(*tokens: Any, **kwargs: Any) None

Print an underlined section name

>>> cli_ui.info_section("Section one")
>>> cli_ui.info("Starting stuff")

Section one
------------

Starting stuff
cli_ui.info_1(*tokens: Any, **kwargs: Any) None

Print an important informative message

>>> cli_ui.info_1("Message")
:: Message
cli_ui.info_2(*tokens: Any, **kwargs: Any) None

Print an not so important informative message

>>> cli_ui.info_2("Message")
=> Message
cli_ui.info_3(*tokens: Any, **kwargs: Any) None

Print an even less important informative message

>>> cli_ui.info_3("Message")
* Message
cli_ui.debug(*tokens: Any, **kwargs: Any) None

Print a debug message.

Messages are shown only when CONFIG["verbose"] is true

>>> cli_ui.debug("Message")
<nothing>

Error messages

Functions below use sys.stderr by default:

cli_ui.error(*tokens: Any, **kwargs: Any) None

Print an error message

>>> cli_ui.error("Message")
Error: message
cli_ui.warning(*tokens: Any, **kwargs: Any) None

Print a warning message

>>> cli_ui.warning("Message")
Warning: message
cli_ui.fatal(*tokens: Any, exit_code: int = 1, **kwargs: Any) None

Print an error message and exit the program

Parameters
  • tokens – list of ui constants or strings, like (cli_ui.red, "this is a fatal  error")

  • exit_code – value of the exit code (default: 1)

>>> cli_ui.fatal("Message")
Error: message
exit(1)

>>> cli_ui.fatal("Another message", exit_code=2)
Error: Another message
exit(2)

Progress messages

cli_ui.dot(*, last: bool = False, fileobj: IO[str] = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>) None

Print a dot without a newline unless it is the last one.

Useful when you want to display a progress with very little knowledge.

Parameters

last – whether this is the last dot (will insert a newline)

>>> for in in range(0, 5):
>>>     cli_ui.dot()
....<no newline>
>>> cli_ui.dot(last=True)
.....
cli_ui.info_count(i: int, n: int, *rest: Any, **kwargs: Any) None

Display a counter before the rest of the message.

rest and kwargs are passed to info()

Current index should start at 0 and end at n-1, like in enumerate()

Parameters
  • i – current index

  • n – total number of items

>>> cli_ui.info_count(4, 12, message)
* ( 5/12) message
cli_ui.info_progress(prefix: str, value: float, max_value: float) None

Display info progress in percent.

Parameters
  • value – the current value

  • max_value – the max value

  • prefix – the prefix message to print

>>> cli_ui.info_progress("Done", 5, 20)
Done: 25%

Formatting

cli_ui.tabs(num: int) str

Compute a blank tab

>>> cli_ui.info("one", "\n",
            cli_ui.tabs(1), "two", "\n",
            cli_ui.tabs(2), "three", "\n",
            sep="")
one
  two
    three
cli_ui.indent(text: str, num: int = 2) str

Indent a piece of text.

>>> quote = (
      "First, we take Manhattan\n"
      "Then we take Berlin!"
)
>>> cli_ui.info("John said:", cli_ui.indent(quote))
John said:
   First, we take Manhattan.
   Then we take Berlin!
cli_ui.info_table(data: Any, *, headers: Union[str, Sequence[str]] = (), fileobj: IO[str] = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>) None
>>> headers=["name", "score"]
>>> data = [
      [(bold, "John"), (green, 10.0)],
      [(bold, "Jane"), (green, 5.0)],
    ]

>>> cli_ui.info_table(data, headers=headers)
name      score
--------  --------
John       10.0
Jane        5.0

Asking for user input

cli_ui.read_input() str

Read input from the user

cli_ui.ask_string(*question: Any, default: Optional[str] = None) Optional[str]

Ask the user to enter a string.

>>> name = cli_ui.ask_string("Enter your name")
:: Enter your name
<john>
>>> name
'john'
cli_ui.ask_choice(*prompt: Any, choices: List[Any], func_desc: Optional[Callable[[Any], str]] = None, sort: Optional[bool] = True) Any

Ask the user to choose from a list of choices.

Will loop until:
  • the user enters a valid index

  • or leaves the prompt empty

In the last case, None will be returned

Parameters
  • prompt – a list of tokens suitable for info()

  • choices – a list of arbitrary elements

  • func_desc – a callable. It will be used to display and sort the list of choices (unless sort is False) Defaults to the identity function.

  • sort – whether to sort the list of choices.

Returns

the selected choice.

>>> choices = ["apple", "banana", "orange"]
>>> fruit = cli_ui.ask_choice("Select a fruit", choices=choices)
:: Select a fruit
  1 apple
  2 banana
  3 orange
<2>
>>> fruit
'banana'

Changed in version 0.10: Add sort parameter to disable sorting the list of choices

Changed in version 0.8: choices is now a named keyword argument

Changed in version 0.7: The KeyboardInterrupt exception is no longer caught by this function.

cli_ui.select_choices(*prompt: Any, choices: List[Any], func_desc: Optional[Callable[[Any], str]] = None, sort: Optional[bool] = True) Any

Ask the user to select one or multiple from a list of choices, delimited by space, comma or semi colon.

Will loop until:

  • the user enters a valid selection

  • or leaves the prompt empty

In the last case, None will be returned

Parameters
  • prompt – a list of tokens suitable for info()

  • choices – a list of arbitrary elements

  • func_desc – a callable. It will be used to display and sort the list of choices (unless sort is False) Defaults to the identity function.

  • sort – whether to sort the list of choices.

Returns

the selected choice(s).

>>> choices = ["apple", "banana", "orange"]
>>> fruits = cli_ui.ask_choice("Select several fruits", choices=choices)
:: Select a fruit
  1 apple
  2 banana
  3 orange
<1, 2>
>>> fruits
['apple', 'banana']
cli_ui.ask_yes_no(*question: Any, default: bool = False) bool

Ask the user to answer by yes or no

>>> with_cream = cli_ui.ask_yes_no("With cream?", default=False)
:: With cream? (y/N)
<y>
>>> with_cream
True
cli_ui.read_password() str

Read a password from the user

cli_ui.ask_password(*question: Any) str

Ask the user to enter a password.

>>> favorite_food = cli_ui.ask_password("Guilty pleasure?")
:: Guilty pleasure?
****
>>> favorite_food
'chocolate'

Displaying duration

class cli_ui.Timer(description: str)

Display time taken when executing a list of statements.

>>> @cli_ui.Timer("something")
    def do_something():
         foo()
         bar()
# Or:
>>> with cli_ui.Timer("something"):
        foo()
        bar()
* Something took 0h 3m 10s 430ms

Auto-correct

cli_ui.did_you_mean(message: str, user_input: str, choices: Sequence[str]) str

Given a list of choices and an invalid user input, display the closest items in the list that match the input.

>>> allowed_names = ["Alice", "John", "Bob"]
>>> name = cli_ui.ask_string("Enter a name")
>>> if not name in allowed_names:
>>>       cli_ui.did_you_mean("Invalid name", user_input, choices)
:: Enter a name
<Joohn>
Invalid name.
Did you mean: John?

Note: if the list of possible choices is short, consider using ask_choice() instead.

Testing

class cli_ui.tests.MessageRecorder

Helper class to tests emitted messages

find(pattern: str) Optional[str]

Find a message in the list of recorded message

Parameters

pattern – regular expression pattern to use when looking for recorded message

reset() None

Reset the list

start() None

Start recording messages

stop() None

Stop recording messages

# Example with pytest

# in conftest.py
from cli_ui.tests import MessageRecorder
import pytest

@pytest.fixture()
def message_recorder():
    message_recorder = MessageRecorder()
    message_recorder.start()
    yield message_recorder
    message_recorder.stop()


# in foo.py
def foo():
    cli_ui.info("Doing foo stuff")


# in test_foo.py
def test_foo(message_recorder):
     foo()
     assert message_recorder.find("foo stuff")

Using cli-ui in concurrent programs

Thanks to the GIL, it’s usually fine to use cli-ui in concurrent contexts.

However, be careful when using code like this:

cli_ui.info("Some thing that starts here", end=" ")
cli_ui.info("and ends there")

It’s possible for a race condition to occur that would clutter standard output if some other code prints anything between to two cli_ui.info calls.

One way to fix the problems is to use locks. You can see examples of this in the examples/ folder of the repository of this project.