Build a User-Friendly CLI from Pure Python Functions

Shahriyar Rzayev
5 min readMar 3, 2022
Greenpass QR-code

DynaCLI (Dynamic CLI) is a cloud-friendly, open source library for converting pure Python functions into Linux Shell commands. This article explains how DynaCLI makes writing Command Line Interfaces in Python easy and efficient, using as an example a function to generate a QR code that records a person’s vaccination status.

This is a continuation of the article How to Write User-friendly Command Line Interfaces in Python, which describes how to use different Python libraries like argparse, Click, Typer, docopt, and Fire to build CLI applications. To understand the motivations and use cases for DynaCLI, read the Medium interview. To learn the differences between DynaCLI and alternatives, refer to DynaCLI vs. Alternatives.

Motivation

The basic idea behind DynaCLI is to accelerate and automate the process of building CLI applications as much as possible by focusing solely on Python code. Function arguments are converted to CLI commands, and DynaCLI generates help messages from the Python function docstrings.

We’ll demonstrate this approach by generating a QR code that indicates a person’s vaccination status.

Sounds interesting? Let’s start exploring…

A conventional CLI building process

Building CLIs, in general, is a two-step process: first, write the core code and, second, develop the CLI predefined as a set of arguments, as shown below.

Let’s start by looking at the code from the original, reference article). In this snippet, they are first writing methods to implement the core code.

Following that is the code needed to build the CLI. This example uses the argparse library (code snippet from the referenced article):

CLI Designing with DynaCLI

With DynaCLI, we can skip the second part by designing our functions to be CLI-friendly. Functionally, the core logic is the same. To demonstrate the differences, we will update the original code as shown below.

Before that, just quickly install DynaCLI to get ready:
pip3 install dynacli

Restructuring

First of all, we would like to restructure the code. Thinking about the CLI design, there should be ./qr-code then green-badge feature-set (the actual Python package), which is for storing all commands, followed by generate to output the actual QR codes:

$ tree green_badge -I __pycache__green_badge
├── generate.py
└── __init__.py

From the original code, we know that the vaccine manufacturers are a limited set of companies; this kind of information is a good fit for type Enum.

Alternatively, we can use Literal. I am going to use Enum here instead of Literal. By choosing this approach, we add some defensive control on input data as Enum will automatically validate the data without any redundant custom validator.

Therefore, we do not need a manufacturer validation inside __post_init__ anymore and can remove it. Going further, the whole purpose of @dataclass here is validation, so we can replace this with TypedDict.

Our updated code generate.py file looks like this:

CLI Building

The second big step is to design the actual generate function in a way that it is going to accept all the necessary information as arguments:

Ideally, it should print a nice error message if one of the vaccination pairs contains wrong or duplicate date — but for the sake of simplicity, we just skip this step.

The major change is that we are going to accept the date and the manufacturer name as key-value pairs. This is quite intuitive, isn’t it?
The CLI call will look something like this:
./qr-code green-badge generate John Doe 1989-10-24 2021-01-01=pfizer 2021-06-01=pfizer

The next important difference is that DynaCLI populates help messages from the docstrings in the methods containing the explanation of the arguments. …You are right, this is quite Pythonic; if the explanations are already added once, why not use the same information to build the help messages in the CLI.

The rest of the code is the same — functionally the code logic is unchanged.

CLI Entrypoint

Now as the last step, we create the CLI entry point with DynaCLI. We have already provided the bootstrapper script, all you need is to provide the path for dynacli command:

$ dynacli init qr-code path=.Successfully created CLI entrypoint qr-code at /home/ssm-user/OSS/medium-articles/how_to_convert_python_functions/code

It will fill the qr-code script with some boilerplate, starter code, you need just change commented portions, in order to have a customized version of your CLI:

Change the commented sections, remove redundant comments and you are ready to go:

As you must have already noticed, there is no CLI pre-processing, adding arguments, version callback, etc. Everything is dead simple Python. DynaCLI grabs the version from __version__ and the CLI name from the `qr_code` docstring.

That’s it — the changes are done and we are ready to run the CLI.

Getting help output:

The green-badge help message comes from the package __init__.py file:

$ cat green_badge/__init__.py """
Generate Green Badge
"""
__version__ = "2.0"

Getting the version of the CLI itself:

$ ./qr-code --versionqr-code - v1.0

Now, let’s think about a different situation when you are porting an already developed package to be exposed by the CLI, and it has its own version set to v2.0. Should it mess with the CLI version? It is not ideal to have a single version for the package and the CLI.

With DynaCLI, you can version your packages and even modules. Again, it is quite Pythonic: just add __version__ to package __init__.py and to the generate.py module itself.

$ ./qr-code green-badge --versionqr-code green-badge - v2.0$ ./qr-code green-badge generate --versionqr-code green-badge generate - v3.0

DynaCLI detects generate.py and the generate function in it. Only public names are exposed by the CLI and the function docstring is used to register the help message.

Here, we would like to stress that, with DynaCLI, there is no need to begin writing things from scratch and redo the whole code. All you need is to import already existing functionality to an intermediate representation as we did ingenerate.py and register it in the CLI. This effectively conforms to the Open/Closed Principle, where your original code is closed to modification but is open to being extended via CLI.

Our final version of generate.py looks like:

You can get help about the command(it is our Python function in fact) using:

In summary, the highlights of using DynaCLI in this sample included:

  • We did not add or register any help messages — they were grabbed from the function docstrings.
  • DynaCLI detects **kwargs and registers them as <name>=<value> pair.
  • No special CLI pre-processing, adding arguments, or version callbacks were required.

Now, it is time to run the command and generate a QR code with the arguments provided:

That’s it. We have focused only on the core functionality, simplified basic features of a CLI application, and reshaped it to be more user-friendly. That’s how CLIs should be!

The source code: how_to_convert_python_functions/code

DynaCLI is an open source offering from BST LABS. Our goal is to make it easier for organizations to realize the full potential of cloud computing through a range of open source and commercial offerings. We are best known for CAIOS, the Cloud AI Operating System, a development platform featuring Infrastructure-from-Code technology. BST LABS is a software engineering unit of BlackSwan Technologies.

--

--

Shahriyar Rzayev

Azerbaijan Python User Group lead — True Pythonista. Open Source enthusiast.