
In this short article, I will go over how to build a completely dynamic Python cli; which will also be self documenting right from a class. For this, I will combine and usepython-fire, prompt_toolkit ,argparse and docstring_parser. Here is the asciinema for this. Once we write the skeleton for our cli, we never have to go back to it again! Every new method we add to our core class will automatically be picked up by the cli along with documentation.
So why these three libraries? Well, they each provide unique functionalities that can be combined together to build really awesome cli’s!
python-fire: Allows one to build a cli from any python object type including functions, classes etc, but lacks autocompletion support without sourcing a bash or fish shell script.prompt_toolkit: Allows one to build really beautiful cli’s which supports input validation, colors, autocomplete etc.argparse: Python built in. It is not really necessary for this blog, but I wanted to demonstrate how to combine them.
What are some of the benefits of combining these four libraries?
- Get fuzzy completion of from any class based python code
- Very scalable. In the future, if I wanted to add another method to an existing class, I do not have to do anything for the cli.
- Documentation provided as the user interacts with the app!
- Fully dynamic! Any changes to the core class is picked up by the cli without any further code necessary from me.
A few things to know before we get started:
- I am not a developer.
- We need to make sure to document our core code as best as we can. Without proper documentation,
docstring_parserwill fail to provide the data needed for dynamic help. I am using Google style docstring in my code. - I am using
return selfat the end of my class methods for the core code for method chaining
The core SearchFiles class
This is our core code. I will not explain this code in depth as it is super simple. The code structure is:
SearchFilesclass- Two methods in this class called
lsandgrep
Directory structure for the code is
├── cli.py
├── search
│ ├── __init__.py
│ └── lsgrep.pyLets build an awesome cli!
First, lets import all of your necessary dependencies. For this example, I am not using any kind of state management or input validations just to keep things simple.
Our imports
import inspect
import argparse
import fire
from docstring_parser import parse
from prompt_toolkit.completion import Completer, Completion, FuzzyCompleter
from prompt_toolkit import PromptSessionfrom search.lsgrep import SearchFiles
We will then define two global variables that we will use in other functions and class
possbile_options = dir(SearchFiles)
options = []The possible_options will hold all the methods that are available (we will filter out _ and __ methods later on)
Define a function to get docstrings from a method
What is get_optionss function does is simply loop over all the methods from a class, get their docs, and parse the docs with docstring-parser . What is specifically gets are the short description of the method, and gets any args that the method takes, and their descriptions as well. Finally, this function will return a dictionary with this data.
Create a custom completer for prompt_toolkit
In the gist, we can see a class called CustomCompleter . What this class is doing is reading the word that the user types in, checks to see if there is a possible match in the list of methods, and if there is a match, seek out its arguments and relevant documentation. This documentation is added to the meta data, and is not part of our completion strategy. Then it follows standard prompt_toolkit signature and yields the various options.
Patch fire.Fire
As seen in the gist, we need to create a function that will patch the fire.Fire instance on load. The reason why this patch is important is because fire by default as of version 0.2.1 does not treat default arguments of a method well. It expects the user to provide a separator; which for a dynamic cli is not really user friendly. By patching, we ensure that any method arguments are passed using the -- flag notation.
Lets put it all together!
Final gist link! This is a fairly simple function. I really do not need to use argparse here, but I am doing so for completions sake. We are capturing user input with argparse , and then adding any user selected options from prompt_toolkit to it. Finally, we pass this string to fire.Fire with our core class as the object, and the command argument as our command.
Now we have a fully functional dynamic python cli with fuzzy completion!