4 Steps to Release a CLI in Python

Photo by Marc-Olivier Jodoin on Unsplash

This is what I learned from creating a Python CLI (digdaglog2sql) in a day.

In just 4 steps, you can release a CLI written in Python easily.

Create a project by using poetry

Poetry is a modern Python packaging and dependency management tool. Poetry is becoming popular and defacto rapidly.

By using Poetry, it enables us to manage package dependency, to create a project template, and to publish to PyPI.

To setup a project with Poetry, this article is the best to read even if you build a CLI.

https://future--architect-github-io.translate.goog/articles/20210611a/?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=ja&_x_tr_pto=wapp (originally written in Japanese)

One thing I added to my project is isort. isort is to sort imports automatically.

Here is the example of my project.

[tool.taskipy.tasks]
test = { cmd = "pytest tests", help = "runs all unit tests" }
pr_test = "task lint"
fmt = { cmd = "black tests digdaglog2sql && isort digdaglog2sql tests", help = "format code" }
lint = { cmd = "task lint_black && task lint_flake8 && task lint_isort && task lint_mypy", help = "exec lint" }
lint_flake8 = "flake8 --max-line-length=88 tests digdaglog2sql"
lint_mypy = "mypy tests digdaglog2sql"
lint_black = "black --check tests digdaglog2sql"
lint_isort = "isort digdaglog2sql tests --check-only"

Create a CLI with Click/Cloup

Click is a famous Python package to build a command line tool. You can easily create a CLI by using decorator.

Here is the example from the Click website:

import click

@click.command()
@click.option("--count", default=1, help="Number of greetings.")
@click.option("--name", prompt="Your name",
              help="The person to greet.")
def hello(count, name):
    """Simple program that greets NAME for a total of COUNT times."""
    for _ in range(count):
        click.echo("Hello, %s!" % name)

if __name__ == '__main__':
    hello()

Cloup is an extension of Click.

Using by Cloup, you can handle option groups and complex constraints like mutually_exclusive as:

@option_group(
    "Cool options",
    option('--foo', help='This text should describe the option --foo.'),
    option('--bar', help='This text should describe the option --bar.'),
    constraint=mutually_exclusive,
)

Constraints of Cloup can validate the dependency and it also renders constraints in help.

Use poetry-dynamic-versioning for version management

poetry-dynamic-versioning is a Python package to do same thing as setuptools-scm. You don’t need to write version number by hand since this package use the version from tag of Git, e.g., “v.0.1.0”.

Managing version by Git enables you to release to PyPI from GitHub Actions. This means you can release to PyPI on mobile device by releasing from GitHub.

After installation of poetry-dynamic-versioning, you just add three thing in pyproject.toml:

[tool.poetry]
version = "0.0.0"

[tool.poetry-dynamic-versioning]
enable = true

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

Note that build-system configuration may vary depending on how you install poetry-dynamic-versioning. See the document for detail.

Introduce GitHub Actions to release the package to PyPI

As I mentioned above, I highly recommend to use GitHub Actions to release a Package to PyPI.

Since GitHub provides Release notes generation feature now, creating a release from GitHub with triggering PyPI release is the best way to publish a new version.

Here is the snippet of GH Actions to release to PyPI by using poetry.

name: Upload Python Package

on:
  release:
    types: [created]

permissions:
  contents: read

jobs:
  deploy:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    steps:
    - uses: actions/checkout@v3
    - name: Set up Python
      uses: actions/setup-python@v3
      with:
        python-version: '3.x'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install poetry        
    - name: Build and publish package
      run: |
        poetry version $(git describe --tags --abbrev=0)
        poetry build
        poetry publish --username __token__ --password ${{ secrets.PYPI_API_TOKEN }}        

Note that, while PyPI API Token can be found on PyPI, if you need to create project scope token, you need to upload a package manually first.

Aki Ariga
Aki Ariga
Staff Software Engineer

Interested in Machine Learning, ML Ops, and Data driven business. If you like my blog post, I’m glad if you can buy me a tea 😉

  Gift a cup of Tea

Related