In the Python ecosystem, there are many packages for CLI applications, both popular ones, like Click, and not so much.  The most common ones were considered in a 
previous article , but little-known, but no less interesting, will be shown here. 
      
        
        
        
      
    
      
        
        
        
      
    
      
        
        
        
      
    
      
        
        
        
      
      As in the first part, a console script for the todolib library will be written for each library in Python 3.7.  In addition to this, a trivial test with these fixtures will be written for each implementation: 
      
        
        
        
      
    
      
        
        
        
      
    @pytest.fixture(autouse=True) def db(monkeypatch): """ monkeypatch         ,         """ value = {"tasks": []} monkeypatch.setattr(todolib.TodoApp, "save", lambda _: ...) monkeypatch.setattr(todolib.TodoApp, "get_db", lambda _: value) return value @pytest.yield_fixture(autouse=True) def check(db): """      """ yield assert db["tasks"] and db["tasks"][0]["title"] == "test" # ,      EXPECTED = "Task 'test' created with number 1.\n"
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
 
      
        
        
        
      
      All source code is available in 
this repository . 
      
        
        
        
      
    
      
        
        
        
      
      cliff 
      
        
        
        
      
      Github 
      
        
        
        
      
      Documentation 
      
        
        
        
      
      Many have heard about OpenStack, an open source platform for IaaS.  Most of it is written in Python, including console utilities that have been repeating CLI functionality for a long time.  This continued until cliff, or Command Line Interface Formulation Framework, appeared as a common framework.  With it, Openstack developers combined packages like python-novaclient, python-swiftclient and python-keystoneclient into one 
openstack program. 
      
        
        
        
      
    
      
        
        
        
      
      Teams 
      
        
        
        
      
      The approach to declaring commands resembles cement and cleo: argparse as a parameter parser, and the commands themselves are created through the inheritance of the Command class.  However, there are small extensions to the Command class, such as Lister, which independently formats the data. 
      
        
        
        
      
    
      
        
        
        
      
      source  from cliff import command from cliff.lister import Lister class Command(command.Command): """Command with a parser shortcut.""" def get_parser(self, prog_name): parser = super().get_parser(prog_name) self.extend_parser(parser) return parser def extend_parser(self, parser): ... class Add(Command): """Add new task.""" def extend_parser(self, parser): parser.add_argument("title", help="Task title") def take_action(self, parsed_args): task = self.app.todoapp.add_task(parsed_args.title) print(task, "created with number", task.number, end=".\n") class Show(Lister, Command): """Show current tasks.""" def extend_parser(self, parser): parser.add_argument( "--show-done", action="store_true", help="Include done tasks" ) def take_action(self, parsed_args): tasks = self.app.todoapp.list_tasks(show_done=parsed_args.show_done) 
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
     
      
        
        
        
      
      Application and main 
      
        
        
        
      
    
      
        
        
        
      
      The application class has 
