Skip to main content

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

PyPI PyPI - Python Version GitHub all releases GitHub license PyPI - Implementation PyPI - Wheel PyPI - Status GitHub issues GitHub forks GitHub stars

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;

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;

  1. Set up the file structure as per this example package, but leave out ./.github/workflows/publish-to-test-pypi.yml for now.
  2. Get local packaging working
  3. Get uploading to TestPyPi working
  4. Get uploading to PyPi working
  5. Add in ./.github/workflows/publish-to-test-pypi.yml and get GitHub CI/CD working

Packaging Python Projects

Building the package

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
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

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.

  • Add the TestPyPi and PyPi API tokens to the repo

  • Open the repo on GitHub using gh browse. In the browser, click Settings -> Secrets -> Actions. Then add two new secrets called PYPI_API_TOKEN and TEST_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)

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 the src/ and tests/ 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 the src/ tree. ./pyproject.toml can be configured so that embedded test/ folders are excluded, but I've gone with the "standard" for now.
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

example_package_grumbit-1.0.2.tar.gz (5.8 kB view details)

Uploaded Source

Built Distribution

example_package_grumbit-1.0.2-py3-none-any.whl (5.8 kB view details)

Uploaded Python 3

File details

Details for the file example_package_grumbit-1.0.2.tar.gz.

File metadata

File hashes

Hashes for example_package_grumbit-1.0.2.tar.gz
Algorithm Hash digest
SHA256 f0f9b6d2d335e99f2eefa06a0eeba5256a6680f7cbaf882256162f9919508692
MD5 0ae26c969eed17db2965d57818574c7b
BLAKE2b-256 cef8b1376147b80b9634a746ba564b7bdd70faab230b1b58e31badb4a31812ee

See more details on using hashes here.

File details

Details for the file example_package_grumbit-1.0.2-py3-none-any.whl.

File metadata

File hashes

Hashes for example_package_grumbit-1.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 cd268e82569879a4bcb10fbe2b5d73d72a75dda826ef9c5cd07a11c3c61a892f
MD5 ca9f251c2d627ef35555d0b508463c25
BLAKE2b-256 0f49a0c3f9ba38dc22b5b78ea55dab77b037fd77e3390c062e6500559163b429

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page