/ Bit by bit / posts

Python Distributions With CLI Scripts

Nov 23, 2021

Let’s create a “hello, world” distributable that creates a CLI executable, greet: first, some preparation:

$ mkdir hello-world
$ cd hello-world
$ python -m venv hello-world      # create a virtual environment, hello-world
$ hello-world/Scripts/activate    # activate the virtual environment
$ mkdir -p src/hello/scripts
$ touch src/hello/__init__.py

Create the greet function in src/hello/__init__.py:

1
2
def greet(name):
    print(f'Hello, {name}')

Nothing earth-shattering for our purpose here. Next, create the CLI script that calls greet when an argument is given on the command line:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# in src/hello/scripts/cli.py

from hello import greet
import sys

def main():
    if len(sys.argv) > 1:
        greet(sys.argv[1])
    else:
        print(f'Please specify a name')

Note the function name main: it’s an arbitrary name but one that setup.cfg1 references so build can generate the executable. Next, let’s create a minimal setup.cfg.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[metadata]
name = hello-world

[options]
package_dir = =src
packages = find:

[options.packages.find]
where = src

[options.entry_points]
console_scripts =
  greet = hello.scripts.cli:main

A few notable points:

Next, create pyproject.toml that build relies on to build the distributable:

1
2
3
[build-system]
requires = ["setuptools >= 42", "wheel"]
build-backend = "setuptools.build_meta"

Finally, it’s time to build the distributable:

$ pip install build       # installs the build tool
$ python -m build --wheel # builds only the wheel distribution

All’s done. To test this, create another virtual environment, install the distributable found in dist/ with pip; this generates the executable greet in the environment’s Scripts/ directory. Ignoring the generated artifacts, the directory should look like the following:

hello-world
├── pyproject.toml
├── setup.cfg
└── src
    └── hello
        ├── __init__.py
        └── scripts
            └── cli.py

  1. According to Configuring metadata, setup.cfg is the preferred format. ↩︎