DEV Community

loading...
Cover image for Build and Test a Command Line Interface with Python, Poetry, Click, and pytest

Build and Test a Command Line Interface with Python, Poetry, Click, and pytest

bowmanjd profile image Jonathan Bowman ・4 min read

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

Install dependencies

We need arrow, and will be using click, so they should be added now:

poetry add arrow click
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Discussion

pic
Editor guide