Philippe Hebert
Philippe Hebert

Reputation: 2028

How to package a Python wheel to be executable as cli using poetry?

Problem

I would like to distribute a python package using poetry and make it executable as a cli, just like black, pipenv, poetry, flake8 and friends.

Example usage would be:

python -m my-package [args]

Configuration

So far I have been succesful in building a wheel and installing it on a docker image using the following configuration:

pyproject.toml

[tool.poetry]
name = "my-package
version = "0.1.0"
description = "A cli tool"
authors = []
packages = [
    { include = "src/main.py" },
]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

Build commands:

poetry build -f wheel
poetry export -o dist/requirements.txt

Docker image:

FROM python:3.8-slim as base

COPY dist/requirements.txt /requirements.txt
RUN pip install --no-cache-dir --upgrade -r /requirements.txt
ARG APP_VERSION
COPY dist/my-package-$APP_VERSION-py3-none-any.whl .
RUN pip install --no-cache-dir --no-deps my-package-$APP_VERSION-py3-none-any.whl

Built using the following command:

docker build -t my-package:latest --build-arg APP_VERSION=$(poetry version -s) .

And run using the following command:

docker run -it my-package:latest /usr/local/bin/python -m my-package

Which results in:

/usr/local/bin/python: No module named my-package

While locally I can execute it by doing:

cd src
python main.py [args]

Concluding question

How can I install/package a wheel to make it executable as a cli?

EDIT

Additional logs:

╰─ docker build -t my-package:latest --build-arg APP_VERSION=0.1.0 .
[+] Building 1.9s (10/10) FINISHED                                                                                                                                                                                   
 => [internal] load build definition from Dockerfile                                                                        0.0s
 => => transferring dockerfile: 353B                                                                                        0.0s
 => [internal] load .dockerignore                                                                                           0.0s
 => => transferring context: 2B                                                                                             0.0s
 => [internal] load metadata for docker.io/library/python:3.8-slim                                                          0.9s
 => [1/5] FROM docker.io/library/python:3.8-slim@sha256:0f6d6953c6612786ed05aaf1de7151dbbb0cea6bc83687040d5f15377be7ef64    0.0s
 => [internal] load build context                                                                                           0.0s
 => => transferring context: 6.12kB                                                                                         0.0s
 => CACHED [2/5] COPY dist/requirements.txt /requirements.txt                                                               0.0s
 => CACHED [3/5] RUN pip install --no-cache-dir --upgrade -r /requirements.txt                                              0.0s
 => CACHED [4/5] COPY dist/my-package-0.1.0-py3-none-any.whl .                                                              0.0s
 => [5/5] RUN pip install --user --no-cache-dir --no-deps my-package-0.1.0-py3-none-any.whl                                 0.9s
 => exporting to image                                                                                                      0.0s
 => => exporting layers                                                                                                     0.0s
 => => writing image sha256:4f41ba75b6bbacd33d64a06eed921c1ca8aeca55e9340536b9208fcb694826dd                                0.0s 
 => => naming to my-package:latest                                                                                          0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
'package' took 2.9720 sec


╰─ docker run -it my-package:latest /bin/bash
root@f61d2505e762:/# python -m  my-package
/usr/local/bin/python: No module named  my-package
root@f61d2505e762:/# python -m my-package
/usr/local/bin/python: No module named my-package
root@f61d2505e762:/# pip freeze > req.txt
root@f61d2505e762:/# cat req.txt
certifi==2021.10.8
charset-normalizer==2.0.9
idna==3.3
my-package @ file:///my-package-0.1.0-py3-none-any.whl
psycopg2-binary==2.9.2
requests==2.26.0
urllib3==1.26.7

Upvotes: 8

Views: 11234

Answers (1)

finswimmer
finswimmer

Reputation: 15202

Running a python application via python -m app uses a different mechanism compared to having a command app.

Let's assume the following project structure:

demo-cli
├── demo_cli
│   ├── __init__.py
│   ├── __main__.py
│   └── cli.py
├── poetry.lock
└── pyproject.toml

pyproject.toml:

[tool.poetry]
name = "demo-cli"
version = "0.1.0"
description = ""
authors = ["finswimmer <[email protected]>"]
packages = [{include = "demo_cli"}]

[tool.poetry.dependencies]
python = "^3.10"

[tool.poetry.scripts]
demo-cli = "demo_cli.cli:say_hello"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

cli.py:

def say_hello():
    print("Hello")

__main__.py:

from demo_cli import cli

cli.say_hello()

This part in the pyproject.toml give us a command demo-cli by defining the method which should be executed:

[tool.poetry.scripts]
demo-cli = "demo_cli.cli:say_hello"

The content of the __main__.py is triggered on a python -m demo_cli. So we need to add the method call there. More detailed information can be found here.

Upvotes: 6

Related Questions