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.py
Add 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.now(tz)
friendly_time = now.format("h:mm a")
location = tz.split("/")[-1].replace("_"," ")
return f"Hello, {location}! The time is {friendly_time}."
def cli(args=None):
"""Process command line arguments."""
if not args:
args = sys.argv[1:]
tz = args[0]
print(greet(tz))
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 install
Execute 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_Ababa
Test 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"]
cli(args)
captured = capsys.readouterr()
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.run(["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.
Top comments (14)
Great article but I have a problem with the scripting part in pyproject.toml:
The location of my project folder contains a space and this results in an error:
The question is how to improve the scripting part in pyproject.toml.
Regards
Michael
Interesting. You are right; that space in
Macintosh SR0
is causing problems. What version of poetry are you using, and how did you install it?It looks like your issue was supposed to have been resolved by Poetry Issue 1774, which is why I ask.
Hi Jonathan,
I've detected another bug in conjunction with a space in the path:
Poetry Issue 3630
I've created a new issue:
Poetry Issue 3643
Seems to be insufficient automated testing during the development of poetry.
Good job finding the first issue, and logging the other! Are both resolved by #3631, or just the first?
Thx - I do my best to learn python so currently I can only help with bug reports :-)
#3631 doesn’t solve the shebang problem, therefore the bug report.
Hi Jonathan,
I saw your comment on GitHub. May be I have to check the consequences of renaming the mount paths on my Mac. Should be the practical solution of the problem.
What about a symlink?
ln -s /Volumes/Macintosh\ SR0/ /HD
or something similar? I am not a Mac expert; just pondering.The path is already linked:
poetry "sees" the path but ignores the symbolic link.
Michael, what do you think of these steps? Are they a decent, at least temporary, workaround?
You can adapt names, paths, etc to your liking.
In summary, the above creates a symlink to your users directory (call it
homeward
orhome
or whatever you like), then, instead of storing the virtual environment in.venv
in the project directory, sets it in poetry's global config to be in a directory of your choosing. Remove the existing.venv
in the project, reinstall the project, and it should use the new virtual environment without spaces.What do you think?
Hi Jonathan,
thx for your suggestion. The symbolic path exists already:
Create the virtualenv inside the project's root directory leads to:
Using the default virtualenv folder looks much better:
So may be this is the bug in poetry: the shebang line is not the symbolic one if the virtualenv folder is in the project's root directory.
That's well put.
Thanks, this was really great to catch up on poetry's features and usage.
Glad to hear it was useful! Thanks.
great article! unblocked me.