DEV Community

Cover image for Build Command Line Tools with Python Poetry
Jonathan Bowman
Jonathan Bowman

Posted on • Edited on

Build Command Line Tools with Python Poetry

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

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

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

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

Execute shell and run command

Enter the Python virtual environment with

poetry shell
Enter fullscreen mode Exit fullscreen mode

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

If you do not want to start a new shell, as above, you can also just run

poetry run greet Africa/Addis_Ababa
Enter fullscreen mode Exit fullscreen mode

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

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

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

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)

Collapse
 
mklengel profile image
Michael Klengel • Edited

Great article but I have a problem with the scripting part in pyproject.toml:

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

The location of my project folder contains a space and this results in an error:

(.venv) ➜  pygreet greet
zsh: /Volumes/Macintosh SR0/Users/mklengel/Development/pygreet/.venv/bin/greet: bad interpreter: /Volumes/Macintosh: no such file or directory
Enter fullscreen mode Exit fullscreen mode

The question is how to improve the scripting part in pyproject.toml.

Regards
Michael

Collapse
 
bowmanjd profile image
Jonathan Bowman

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.

Collapse
 
mklengel profile image
Michael Klengel • Edited

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.

➜  ~ poetry --version
Poetry version 1.1.4
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
bowmanjd profile image
Jonathan Bowman

Good job finding the first issue, and logging the other! Are both resolved by #3631, or just the first?

Thread Thread
 
mklengel profile image
Michael Klengel • Edited

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.

Thread Thread
 
mklengel profile image
Michael Klengel • Edited

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.

Thread Thread
 
bowmanjd profile image
Jonathan Bowman

What about a symlink? ln -s /Volumes/Macintosh\ SR0/ /HD or something similar? I am not a Mac expert; just pondering.

Thread Thread
 
mklengel profile image
Michael Klengel

The path is already linked:

(.venv) ➜  ~ pwd
/Users/mklengel
(.venv) ➜  ~ ls -l Development 
lrwxr-xr-x  1 mklengel  staff  49 Jan 15 22:03 Development -> /Volumes/Macintosh SR0/Users/mklengel/Development
(.venv) ➜  ~ cd Development/pygreet 
(.venv) ➜  pygreet pwd
/Users/mklengel/Development/pygreet
(.venv) ➜  pygreet poetry shell
Virtual environment already activated: /Volumes/Macintosh SR0/Users/mklengel/Development/pygreet/.venv
Enter fullscreen mode Exit fullscreen mode

poetry "sees" the path but ignores the symbolic link.

Thread Thread
 
bowmanjd profile image
Jonathan Bowman

Michael, what do you think of these steps? Are they a decent, at least temporary, workaround?

sudo ln -s '/Volumes/Macintosh SR0/Users' /homeward
cd /homeward/mklengel/Development/pygreet/
rm -rf .venv
poetry config virtualenvs.in-project false
poetry config virtualenvs.path /homeward/mklengel/Development/.poetryvenv
poetry install
Enter fullscreen mode Exit fullscreen mode

You can adapt names, paths, etc to your liking.

In summary, the above creates a symlink to your users directory (call it homeward or home 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?

Thread Thread
 
mklengel profile image
Michael Klengel

Hi Jonathan,

thx for your suggestion. The symbolic path exists already:

➜  pygreet ls -ld $HOME/Development/pygreet
drwxr-xr-x  8 mklengel  staff  256  5 Feb 13:38 /Users/mklengel/Development/pygreet
Enter fullscreen mode Exit fullscreen mode

Create the virtualenv inside the project's root directory leads to:

➜  pygreet head -1 .venv/bin/greet
#!/Volumes/Macintosh SR0/Users/mklengel/Development/pygreet/.venv/bin/python
Enter fullscreen mode Exit fullscreen mode

Using the default virtualenv folder looks much better:

➜  pygreet poetry config virtualenvs.in-project false

➜  pygreet poetry config --list
cache-dir = "/Users/mklengel/Library/Caches/pypoetry"
experimental.new-installer = true
installer.parallel = true
virtualenvs.create = true
virtualenvs.in-project = false
virtualenvs.path = "{cache-dir}/virtualenvs"  # /Users/mklengel/Library/Caches/pypoetry/virtualenvs

➜  pygreet poetry install
Creating virtualenv greet-hlYPq_3g-py3.9 in /Users/mklengel/Library/Caches/pypoetry/virtualenvs
Installing dependencies from lock file

Package operations: 24 installs, 0 updates, 0 removals

  • Installing pyparsing (2.4.7)
  • Installing six (1.15.0)
  • Installing appdirs (1.4.4)
  • Installing attrs (20.3.0)
  • Installing click (7.1.2)
  • Installing mccabe (0.6.1)
  • Installing more-itertools (8.6.0)
  • Installing mypy-extensions (0.4.3)
  • Installing packaging (20.9)
  • Installing pathspec (0.8.1)
  • Installing pluggy (0.13.1)
  • Installing py (1.10.0)
  • Installing pycodestyle (2.6.0)
  • Installing pyflakes (2.2.0)
  • Installing python-dateutil (2.8.1)
  • Installing regex (2020.11.13)
  • Installing toml (0.10.2)
  • Installing typed-ast (1.4.2)
  • Installing typing-extensions (3.7.4.3)
  • Installing wcwidth (0.2.5)
  • Installing arrow (0.17.0)
  • Installing black (20.8b1)
  • Installing flake8 (3.8.4)
  • Installing pytest (5.4.3)

Installing the current project: greet (0.1.0)
➜  pygreet head -1 ~/Library/Caches/pypoetry/virtualenvs/greet-hlYPq_3g-py3.9/bin/greet
#!/Users/mklengel/Library/Caches/pypoetry/virtualenvs/greet-hlYPq_3g-py3.9/bin/python

➜  pygreet poetry shell
Spawning shell within /Users/mklengel/Library/Caches/pypoetry/virtualenvs/greet-hlYPq_3g-py3.9

➜  pygreet . /Users/mklengel/Library/Caches/pypoetry/virtualenvs/greet-hlYPq_3g-py3.9/bin/activate

(greet-hlYPq_3g-py3.9) ➜  pygreet greet Africa/Addis_Ababa
Hello, Addis Ababa! The time is 4:50 pm.
Enter fullscreen mode Exit fullscreen mode

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.

Thread Thread
 
bowmanjd profile image
Jonathan Bowman

That's well put.

Collapse
 
jamescarr profile image
James Carr

Thanks, this was really great to catch up on poetry's features and usage.

Collapse
 
bowmanjd profile image
Jonathan Bowman

Glad to hear it was useful! Thanks.

Collapse
 
jermwatt profile image
Jeremy Watt

great article! unblocked me.