This is a working example that uses a GitHub actions CI/CD workflow to test, build and upload a Python package to TestPyPi and PyPi.
Project description
Python Packaging Example
This is a working example that uses a GitHub actions CI/CD workflow to test, build and upload a Python package to TestPyPi and PyPi.
I created this example package by working through these guides;
- Packaging Python Projects
- Publishing package distribution releases using GitHub Actions CI/CD workflows
- PyTest With GitHub Actions
The rest of the README describes to set up a new project in the same way.
When set up;
- Test and upload to TestPyPi occurs when the package version number is updated and a commit is made to the master branch
- Test and upload to PyPi occurs when a commit is tagged
- The package can be installed using
pip install example-package-grumbit
Table of contents
High level
At a high level, the process for setting up GitHub CI/CD project packaging, including pytesting, looks like this;
- Set up the file structure as per this example package, but leave out
./.github/workflows/publish-to-test-pypi.yml
for now. - Get local packaging working
- Get uploading to TestPyPi working
- Get uploading to PyPi working
- Add in
./.github/workflows/publish-to-test-pypi.yml
and get GitHub CI/CD working
Packaging Python Projects
Building the package
- The package metadata is configured in ./pyproject.toml
- Once configured, it can be built with;
cd <pacakges directory>
python3 -m venv .venv # Create the venv if it doesn't exist yet
source .venv/bin/activate
python3 -m pip install --upgrade pip setuptools wheel pip-tools pytest # Install the tools needed for the build tool
python3 -m pip install --upgrade build # Install the build tool itself
python3 -m build # build the package
Uploading the package to TestPyPi
- Upload the package for testing using;
python3 -m pip install --upgrade twine # Install the twine upload tool
python3 -m twine upload --repository testpypi dist/* # Upload to TestPyPi
# When prompted, the username is __token__ and the password is the TestPyPi global scope API token
-
Having uploaded the package, a package specific API token should be set up and saved in TestPyPi
-
Check the package can be downloaded and used in a new
venv
;
cd <some new tmp directory>
python3 -m venv .venv
source .venv/bin/activate
package_name="example-package-grumBit"
python3 -m pip install --index-url https://test.pypi.org/simple/ --pre ${package_name} # Check the package can be installed
python3 -c "from example_package_grumbit import example; print(example.add_one(1))" # Check package functions
Uploading the package to PyPi
python3 -m twine upload dist/* # Upload to PyPi
# When prompted, the username is __token__ and the password is the PyPi global scope API token
- Having uploaded the package, a package specific API token should be set up and saved in PyPi
Manually updating the package
- Each time the package is updated, it's version must be updated in the
[project]
section of ./pyproject.toml, then it needs to be re-built and uploaded;
vs ./pyproject.toml
python3 -m build # build the package
python3 -m twine check dist/* # check the package can be uploaded
python3 -m twine upload --repository testpypi dist/* # test uploading using TestPyPi
python3 -m twine upload dist/* # Upload to PyPi
GitHub Actions CI/CD workflows
Set up
- If the project isn't already sync'd up to GitHub, run;
cd "<the project's directory>"
repo_name="<the new repo's name>"
gh repo create "${repo_name}" --private
git init
git add --all
git commit -m "init"
git branch -M master
git remote add origin git@github.com:grumBit/${repo_name}.git
git push -u origin master
-
If the default branch isn't
master
, either change it on GitHub, or change.github/workflows/publish-to-test-pypi.yml
. -
Open the repo on GitHub using
gh browse
. In the browser, clickSettings
->Secrets
->Actions
. Then add two new secrets calledPYPI_API_TOKEN
andTEST_PYPI_API_TOKEN
, with the API tokens created after uploading the packages above -
Create and configure .github/workflows/publish-to-test-pypi.yml workflow definition
- NB: This example package's
publish-to-test-pypi.yml
already has the parts needed for auto-testing included (see below)
- NB: This example package's
Uploading to TestPyPi
via commit to master
- Every time a commit is made to the
master
branch, the GitHub CI/CD will run.- NB: For the packaging to succeed, the version must be updated in ./pyproject.toml.
- All commits to master will be uploaded to
TestPyPi
Uploading to PyPi
via tagging
-
Putting a tag on a commit and pushing it will cause GitHub CI/CD to run and create a PyPi release.
-
Use the following to tag the lastest commit (i.e.
HEAD
) with the version currently configured in./pyproject.toml
;
version_tag=v$(cat ./pyproject.toml | egrep "^version" | cut -d '"' -f2)
version_tag_info="Some release info"
git tag -a "${version_tag}" -m "${version_tag_info}"
git push --tag
- Use the following to tag a prior commit;
version_tag="vX.X.X"
version_tag_info="Some release info"
commit_sha="16fb0fd"
git tag -a "${version_tag}" "${commit_sha}" -m "${version_tag_info}"
git push --tag
Running pytest in GitHub CI/CD
- The workflow steps in the GitHub CI/CD guide didn't include running pytests. To get pytest to run the packages dependencies needed to be installed and then pytest run prior to the build step using these additional steps;
- name: Install requirements
run: >-
python -m
pip install
--requirement requirements.txt
- name: Run tests
run: >-
python -m
pytest
- As per the directory structure in the packaging projects guide, I put the tests in a separate hierarchy to the source. This meant
__init__.py
needed to added to thesrc/
andtests/
directory like this;- NB: I'm not 100% sure about this structuring. It follows the guide and means the test code isn't packaged up, however, I think it's less convenient than embedding
test/
folders within thesrc/
tree../pyproject.toml
can be configured so that embeddedtest/
folders are excluded, but I've gone with the "standard" for now.
- NB: I'm not 100% sure about this structuring. It follows the guide and means the test code isn't packaged up, however, I think it's less convenient than embedding
packaging_tutorial/
├── src/
│ ├── __init__.py
│ └── example_package_grumbit/
│ ├── __init__.py
│ └── example.py
└── tests/
├── __init__.py
└── example_package_grumbit/
├── __init__.py
└── test_example.py
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file example_package_grumbit-1.0.2.tar.gz
.
File metadata
- Download URL: example_package_grumbit-1.0.2.tar.gz
- Upload date:
- Size: 5.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.9.14
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | f0f9b6d2d335e99f2eefa06a0eeba5256a6680f7cbaf882256162f9919508692 |
|
MD5 | 0ae26c969eed17db2965d57818574c7b |
|
BLAKE2b-256 | cef8b1376147b80b9634a746ba564b7bdd70faab230b1b58e31badb4a31812ee |
File details
Details for the file example_package_grumbit-1.0.2-py3-none-any.whl
.
File metadata
- Download URL: example_package_grumbit-1.0.2-py3-none-any.whl
- Upload date:
- Size: 5.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.1 CPython/3.9.14
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | cd268e82569879a4bcb10fbe2b5d73d72a75dda826ef9c5cd07a11c3c61a892f |
|
MD5 | ca9f251c2d627ef35555d0b508463c25 |
|
BLAKE2b-256 | 0f49a0c3f9ba38dc22b5b78ea55dab77b037fd77e3390c062e6500559163b429 |