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
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 Python Fire, so they both should be added now:
poetry add arrow fire
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.
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
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'
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)
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"
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.
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
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.
Top comments (1)
PythonFire is amazing !
Thanks for sharing