~ 8 min read
October 2023 - Packaging a Python Program for PyPI: Tools, Tips + GitLab CI Config
Written by Brie Carranza
TL;DR | The tools that I recommend are bump2version and cookiecutter (with the cookiecutter-pylibrary template in particular) and the tips are βuse Test PyPIβ and βπ€·ββοΈ maybe thereβs no need to sign your PyPI packageβ.
π Hello, world!
Last month, I announced the π release of v1.0
of pastebin-bisque
, a Python program Iβve been working on since 2020. The v1.0
release included publishing pastebin-bisque
on PyPI. I wanted to take a moment to write about some of the most helpful tools and notes from my experience.
This is intended to be a quick blog post but hereβs an even quicker outline:
- π₯ The power of
cookiecutter
- AKA how to not write annoying or boring code and skip straight to the stuff you wanted to write in the first place
- π Umm,
bump2version
is awesome- A super useful tool for managing the process of releasing a new version of a package
- π§ͺ Use Test PyPI (
test.pypi.org
)- To make sure your package looks right before going live
- π On package signing
- β¦and why I declined
- π Plus: the full
.gitlab-ci.yml
that I use to- Run tests against Python 3.8-3.11 with
tox
- Build the package
- Upload the package to GitLab Container Registry, Test PyPI and PyPI
- Authenticate to (Test) PyPI with a token
- Install and run the package that is published to the registries above
- Run tests against Python 3.8-3.11 with
πΌ The Big Picture
A few words on the big picture, for context:
- In early 2020, I started pastebin-bisque with this commit on February 26.
- To use it, you did
pip install -r requirements.txt
and thenpython main.py
after cloning thegit
repo.
- To use it, you did
- In 2022,
pastebin-bisque
started getting some attention and I wanted to make the program easier to use and easier to develop and contribute to.- Plus, I always wanted to make a proper Python package on PyPI. (I didnβt want to do it just to do it; I wanted to have a good reason and I finally had one.)
- In September 2023, I refactored the code so that it could be installed via
pip install pastebin-bisque
and executed withpastebin-bisque
.- A significant part of that effort was moving the contents of
main.py
to a new home insrc/pastebin_bisque/cli.py
.
- A significant part of that effort was moving the contents of
This post is a quick(ish) list of the stuff I learned and the tools that were most helpful for me during this process. Iβm excluding the obvious stuff that is better explained elsewhere, like git
and tox
. I assume that the reader has some familiarity with writing Python.
π₯ The power of cookiecutter
I used cookiecutter
to save β± time. Specifically, I wanted to get to the exciting part and I didnβt want to write (boring) skeleton code.
So, cookiecutter
is:
A cross-platform command-line utility that creates projects from cookiecutters (project templates), e.g. Python package projects, C projects.
After inspecting several options, the specific cookiecutter
template that I used is cookiecutter-pylibrary and it took care a bunch of stuff including:
tox
for testingpastebin-bisque
against multiple versions of Python- Documentation with Sphinx β ready for use with ReadTheDocs
- Support for
bump2version
(keep readingβ¦)
The easiest way to demonstrate this might be to take a look at the output of tree
after generating the project with cookiecutter
.
cookiecutter gh:ionelmc/cookiecutter-pylibrary
The cookiecutter
command prompts one to answer quite a few questions. After accepting the defaults, the directory structure looks like this:
# tree python-cutestcat
python-cutestcat
βββ AUTHORS.rst
βββ CHANGELOG.rst
βββ CONTRIBUTING.rst
βββ LICENSE
βββ MANIFEST.in
βββ README.rst
βββ ci
β βββ bootstrap.py
β βββ requirements.txt
β βββ templates
βββ docs
β βββ authors.rst
β βββ changelog.rst
β βββ conf.py
β βββ contributing.rst
β βββ index.rst
β βββ installation.rst
β βββ readme.rst
β βββ reference
β β βββ cutestcat.rst
β β βββ index.rst
β βββ requirements.txt
β βββ spelling_wordlist.txt
β βββ usage.rst
βββ pyproject.toml
βββ pytest.ini
βββ setup.py
βββ src
β βββ cutestcat
β βββ __init__.py
β βββ __main__.py
β βββ cli.py
βββ tests
β βββ test_cutestcat.py
βββ tox.ini
8 directories, 28 files
π Cool! The emphasis was on editing src/cutestcat/cli.py
to start to add my code. Once the cookiecutter
invocation was complete, I had a pastebin-bisque
executable. I started bringing the imports and functions from main.py
into src/pastebin_bisque/cli.py
until the functionality of the original program was restored.
Effectively, this is the loop I used while actively developing:
while true ; do
vim src/cutestcat/cli.py ;
rm -f $(which cutestcat) && python setup.py install && cutestcat ;
done
The rm -f $(which cutestcat) && python setup.py install && cutestcat
loop removes the old executable, creates a new one based on my changes and runs it. I stayed in that loop until things worked the way I wanted or until I needed to take a break.
π Umm, bump2version
is awesome
The cookiecutter
prompts asked about tbump
or bump2version
. Iβd been interested in bump2version
already anyway so I chose it and itβs really nice. I would definitely recommend further adjusting .bumpversion.cfg
to customize things the way youβd like.
Youβd then use one of these commands depending on what kind of version youβd like to release:
bump2version major
bump2version minor
bump2version patch
If you followed along in the earlier section and want to test the release process, you might find this loop helpful:
pre-commit run --all-files && \
git commit -am"β¨ Use dependencies from PyPi" && \
bump2version patch && git push --tags
The .bumpversion.cfg
file contains the current_version
and has information about where to replace the current version with the new version in your project. Youβll see setup.py
and docs/conf.py
and files like that in .bumpversion.cfg
.
π Releasing a new major version becomes:
bump2version major && git push --tags
- Go watch the pipeline!
- βΆοΈ Press Play to publish the package
The βπ deploy to π PyPiβ job that publishes the package to PyPI is a manual job so it requires me to push Play when the earlier steps are complete.
π§ͺ Use TestPyPI (test.pypi.org)
)
Since this was the first Python package Iβd published, this was the first time I had use for something like TestPyPI. Given the permanence of releases on PyPI, itβs very nice to have a testing environment that mimics the real thing pretty well. You can totally see my tests.
The docs on π Using TestPyPI are pretty great but Iβll say a little more.
βοΈ On PyPI and TestPyPI
You can use our friend twine
to upload to TestPyPI as I do in the π deploy to π§ͺ TestPyPi CI job (after python -m build
succeeds):
TWINE_USERNAME=__token__ TWINE_REPOSITORY_URL=https://test.pypi.org/legacy/ \
twine upload --non-interactive --comment "π¦ hello world" \
--skip-existing --password $TESTPYPI_TOKEN dist/*
TWINE_USERNAME
: Youβll want to setTWINE_USERNAME
to__token__
if you are using an API token to authenticate.TESTPYPI_TOKEN
: Youβll also need to setTESTPYPI_TOKEN
to the value of the API token.
A note about using TestPyPI: do not expect that every package on PyPI is also on TestPyPI. In my GitLab CI config, I do this to install my package from TestPyPI while getting the dependencies from PyPI:
pip install -i https://test.pypi.org/simple/ pastebin-bisque \
--extra-index-url https://pypi.org/simple
Another tip: donβt trust the namespaces to have the same ownership between PyPI and TestPyPI. In other words: the person who authored the package at test.pypi.org/project/whatever
may not be the person who authored the package at pypi.org/project/whatever
.
This doesnβt mean much but as of this writing:
- π§ͺ TestPyPI:
- 180,202 projects
- 964,359 releases
- 1,993,081 files
- 171,379 users
- π PyPI:
- 487,007 projects
- 4,962,866 releases
- 9,315,735 files
- 748,166 users
β¦it does mean that there are a few hundred thousand projects that are not on TestPyPI. Whatβs the angle? If I wanted to target Python developers, I would look for projects that are not on Test PyPI. You want something that is used often enough but isnβt super high profile. A high profile package is likely to already be on TestPyPI or for its sudden appearance on TestPyPI to be noticeable. This is a more subtle, less scattershot approach to the kinds of things written up in articles like 10 malicious PyPI packages found stealing developerβs credentials. π€·ββοΈ It probably would not be worth it but one might get βluckyβ. π
π On package signing and why I declined
When I looked into signing the package I was building, I ultimately decided against proceeding. While I donβt hold all of the views in the linked articles, this summarizes my thinking and is followed by a more extensive reading list:
source: PGP signatures on PyPI: worse than useless
π Reading List
- PyPI and gpg signed packages
- Package signing in PIP - It works, in a roundabout sort of way
- PGP signatures on PyPI: worse than useless
- Source: woodruffw/pypi-pgp-statistics
- GPG signing - how does that really work with PyPI?
- Remove GPG references from publishing tutorial
I wound up declining to sign the pastebin-bisque
package. The time required would not be worth the benefit at this point in time.
πΈ Extra: Full .gitlab-ci.yml
for building and deploying Python package to GitLab Package Registry, TestPyPI and PyPI
Want to see the .gitlab-ci.yml
that I am using for building, testing and releasing the pastebin-bisque
package to GitLabβs Package Registry, TestPyPI and to PyPI?
Hereβs what these looked like in v1.0.0
:
- π¦ See the .gitlab-ci.yml for
v1
ofpastebin-bisque
on GitLab. - π See the .gitlab-ci.yml for
v1
ofpastebin-bisque
on Sourcegraph.
πββ¬ Whatβs next?
As the 10th Hacktoberfest comes to an end, Iβll be releasing v1.0.1
of pastebin-bisque
and Iβm super excited about the new contributors whose work will be included in that release: π» thank you!