Skip to main content

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

Metadata

Release files for example-package-grumbit 1.0.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for example-package-grumbit 1.0.2
File Size Uploaded Provenance Actions
example_package_grumbit-1.0.2.tar.gz 5.8 kB Without provenance Details for example_package_grumbit-1.0.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for example-package-grumbit 1.0.2
File Interpreter ABI Platform Actions
example_package_grumbit-1.0.2-py3-none-any.whl Python 3 none any Details for example_package_grumbit-1.0.2-py3-none-any.whl

Total release size: 11.6 kB

Release files / example_package_grumbit-1.0.2.tar.gz

Download URL example_package_grumbit-1.0.2.tar.gz
Size 5.8 kB
Tags Source
SHA-256 checksum
How to use checksums
f0f9b6d2d335e99f2eefa06a0eeba5256a6680f7cbaf882256162f9919508692
BLAKE2b-256 checksum
How to use checksums
cef8b1376147b80b9634a746ba564b7bdd70faab230b1b58e31badb4a31812ee
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.1 CPython/3.9.14

Release files / example_package_grumbit-1.0.2-py3-none-any.whl

Download URL example_package_grumbit-1.0.2-py3-none-any.whl
Size 5.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cd268e82569879a4bcb10fbe2b5d73d72a75dda826ef9c5cd07a11c3c61a892f
BLAKE2b-256 checksum
How to use checksums
0f49a0c3f9ba38dc22b5b78ea55dab77b037fd77e3390c062e6500559163b429
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/4.0.1 CPython/3.9.14

Security for example-package-grumbit 1.0.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page