initialize_app and 
clean_up methods , in our case, they initialize the application and save the data. 
      
        
        
        
      
    
      
        
        
        
      
      source  from cliff import app from cliff.commandmanager import CommandManager from todolib import TodoApp, __version__ class App(app.App): def __init__(self): 
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
     
      
        
        
        
      
      Work examples 
      
        
        
        
      
    
      
        
        
        
      
     igor$ ./todo_cliff.py add "sell the old laptop" Using database file /home/igor/.local/share/todoapp/db.json Task 'sell the old laptop' created with number 0. Saving database to a file /home/igor/.local/share/todoapp/db.json
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      Logging out of the box!  And if you look 
under the hood , you can see that it was done in a clever way: info in stdout, and warning / error in stderr, and, if necessary, is disabled by the 
--quiet flag. 
      
        
        
        
      
    
      
        
        
        
      
     igor$ ./todo_cliff.py -q show +--------+----------------------+--------+ | Number | Title | Status | +--------+----------------------+--------+ | 1 | sell the old laptop | β | +--------+----------------------+--------+
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      As already mentioned, Lister formats the data, but the table is not limited to: 
      
        
        
        
      
    
      
        
        
        
      
     igor$ ./todo_cliff.py -q show -f json --noindent [{"Number": 0, "Title": "sell old laptop", "Status": "\u2718"}]
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      In addition to json and table, yaml and csv are available. 
      
        
        
        
      
    
      
        
        
        
      
      There is also a default hidden trace: 
      
        
        
        
      
    
      
        
        
        
      
     igor$ ./todo_cliff.py -q remove 3 No such task.
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      More available REPL and fuzzy search aka 
fuzzy search : 
      
        
        
        
      
    
      
        
        
        
      
     igor$ ./todo_cliff.py -q (todo_cliff) help Shell commands (type help %topic%): =================================== alias exit history py quit shell unalias edit help load pyscript set shortcuts Application commands (type help %topic%): ========================================= add complete done help remove show (todo_cliff) whow todo_cliff: 'whow' is not a todo_cliff command. See 'todo_cliff --help'. Did you mean one of these? show
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      Testing 
      
        
        
        
      
    
      
        
        
        
      
      It's simple: an App object is created and run () is also called, which returns an exit code. 
      
        
        
        
      
    
      
        
        
        
      
     def test_cliff(capsys): app = todo_cliff.App() code = app.run(["add", "test"]) assert code == 0 out, _ = capsys.readouterr() assert out == EXPECTED
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      Pros and cons 
      
        
        
        
      
    
      
        
        
        
      
      Pros: 
      
        
        
        
      
    
      
        
        
        
      
    -   Various amenities out of the box; 
 -   Developed by OpenStack; 
 -   Interactive mode; 
 -   Extensibility through setuptools entrypoint and CommandHook; 
 -   Sphinx plugin for auto documentation to CLI; 
 -   Command completion (bash only); 
 
      
        
        
        
      
      Minuses: 
      
        
        
        
      
    
      
        
        
        
      
    -   A small documentation, which basically consists of a detailed but single example; 
 
      
        
        
        
      
      Another bug was noticed: in case of an error when hiding the stack trace, exit code is always zero. 
      
        
        
        
      
    
      
        
        
        
      
      Plac 
      
        
        
        
      
      Github 
      
        
        
        
      
      Documentation 
      
        
        
        
      
    
      
        
        
        
      
      At first glance, Plac seems to be something like Fire, but 
in reality it is like Fire, which hides the same argparse and much more under the hood. 
      
        
        
        
      
    
      
        
        
        
      
      Plac follows, citing the documentation, "the ancient principle of the computer world: 
Programs should simply solve ordinary cases and the simple should remain simple, and the complex at the same time achievable ."  The author of the framework has been using Python for more than nine years and wrote it with the expectation of solving β99.9% of tasksβ. 
      
        
        
        
      
    
      
        
        
        
      
      Commands and main 
      
        
        
        
      
    
      
        
        
        
      
      Attention to annotations in the show and done methods: this is how Plac parses the parameters and argument help, respectively. 
      
        
        
        
      
    
      
        
        
        
      
      source  import plac import todolib class TodoInterface: commands = "add", "show", "done", "remove" def __init__(self): self.app = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.app.save() def add(self, task): """ Add new task. """ task = self.app.add_task(title=task) print(task, "created with number", task.number, end=".\n") def show(self, show_done: plac.Annotation("Include done tasks", kind="flag")): """ Show current tasks. """ self.app.print_tasks(show_done=show_done) def done(self, number: "Task number"): """ Mark task as done. """ task = self.app.task_done(number=int(number)) print(task, "marked as done.") def remove(self, number: "Task number"): """ Remove task from the list. """ task = self.app.remove_task(number=int(number)) print(task, "removed from the list.") if __name__ == "__main__": plac.Interpreter.call(TodoInterface)
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
     
      
        
        
        
      
      Testing 
      
        
        
        
      
    
      
        
        
        
      
      Unfortunately, testing exit code Plac does not allow.  But testing itself is real: 
      
        
        
        
      
    
      
        
        
        
      
     def test_plac(capsys): plac.Interpreter.call(todo_plac.TodoInterface, arglist=["add", "test"]) out, _ = capsys.readouterr() assert out == EXPECTED
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      Pros and cons 
      
        
        
        
      
    
      
        
        
        
      
      Pros: 
      
        
        
        
      
    
      
        
        
        
      
    -   Simple use; 
 -   Interactive mode with readline support; 
 -   Stable API 
 -   Excellent documentation; 
 
      
        
        
        
      
      But the most interesting thing about Plac is hidden in 
