Build configurable CLI tools easily in Python

Framework capabilities

  • Easy addition of loosely coupled subcommands
  • Normalized access to configuration files
  • Standardized use of stdin, stdout, and stderr
  • Plugin-type system for handling alternate UIs (such as curses or even a web UI)
  • Simple line editor with completion support for user input
  • Abstracts some of the argparse complexity
  • Applies conventions to application code structure
  • Supports test-driven development and CICD

Logo by Freepik-Flaticon

Approach

WizLib wraps the built-in ArgumentParser with a set of functions, classes, and conventions.

Commands exist independently. To add a new command, simply add a Python file in the command directory with a class definition that inherits from the base command. The command will automatically appear as an option in usage, and the implementation has access to handlers for arguments, inputs, user interfaces, and values from a configuration file for the application.

A WizLib application has the following directory structure at a mimimum. In this case, the app is called Sample with the main command sample and one subcommand doit.

sample
 ├─ .git
 └─ sample
     ├─ __init__.py
     ├─ __main__.py
     └─ command
         ├─ __init__.py
         └─ doit_command.py

API

WizLib itself defines several Python classes and functions for inclusion in projects. They include:

  • WizApp - Base class for a WizLib app
  • Command - Root class for the app-specific command class, which forms the base class for other commands
  • ConfigHandler - handles configuration, either through environment variables or a YAML configuration file
  • StreamHandler - simplifies handling of input via stdin for non-tty inputs such as pipes
  • ClassFamily - a primitive class that loads all subclasses in a directory into a "family" which can be queried a lookup, avoiding the need to include or reference every member of the family independently
  • SuperWrapper - a primitive class that "wraps" subclass methods, so that the superclass method gets calls before and after the subclass method - like an inversion of super()