Python packaging for idiots (me)
I have now reached the need to package a Python package and publish it through PyPI for the world (albeit a very small world) to have unfettered access to it. The package is called "Metapype", and its purpose is to simplify generating and managing scientific metadata. This post, however, is not about Metapype; rather, it is about the painful process (try and try again) I learned to package this Python package.
First off, I decided to be modern and package Metapype without invoking setup.py
. Instead, I am using PyPA's build
tool (pip install build
; and not yet in conda-forge
) to create both the "wheel" and the "sdist" packages, which uses configurations in pyproject.toml
, setupf.cfg
, and MANIFEST.in
files (more below). Using build
avoids the controversial use of setup.py
to install and create distributable packages - it still uses setuptools
, but does require the reflective execution of setup.py
. The bonus about using setup.cfg
is that modern pip
will read its attribute content just like setup.py
to perform a local installation of the package. Unfortunately, I must keep setup.py
alive and well (i.e., in parity with setup.cfg
) because pip
does not yet support "editable" installations using setup.cfg
. Oh well.
Sidenote: my experience indicates that build
(and I assume its use of setuptools
) preferentially uses setup.py
over setup.cfg
if both are in the same directory, which is causing an issue when pulling in the long_description
from README.md
. I've not yet figured this one out.
Pre-step
Remember always (and I emphasize "always") to remove lingering build cruft before attempting a new build. Why you ask? I don't know the details, but prior build content seems to get sucked into the new build and be there for the world to see. Make it a habit to clean up the package state before attempting a new build - this will save you grief and time:
rm -rf dist/ src/metapype.egg-info
Step 1: Bump Version
Once I have the local git state set correctly, I bump the Metapype version value in src/metapype/VERSION.txt
and perform the perfunctory git commit and push to GitHub. The file VERSION.txt
is a one-line file that contains the semantic version value (e.g., 1.4.0). This is picked up by setup.cfg
, setup.py
, and src/metapype/__init__.py
.
Step 2: Create Tag
With Metapype at the latest commit state and in the Metapype project root, I set a tag to the current version (as noted in src/metapype/VERSION.txt
) and push it to GitHub:
git tag -a v1.4.0 -m "Release with new evaluation"
git push origin v1.4.0
Step 3: Build Dist
With Metapype ready to be published and distributed through PyPI, the first thing I do is to navigate into the Metapype project root directory (where pyproject.toml
and setup.cfg
exist) using a new terminal that has the Conda tools
virtual environment activated (this is where the build
package is installed). Once in the project root, issue the following command (best to double-check the "Pre-step"):
python -m build
This will create the build
, dist
, and src/metapype/metapype.egg-info
artifacts. It is the dist
directory that contains the metapype-1.4.0-py3-none-any.whl
wheel and metapype-1.4.0.tar.gz
sdist packages used for publishing to PyPI.
Step 4: Publish to PyPI with Twine
The final step is to Publish the wheel and sdist packages to PyPI using twine
. Again, use the tools
virtual environment to execute twine
as follows:
twine upload --repository testpypi dist/*
for PyPI's test server
or
twine upload dist/*
for PyPI's production server.
Either case will require the credentials of the target user (Metapype will use the edi.repository
user id).
Note: Pypi no longer accepts login credentials for uploads. You must use an authentication token obtained from pypi.org for your project. See here for a user's perspective on uploading to pypi.org with an auth-token.
Step 5: Perform an installation into a test virtual environment
As a sanity check, it is best to perform an installation of Metapype into a test virtual environment just to make sure everything is working as expected:
pip install metapype
pyproject.toml
[build-system]
requires = [
"setuptools>=49.6.0",
"wheel"
]
build-backend = "setuptools.build_meta"
setup.cfg
[metadata]
name = metapype
version = file: src/metapype/VERSION.txt
author = Environmental Data Initiative
author_email = support@environmentaldatainitiative.org
description = Metapype: science metadata manipulation library
long_description = file: README.md
long_description_content_type = text/markdown
license_file = LICENSE
url = https://github.com/PASTAplus/metapype-eml
project_urls =
Bug Tracker = https://github.com/PASTAplus/metapype-eml/issues
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent
[options]
package_dir =
=src
packages = find:
include_package_data = True
python_requires = >=3.8
install_requires =
click==7.1.2
daiquiri==3.0.0
lxml==4.6.2
rfc3986==1.4.0
[options.packages.find]
where = src
include = metapype, metapype.eml, metapype.model
MANIFEST.in
include src/metapype/eml/rules.json
include src/metapype/VERSION.txt
global-include src/metapype/*.md