advanced usage : 
      
        
        
        
      
    
      
        
        
        
      
    -   Execution of several commands in threads and subprocesses; 
 -   Parallel computing; 
 -   telnet server; 
 
      
        
        
        
      
      Minuses: 
      
        
        
        
      
    
      
        
        
        
      
    -   You cannot test exit code; 
 -   Poor project life. 
 
      
        
        
        
      
      Plumbum 
      
        
        
        
      
      Github 
      
        
        
        
      
      Documentation 
      
        
        
        
      
      CLI Documentation 
      
        
        
        
      
      Plumbum, in fact, is not such a little-known framework - almost 2000 stars, and there is something to love for it, because, roughly speaking, it implements the UNIX Shell syntax.  Well, with additives: 
      
        
        
        
      
    
      
        
        
        
      
     >>> from plumbum import local >>> output = local["ls"]() >>> output.split("\n")[:3] ['console_examples.egg-info', '__pycache__', 'readme.md'] 
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      Commands and main 
      
        
        
        
      
    
      
        
        
        
      
      Plumbum also has tools for the CLI, and not to say that they are just an addition: there are nargs, commands, and colors: 
      
        
        
        
      
    
      
        
        
        
      
      source  from plumbum import cli, colors class App(cli.Application): """Todo notes on plumbum.""" VERSION = todolib.__version__ verbosity = cli.CountOf("-v", help="Increase verbosity") def main(self, *args): if args: print(colors.red | f"Unknown command: {args[0]!r}.") return 1 if not self.nested_command: 
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
     
      
        
        
        
      
      Testing 
      
        
        
        
      
    
      
        
        
        
      
      Testing applications on Plumbum differs from others, except that the need to pass also the name of the application, i.e.  first argument: 
      
        
        
        
      
    
      
        
        
        
      
     def test_plumbum(capsys): _, code = todo_plumbum.App.run(["todo_plumbum", "add", "test"], exit=False) assert code == 0 out, _ = capsys.readouterr() assert out == "Task test created with number 0.\n"
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      Pros and cons 
      
        
        
        
      
    
      
        
        
        
      
      Pros: 
      
        
        
        
      
    
      
        
        
        
      
    -   Great toolkit for working with external teams; 
 -   Support for styles and colors; 
 -   Stable API 
 -   Active life of the project; 
 
      
        
        
        
      
      No flaws were noticed. 
      
        
        
        
      
    
      
        
        
        
      
      cmd2 
      
        
        
        
      
      Github 
      
        
        
        
      
      Documentation 
      
        
        
        
      
    
      
        
        
        
      
      First of all, cmd2 is an extension over 
cmd from the standard library, 
      
        
        
        
      
      those.  It is intended for interactive applications.  Nevertheless, it is included in the review, since it can also be configured for normal CLI mode. 
      
        
        
        
      
    
      
        
        
        
      
      Commands and main 
      
        
        
        
      
    
      
        
        
        
      
      cmd2 requires a certain rule: the commands must start with the 
