Build Command Line Tools with Python Poetry
· Updated
Categories: Python
Poetry is a robust and convenient tool for building Python projects. The article Getting Started with Python Poetry demonstrated this in simple terms.
Now, let’s add another layer: using Poetry to develop a simple command line interface.
Review: the project so far
The project structure looks like this:
pygreet/
├── README.rst
├── poetry.lock
├── pyproject.toml
├── src
│ └── greet
│ ├── __init__.py
│ └── location.py
└── tests
├── __init__.py
└── test_greet.pyAdd command line processing
In src/greet/location.py, add a function that processes command line arguments.
"""Send greetings."""
import arrow
import sys
def greet(tz):
"""Greet a location."""
now = arrow.()
friendly_time = now.("h:mm a")
location = tz.("/")[-1].("_"," ")
return f"Hello, {location}! The time is {friendly_time}."
def cli(args=None):
"""Process command line arguments."""
if not args:
args = sys.[1:]
tz =[0]
print(())
In this case, I just called the function “cli,” but that name is not sacred.
Why not just read sys.argv in the body of the function? Why pass it as a default parameter?
Because testing, which we will see in a bit.
Add script to pyproject.toml
In the pyproject.toml file, we will utilize a [tool.poetry.scripts] section. Add it now if it is not there already, and add a script variable pointing to the function we just wrote.
[tool.poetry.scripts]
greet = "greet.location:cli"
Does the nomenclature make sense? Basically, it can be read package.submodule:function. We have a package “greet” with a submodule (Python file) “location” with the command-processing function “cli”.
Re-install project
I had to install the project again to update the command entrypoints.
poetry installExecute shell and run command
Enter the Python virtual environment with
poetry shell
then try out the command we just built:
(pygreet-abcd1234-py3.8) $ greet Africa/Addis_Ababa
Hello, Addis Ababa! The time is 1:49 pm.
If you do not want to start a new shell, as above, you can also just run
poetry run greet Africa/Addis_AbabaTest the command processor with pytest
Always write tests. Because we wrote the cli() command processing function to accept an arbitrary list as arguments, this makes testing easy.
Let’s add a test_cli.py file in the tests directory:
from greet.location import cli
def test_greet_cli(capsys):
args = ["America/Argentina/San_Juan"]
()
captured = capsys.()
result = captured.out
assert "San Juan!" in result
The args list is basically a fabrication of the list normally provided by sys.argv.
The pytest fixture capsys allows capturing stdout, so we can test the output, even though the function had no return value (it only used print()).
Does it work?
poetry run pytest
Note that we could write a test using subprocess.run(), and that appears to work as well.
import subprocess
def test_greet_cli2():
result = subprocess.(["greet", "America/Costa_Rica"], capture_output=True)
assert b"Costa Rica!" in result.stdout
The command line parsing in this article is rudimentary at best. For a more flexible and robust interface, consider using packages such as Click, Fire, and the Python Standard Library’s own argparse.
You may appreciate my article on using Poetry with Click.
Happy developing.