Python CLI UI¶
Tools for nice user interfaces in the terminal.
Contents
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 printedquiet – Hide every message except
warning()
,error()
, andfatal()
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
andkwargs
are passed toinfo()
Current index should start at 0 and end at
n-1
, like inenumerate()
- 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 choicesChanged in version 0.8:
choices
is now a named keyword argumentChanged 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.