do_ prefix, but otherwise everything is clear: 
      
        
        
        
      
    
      
        
        
        
      
      interactive mode  import cmd2 import todolib class App(cmd2.Cmd): def __init__(self, **kwargs): super().__init__(**kwargs) self.todoapp = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.todoapp.save() def do_add(self, title): """Add new task.""" task = self.todoapp.add_task(str(title)) self.poutput(f"{task} created with number {task.number}.") def do_show(self, show_done): """Show current tasks.""" self.todoapp.print_tasks(bool(show_done)) def do_done(self, number): """Mark task as done.""" task = self.todoapp.task_done(int(number)) self.poutput(f"{task} marked as done.") def do_remove(self, number): """Remove task from the list.""" task = self.todoapp.remove_task(int(number)) self.poutput(f"{task} removed from the list.") def main(**kwargs): with App(**kwargs) as app: app.cmdloop() if __name__ == '__main__': main()
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
     
      
        
        
        
      
      non-interactive mode   Normal mode requires a little extra movement. 
      
        
        
        
      
    
      
        
        
        
      
      For example, you have to go back to argparse and write the logic for the case when the script is called without parameters.  And now the commands get 
argparse.Namespace . 
      
        
        
        
      
    
      
        
        
        
      
      The parser is taken from the argparse example with minor additions - now sub-parsers are attributes of the main 
ArgumentParser . 
      
        
        
        
      
    
      
        
        
        
      
     import cmd2 from todo_argparse import get_parser parser = get_parser(progname="todo_cmd2_cli") class App(cmd2.Cmd): def __init__(self, **kwargs): super().__init__(**kwargs) self.todoapp = todolib.TodoApp.fromenv() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.todoapp.save() def do_add(self, args): """Add new task.""" task = self.todoapp.add_task(args.title) self.poutput(f"{task} created with number {task.number}.") def do_show(self, args): """Show current tasks.""" self.todoapp.print_tasks(args.show_done) def do_done(self, args): """Mark task as done.""" task = self.todoapp.task_done(args.number) self.poutput(f"{task} marked as done.") def do_remove(self, args): """Remove task from the list.""" task = self.todoapp.remove_task(args.number) self.poutput(f"{task} removed from the list.") parser.add.set_defaults(func=do_add) parser.show.set_defaults(func=do_show) parser.done.set_defaults(func=do_done) parser.remove.set_defaults(func=do_remove) @cmd2.with_argparser(parser) def do_base(self, args): func = getattr(args, "func", None) if func: func(self, args) else: print("No command provided.") print("Call with --help to get available commands.") def main(argv=None): with App() as app: app.do_base(argv or sys.argv[1:]) if __name__ == '__main__': main()
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
     
      
        
        
        
      
      Testing 
      
        
        
        
      
    
      
        
        
        
      
      Only an interactive script will be tested. 
      
        
        
        
      
    
      
        
        
        
      
      Since testing interactive applications requires a large number of human and time resources, cmd2 developers tried to solve this problem with the help of 
