thoughts.sort()

Publish PyPi package

March 18, 2021

Tags: python, pip, pypi, projects

Publishing PyPi packages Being in the process of publishing my second PyPi-package, I take notes on the process for quicker turn-around in the future.

I usually start my projects very barebones, with a single directory and a main.py file:

$ tree
project-name
├── main.py
├── README.md
├── requirements.txt

After something useful has been hacked together, I start moving things around.

$ tree
project-name
├── LICENSE
├── README.md
├── data
│   └── example_data.txt
├── package_name
│   ├── __init__.py
│   └── package_name.py
├── requirements.txt
├── setup.py
└── tests

As an MVP, the important thing here is to move the main.py into a package directory and to create a setup.py.

The setup.py file looks something like this:

import setuptools

with open('README.md', 'r') as fh:
    long_description = fh.read()

setuptools.setup(
    name='project_name',
    version='0.0.1',
    author='Casper Lehmann',
    author_email='@.com',
    description='Project description',
    long_description=long_description,
    long_description_content_type='text/markdown',
    url='https://github.com/casperlehmann/project-name',
    packages=setuptools.find_packages(),
    classifiers=[
        'Programming Language :: Python :: 3',
        'License :: OSI Approved :: MIT License',
        'Operating System :: OS Independent',
    ],
    python_requires='>=3.6',
    install_requires=[
        'wheel',
    ],
)

To test the installation process, run:

$ python setup.py sdist bdist_wheel

A classic error to get on this one is:

$ python setup.py sdist bdist_wheel

usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
   or: setup.py --help [cmd1 cmd2 ...]
   or: setup.py --help-commands
   or: setup.py cmd --help

error: invalid command 'bdist_wheel'

The problem here is that the wheel package is not installed. Pip install it in your venv, or globally, if you like to YOLO.

$ pip install wheel
Collecting wheel
  Using cached wheel-0.36.2-py2.py3-none-any.whl (35 kB)
Installing collected packages: wheel
Successfully installed wheel-0.36.2

Run setup.py again, and you’ll get a dist-directory in your project. This contains all that needs to be uploaded.

Register with PyPi

https://pypi.org/account/register/

Prepare

$ pip install twine

Test

Upload to TestPyPi

$ twine upload --repository testpypi dist/*

Pip install with no-deps, since we cannot guarantee that TestPyPi has the same packages available as PyPi.

Note: This example uses —index-url flag to specify TestPyPI instead of live PyPI. Additionally, it specifies —no-deps. Since TestPyPI doesn’t have the same packages as the live PyPI, it’s possible that attempting to install dependencies may fail or install something unexpected. While our example package doesn’t have any dependencies, it’s a good practice to avoid installing dependencies when using TestPyPI.

https://readthedocs.org/projects/python-packaging-user-guide/downloads/pdf/latest/, p. 25

Try it in a new venv.

$ python -m venv .venv
$ pip install --no-deps -i https://test.pypi.org/simple/ project-name==0.0.1
$ python -m project_name

The last command will fail if there are missing packages, but that’s OK as long as our package installed.

Publish

$ twine upload dist/*

Install

$ pip install project_name

Adding command line script

Two steps need to be completed to install a package as a command line on your system.

Write a script that handles arguments, etc. Then specify the inclusion of this script in setup.py.

In setup.py add the following:

setuptools.setup(
    ...
    scripts = ['bin/script-name'],
)

Then create the file bin/script-name (no extension required), and write something like the following:

#!/usr/bin/env python

import argparse

from project_name import function_name

parser = argparse.ArgumentParser(description='Describe the functionality')

parser.add_argument('Path',
                    metavar='path',
                    type=str,
                    help='the path to some file')

args = parser.parse_args()
function_name.convert(args.Path)

Sources: