Sometimes, I get curious about a solved problem. Mundane things that we tend to think about from time to time - but not often enough to usually do anything about. When I get an itch about things I have a tendency to quickly prototype a tool to help with my understanding. This has two practical benefits:
- I learn about whatever it is I'm trying to solve by doing
- I get to practice coding on a problem that isn't related to work
Admittedly, the problems are rarely difficult; however, they are usually satisfying to solve. I have come up with a decent design pattern (read: unoriginal) that helps me implement these tools.
Anatomy of a CLI application
There's three parts to these little toy tools that I create.
- infrastructure
- parser
- driver
Infrastructure
This is a collection of parts that is general good practice for
Python
development. These are things that I would implement for Python
projects in general.
- pipx
- pipenv
- black
pipx
is a tool to install and run Python
applications in
isolated environments (sort of like brew
, npx
, or apt
). If you're
familiar with the Python
ecosphere then you've used pip
to install packages
before. pipx
utilizes pip
but it installs and manages packages that can be
run from the command line (packages like cowsay
, black
, or pipenv
for a
few examples).
pipenv
creates and manages virtual environments in order to assist with
dependency management in your projects. A typical Python
project would use a
combination of: pip
, venv
, and requirements.txt
in order to manage
dependencies and environment. pipenv
distills all of those components down
into a single tool. A major benefit is that you don't pollute your system
Python
with the packages and requirements of some arbitrary project. A virtual
environment is created specifically for the project you're working on. To top
it off, you get the benefit of dependency management so you know exactly
what's going in to the build.
black
is a very opinionated formatter for Python
. I'm going to be honest:
I think some of its opinions are ugly; however, I value the formatting
consistency over my personal preferences.
How do we use them together? Like so:
Install pipx
on your system
# Example is for Ubuntu/Debian
$ sudo apt-get install python3-pip
$ python3 -m pip install --user pipx
# Add the following line to your shell configuration file
$ export PATH=$PATH:~/.local/bin
Install pipenv
and black
via pipx
# Install packages with `pipx` to isolate it from the system python
$ pipx install pipenv
$ pipx install black
Install packages into the project environment (pipenv
reads a file called
Pipfile
)
>$ pipenv install
An example Pipfile
:
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[requires]
python_version = "3.8"
[dev-packages]
pylint = "*"
That's it. Then when you want to run your project in your virtual environment that has all its dependencies already installed you might do the following:
>$ pipenv run foo.py
Parser
The CLI parsing library that I have a preference for in Python
is
argparse. I typically
implement a file that mirrors the name of the tool I'm implementing and that
really only contains the implementation of the CLI itself. So, we might have a
file called tool.py
that's implemented as follows:
#!/usr/bin/env python3
"""
Some CLI tool
"""
from argparse import ArgumentParser
from driver import Driver
__author__ = "agraham"
__name__ = "main"
def process_arguments():
"""
Process command line arguments
:return: parsed_arguments
:rtype: obj
"""
parser = ArgumentParser(prog="tool")
parser.add_argument(
"-v",
"--version",
action="version",
version="%(prog)s 0.1.0",
help="Show application version",
)
parser.add_argument(
"-u",
"--user",
action="store",
dest="user",
type=str,
required=True,
help="Enter your username!"
)
parsed_args = parser.parse_args()
return parsed_args
if __name__ == "__main__":
# Receive arguments from the parser
ARGS = process_arguments()
# Instantiate the tool
DRIVER = Driver(ARGS)
# Execute the tool
DRIVER.tool_driver()
The script takes the results it gets from argparse
. We can see it defines two
arguments (in short and long forms) for version
and user
. A neat bonus to
using a library like argparse
is the addition of a built-in help:
$ pipenv run tool.py -h
usage: tool [-h] [-v] -u USER
required arguments:
-u, --user Enter your username!
optional arguments:
-h, --help show this help message and exit
-v, --version Show application version
This part parses and bundles up the arguments received by the parser and passes
them on to the next component, which is something I call the driver
.
Driver
The driver
is where the the actual implementation of the tool lives. It
carries out its tasks based on the information it receives from the parser
.
So, for our token example, we might have a file called driver.py
that looks
like so:
"""
Implements the main functionality of tool
"""
__author__ = "agraham"
class Driver:
"""
This class implements tool
"""
def __init__(self, args):
self.username = args.user
def pytax_driver(self):
"""
Main driver for pytax
:return: None
"""
self.hello()
def hello(self):
"""
Say hello to the user
"""
print(f"Hello {self.user}!")
Running it together we'd get something like:
$ pipenv run tool.py -u agraham
Hello agraham!
Contrived, yeah?
Examples
The following projects, pymort
and pytax
, are two simple CLI tools that I
wrote that utilize the paradigm described above. You can check them out on my
gitlab.
pymort
pymort is a tool to help visualize a debt amortization schedule.
pytax
pytax is a tool to help visualize and calculate your marginal tax schedule.