Build and Test a Command Line Interface with Python, Poetry, Click, and pytest
Jonathan Bowman
Posted on August 7, 2020
The Click package makes it easy to develop a pretty command line interface (CLI) for your Python project.
Poetry is a mature and modern way to manage a Python project and its dependencies. You might enjoy reading my introduction to Poetry as well as a brief explanation of using Poetry to expose command line scripts in your project.
It is possible to parse your own command line arguments through the list provided by sys.argv
, as we did in the previously mentioned Poetry/CLI article. However, packages like Click, Fire, and the Python Standard Library's own argparse provide such simplicity and efficiency, a roll-your-own solution simply doesn't make sense.
I have also documented using Poetry with Python Fire, another Python package for CLI generation. The Poetry setup has similarities.
Let's choose Click for this exercise, and pair it with Poetry for project maintenance.
Create the project and add a module
poetry new --name greet --src clickgreet
cd clickgreet
I add a file called greet.py
in the src/greet
subdirectory, with the following contents:
"""Send greetings."""
import time
import arrow
def greet(tz, repeat=1, interval=3):
"""Parse a timezone and greet a location a number of times."""
for i in range(repeat):
if i > 0: # no delay needed on first round
time.sleep(interval)
now = arrow.now(tz)
friendly_time = now.format("h:mm a")
seconds = now.format("s")
location = tz.split("/")[-1].replace("_"," ")
print(f"Hello, {location}!")
print(f"The time is {friendly_time} and {seconds} seconds.\n")
Install dependencies
We need arrow, and will be using click, so they should be added now:
poetry add arrow click
Add a script end point in pyproject.toml
To expose the greet
function as a command line script, add a tool.poetry.scripts
section to pyproject.toml
.
[tool.poetry.scripts]
greet = "greet.greet:greet"
That sets greet
the script to look in greet
the package for greet
the module and use greet
the function. Creative naming is not my strong point.
Now that the script is set up, install the package and script with
poetry install
Now let's run the newly installed script:
$ poetry run greet
Traceback (most recent call last):
File "<string>", line 1, in <module>
TypeError: greet() missing 1 required positional argument: 'tz'
I thought that went well.
Using Click to parse command line arguments
We need a way to parse command line arguments and pass those as parameters to the function. Click uses decorators to do this easily.
Here is the original function, now with appropriate decorators added:
"""Send greetings."""
import time
import arrow
import click
@click.command()
@click.argument("tz")
@click.option("--repeat", "-r", default=1, type=int)
@click.option("--interval", "-i", default=3, type=int)
def greet(tz, repeat=1, interval=3):
"""Parse a timezone and greet a location a number of times."""
for i in range(repeat):
if i > 0: # no delay needed on first round
time.sleep(interval)
now = arrow.now(tz)
friendly_time = now.format("h:mm a")
seconds = now.format("s")
location = tz.split("/")[-1].replace("_", " ")
print(f"Hello, {location}!")
print(f"The time is {friendly_time} and {seconds} seconds.\n")
The click.command
make the function a command line tool, while click.argument
specifies a positional argument tz
. Even though specified first in the above code, syntactically it will come after the options in the command itself. These options are specified with the two click.option
decorators. The type=int
arguments are not strictly necessary because the default values are integers which in effect infers the type. One would want them, however, if the default was not specified and the desired type was not string.
With these decorators, we have a working command line interface.
$ poetry run greet --help
Usage: greet [OPTIONS] TZ
Parse a timezone and greet a location a number of times.
Options:
-r, --repeat INTEGER
-i, --interval INTEGER
--help Show this message and exit.
$
$ poetry run greet -r 2 -i 1 Europe/Luxembourg
Hello, Luxembourg!
The time is 3:00 am and 36 seconds.
Hello, Luxembourg!
The time is 3:00 am and 37 seconds.
We have ourselves an incredibly useful and elegant command line tool.
Testing Click interfaces with pytest
Testing command line interfaces can demand a bit of creativity. Thankfully, Click provides CliRunner
, a command line runner for testing.
from click.testing import CliRunner
from greet.greet import greet
def test_greet_cli():
runner = CliRunner()
result = runner.invoke(greet, ["Europe/Istanbul"])
assert result.exit_code == 0
assert "Hello, Istanbul!" in result.output
Run the above with poetry run pytest
.
Does the test pass?
Happiness.
Again, take a look at a similar tutorial involving Python Fire if interested in comparing the tools.
Posted on August 7, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 7, 2020