TestPyPi via commit to masterPyPi via taggingThis 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;
pip install example-package-grumbitAt a high level, the process for setting up GitHub CI/CD project packaging, including pytesting, looks like this;
./.github/workflows/publish-to-test-pypi.yml for now../.github/workflows/publish-to-test-pypi.yml and get GitHub CI/CD workingcd <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 packagepython3 -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 tokenHaving 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 functionspython3 -m twine upload dist/* # Upload to PyPi
# When prompted, the username is __token__ and the password is the PyPi global scope API token[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
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 [email protected]:grumBit/${repo_name}.git
git push -u origin masterIf 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
publish-to-test-pypi.yml already has the parts needed for auto-testing included (see below)TestPyPi via commit to mastermaster branch, the GitHub CI/CD will run.
TestPyPiPyPi via taggingPutting 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 --tagversion_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 - name: Install requirements
run: >-
python -m
pip install
--requirement requirements.txt
- name: Run tests
run: >-
python -m
pytest__init__.py needed to added to the src/ and tests/ directory like this;
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