How can I create a command line tool and execute it like a script in Python

What I mean by this, is for example, I can download mypy with:

pip install -U mypy

Then, I can run mypy directly into the terminal with:

mypy <app>

How is this be done without needing to write the full python3 -m mypy.py <app>? E.g. if I wanted to convert a Python project made with poetry into a command line tool, without needing to specify python3 / .py?

​

This way non-Python users can invoke the tool without knowledge of Python, poetry, or virtual envs?

​

8 comments · 63% upvoted · other discussions

comments

POGtastic

Assuming that they have their environment set up properly, you add a line to your pyproject.toml that tells Poetry to create another Python script in the bin folder.

Suppose that I have the following structure for a project.

$ tree myecho
myecho
├── myecho
│  ├── __init__.py
│  └── main.py
├── pyproject.toml
├── README.md
└── tests
    └── __init__.py

The myecho package contains a main.py file as follows:

$ cat myecho/myecho/main.py
#!/usr/bin/env python

import sys

def main():
    print(*sys.argv[1:], sep=" ")
# I don't actually invoke this - that's for Poetry to do

My pyproject.toml file contains an entry that will tell it to produce an executable script that will invoke that main function.

$ cat myecho/pyproject.toml
...
[tool.poetry.scripts]
myecho = "myecho.main:main"

The name of the executable is the lvalue, and the string is <package>.<module>:<function>.


Building the project and installing the resulting .whl will put an executable script called myecho in the exact same location as pip. So, for example:

$ cd myecho
$ poetry build
Building myecho (0.1.0)
  - Building sdist
  - Built myecho-0.1.0.tar.gz
  - Building wheel
  - Built myecho-0.1.0-py3-none-any.whl
$ python -m venv ~/blarg
$ . ~/blarg/bin/activate
(blarg) $ pip install dist/myecho-0.1.0-py3-none-any.whl
Processing ./dist/myecho-0.1.0-py3-none-any.whl
Installing collected packages: myecho
Successfully installed myecho-0.1.0
(blarg) $ myecho foo bar baz
foo bar baz
(blarg) $ which myecho
/home/pog/blarg/bin/myecho

I'm using a virtualenv, but this will work with the system Python environment as well.

Solsticized [OP]

Thank you for your comprehensive reply! I had no idea about the [tool.poetry.scripts] part of poetry. I feel like a lot of these script entrypoints are pretty obscure in Python. Thanks again

Gshuri

When you create a package Python allows you to define entry points, which are scripts or executables that will be installed when you install the package.

How you define the entry points varies a little depending on which system you use to build your package. mypy uses setuptools so it defines the entry points in setup.py under the "console_scripts" section (see line 211 https://github.com/python/mypy/blob/master/setup.py#L211).

For poetry you need to define a scripts section in the pyproject.toml file (see https://python-poetry.org/docs/pyproject/#scripts).

However this still requires users to have some understanding of Python. At minimum they will need to be able to install Python on their computer and understand how to install a package using pip.

Solsticized [OP]

Ahh that makes sense. Thank you for your explanation. The whole idea of setuptools and poetry scripts is pretty obscure in Python

ofnuts

On Linux (and possibly OSX),

  1. You make the file "executable" (chmod +x myapp.py)
  2. You add a "shebang" in it. This is a first line in the file that starts with #! and contains the name of an executable to interpret the file: #! /usr/bin/python3
  3. You can rename the file to drop the .py extension (many editors will use the shebang to identify the file type if you need to edit it again)

You can also wrap it in a shell script (especially if you need a specific environment, etc...).

AFAIK on Windows, you can tell the Windows CLI to open the .py type with your installed Python interpreter (if you type any file name in the command prompt, Windows trie to "open" it with the registered application for the type).

POGtastic

Suggestion for the shebang line: Use /usr/bin/env python instead. The reason for this is that in a virtualenv, the environment and PATH get changed, so making the shebang line access the environment will use the venv's Python interpreter. Using an absolute path will result in always using the system's Python, which might not be what you want!

Example:

#! /usr/bin/python
import sys
print(sys.executable)

Running in the shell, both inside and outside a venv:

$ ./test.py
/usr/bin/python
$ python -m venv blarg
$ . blarg/bin/activate
(blarg) $ ./test.py
/usr/bin/python # oh no

By contrast, if we do the following:

#! /usr/bin/env python
import sys
print(sys.executable)

Now, when we have the venv activated, running the script as an executable will use the venv's Python interpreter instead.

$ ./test.py
/usr/bin/python
$ . blarg/bin/activate
(blarg) $ ./test.py
/home/pog/blarg/bin/python
SHKEVE

the best solutions i can think of are creating bash scripts or terminal aliases for this purpose.

if you’re distributing to others, you could look into making an executable with something like pyinstaller.

the_brown_cow

You can use pipx. I use it for black, mypy, pylint, ipython, httpie to name a few.

The only thing the user would need is Python, pip, and pipx.

pip install pipx —user pipx install mypy