DEV Community

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

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

bowmanjd profile image Jonathan Bowman ・4 min read

The Python Fire package provides a way to develop a command line interface (CLI) in Python, automatically, with very minimal extra code. The package comes from Google but is "not an official Google product".

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.

I have also documented using Poetry with Click, another Python package for CLI generation. Where appropriate, the Poetry setup information in this article is quite similar to the Poetry setup in that one.

Create the project and add a module

poetry new --name greet --src firegreet
cd firegreet
Enter fullscreen mode Exit fullscreen mode

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 Python Fire, so they both should be added now:

poetry add arrow fire
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.

Once I find a good name, I use it for everything. That what my son Darryl says, anyway. His brother Darryl agrees.

Now that the script is set up, install the package and script with

poetry install
Enter fullscreen mode Exit fullscreen mode

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

The best laid plans...

Using [Fire] to parse command line arguments

To make this work, we need to define command line arguments and pass those as parameters to the greet function.

This is exactly what Python Fire can do automatically. However, we do need to adjust our strategy.

First, import fire and add a simple command processing function to the code. The result should read something like this:

"""Send greetings."""

import time

import arrow
import fire


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")


def cli():
    fire.Fire(greet)
Enter fullscreen mode Exit fullscreen mode

That cli() function is all we added, other than import fire.

The cli() function, not greet(), needs to be the script end point, so pyproject.toml should have a minor adjustment:

[tool.poetry.scripts]
greet = "greet.greet:cli"
Enter fullscreen mode Exit fullscreen mode

Now, try poetry run greet --help

Yeah. All that for almost no work. This is how Python Fire shines: laziness yields such elegance.

$ poetry run greet -r 2 -i 1 Canada/Saskatchewan
Hello, Saskatchewan!
The time is 4:11 am and 43 seconds.

Hello, Saskatchewan!
The time is 4:11 am and 44 seconds.
Enter fullscreen mode Exit fullscreen mode

Now that we have tested it, we know it works. Wait... that was not real testing. Enter pytest.

Testing Python Fire interfaces with pytest

Testing command line interfaces takes a bit of thought. Not surprisingly, with pytest and Python Fire, I can continue to be lazy.

The fire.Fire() function is also used in testing. We can put the following in tests/test_greet.py:

import fire

from greet.greet import greet


def test_greet_cli(capsys):
    fire.Fire(greet, ["Egypt"])
    captured = capsys.readouterr()
    result = captured.out
    assert "Hello, Egypt!" in result
Enter fullscreen mode Exit fullscreen mode

As seen above, the fire.Fire() function can accept a list of command line arguments.

I used pytest's capsys fixture to capture the output.

Does poetry run pytest pass?

Satisfying.

Again, take a look at a similar tutorial involving Click if interested in comparing the tools.

Discussion (0)

pic
Editor guide