transcriptions - text files with examples of input and expected output.  For example: 
      
        
        
        
      
    
      
        
        
        
      
     (Cmd) add test Task 'test' created with number 0.
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      Thus, all that is required is to transfer the list of files with transcriptions to the 
App : 
      
        
        
        
      
    
      
        
        
        
      
     def test_cmd2(): todo_cmd2.main(transcript_files=["tests/transcript.txt"])
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      Pros and cons 
      
        
        
        
      
    
      
        
        
        
      
      Pros: 
      
        
        
        
      
    
      
        
        
        
      
    -   Good API for interactive applications; 
 -   Actively developing in recent years; 
 -   An original approach to testing; 
 -   Good documentation. 
 
      
        
        
        
      
      Minuses: 
      
        
        
        
      
    
      
        
        
        
      
    -   Requires argparse and additional code when writing non-interactive applications; 
 -   Unstable API; 
 -   In some places the documentation is empty. 
 
      
        
        
        
      
      Bonus: Urwid 
      
        
        
        
      
      Github 
      
        
        
        
      
      Documentation 
      
        
        
        
      
      Tutorial 
      
        
        
        
      
      Program Examples 
      
        
        
        
      
    
      
        
        
        
      
      Urwid is a framework from a slightly different world - from curses and npyscreen, that is, from the Console / Terminal UI.  Nevertheless, it is included in the review as a bonus, since, in my opinion, it deserves attention. 
      
        
        
        
      
    
      
        
        
        
      
      App and teams 
      
        
        
        
      
    
      
        
        
        
      
      Urwid has a large number of widgets, but it does not have concepts like a window or simple tools for accessing neighboring widgets.  Thus, if you want to get a beautiful result, you will need thoughtful design and / or use of other packages, otherwise you will have to transfer data in the attributes of the buttons, as here: 
      
        
        
        
      
    
      
        
        
        
      
      source  import urwid from urwid import Button import todolib class App(urwid.WidgetPlaceholder): max_box_levels = 4 def __init__(self): super().__init__(urwid.SolidFill()) self.todoapp = None self.box_level = 0 def __enter__(self): self.todoapp = todolib.TodoApp.fromenv() self.new_menu( "Todo notes on urwid", 
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
     
      
        
        
        
      
      teams  app = App() def menu(title, *items) -> urwid.ListBox: body = [urwid.Text(title), urwid.Divider()] body.extend(items) return urwid.ListBox(urwid.SimpleFocusListWalker(body)) def add(button): edit = urwid.Edit("Title: ") def handle(button): text = edit.edit_text app.todoapp.add_task(text) app.popup("Task added") app.new_menu("New task", edit, Button("Add", on_press=handle)) def list_tasks(button): tasks = app.todoapp.list_tasks(show_done=True) buttons = [] for task in tasks: status = "done" if task.done else "not done" text = f"{task.title} [{status}]" 
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
     
      
        
        
        
      
      main 
      
        
        
        
      
     if __name__ == "__main__": try: with app: urwid.MainLoop(app).run() except KeyboardInterrupt: pass
      
      
        
        
        
      
    
        
        
        
      
      
        
        
        
      
    
     
      
        
        
        
      
      Pros and cons 
      
        
        
        
      
    
      
        
        
        
      
      Pros: 
      
        
        
        
      
    
      
        
        
        
      
    -   Great API for writing different TUI applications; 
 -   Long development history (since 2010) and stable API; 
 -   Competent architecture; 
 -   Good documentation, there are examples. 
 
      
        
        
        
      
      Minuses: 
      
        
        
        
      
    
      
        
        
        
      
    -   How to test is unclear.  Only tmux send-keys comes to mind; 
 -   Uninformative errors when widgets are not properly arranged. 
 
      
        
        
        
      
      * * * 
      
        
        
        
      
      Cliff is a lot like Cleo and Cement and is generally good for large projects. 
      
        
        
        
      
    
      
        
        
        
      
      I personally would not dare to use Plac, but I recommend reading the source code. 
      
        
        
        
      
    
      
        
        
        
      
      Plumbum has a convenient CLI toolkit and a brilliant API for executing other commands, so if you are rewriting shell scripts in Python, then this is what you need. 
      
        
        
        
      
      cmd2 works well as a basis for interactive applications and for those who want to migrate from the standard cmd. 
      
        
        
        
      
    
      
        
        
        
      
      And Urwid features beautiful and user-friendly console applications. 
      
        
        
        
      
    
      
        
        
        
      
      The following packages were not included in the review: 
      
        
        
        
      
    
      
        
        
        
      
    -   aioconsole - no non-interactive mode; 
 -   pyCLI - no support for subcommands; 
 -   Clint - no support for subcommands, repository in the archive; 
 -   commandline - itβs too old (last release in 2009) and uninteresting; 
 -   CLIArgs - old (last release in 2010) 
 -   opterator - relatively old (last release in 2015)