Create and publish your first Python Package
Overview
If you have never created a Python package are interested in creating one, this post would be the right place to start with!
Tools
- pyenv: Python version management
- conda: Python environment management
- poetry: Dependency package management
- Python Packages (py-pkgs.org): How to create a Python Package
- cookiecutter: Tool to create a Python project from a template
- PyPI (Python Package Index): Python package hub where you can publish your package and you can use packages from.
- Test Python Package Index: Python package hub for testing
- python-semantic-release: Tool to automate Python package release based on semantic commit
- flake8, black, isort: Formater、Linter
Prepare environment with Conda
Installation is here:
- Download the miniconda3 installer for your platform. e.g. Miniconda3 macOS Apple M1 ARM 64-bit bash.
- Execute the installer
bash Miniconda3-latest-MacOSX-x86_64.sh
Now, you can create a conda environment with the following command:
conda create --name <environment name> python=3.9 -y
Then, you need to activate it with the following command:
conda activate <environment name>
Manage your package with Poetry
First, you need to install poetry:
curl -sSL https://install.python-poetry.org | python3 -
If you encounter the following warning, just follow the instruction and move the directory.
Configuration file exists at xxx/Library/Application Support/pypoetry, reusing this directory.
Consider moving configuration to xxx/Library/Preferences/pypoetry, as support for the legacy directory will be removed in an upcoming release.
mkdir -p ~/Library/Preferences/pypoetry
mv ~"/Library/Application Support/pypoetry/config.toml" ~/Library/Preferences/pypoetry/config.toml
Create a package with cookiecutter
Cookiecutter is a tool to create a python project from a template.
You can install cookiecutter by the following command:
pip install cookiecutter
or you can use brew
if you’re using MacOS
brew install cookiecutter
Then, create Python project from this cookiecutter template: https://github.com/py-pkgs/py-pkgs-cookiecutter
cookiecutter https://github.com/py-pkgs/py-pkgs-cookiecutter.git
Implement your package
You can implement your own package in the generated Python project above. The structure would be like the following (from How to package a Python):
pycounts
├── CHANGELOG.md ┐
├── CONDUCT.md │
├── CONTRIBUTING.md │
├── docs │ Package documentation
│ └── ... │
├── LICENSE │
├── README.md ┘
├── pyproject.toml ┐
├── src │
│ └── pycounts │ Package source code, metadata,
│ ├── __init__.py │ and build instructions
│ ├── moduleA.py │
│ └── moduleB.py ┘
└── tests ┐
└── ... ┘ Package tests
pycounts
is the name of the example package.
Build your package
You can build your Python package with the following command:
poetry build
Everything is ready because you created your project from the template.
This command will generate wheel and src under dist
directory, which can be configured by dist_path
in pyproject.toml
.
Update package version
When you add some feature or create the first version of your package. You need to upgrade your package version.
You can do it by the poetry version
command:
poetry version <rule>
rule
can be one of the followings:
major
: major version upgrade e.g. 1.2.0 -> 2.0.0minor
: minor version upgrade e.g. 1.2.0 -> 1.3.0patch
: patch version upgrade e.g. 1.2.0 -> 1.2.1premajor
: premajor version upgrade e.g. 1.2.0 -> 2.0.0a0preminor
: preminor version upgrade e.g. 1.2.0 -> 1.3.0a0prepatch
: prepatch version upgrade e.g. 1.2.0 -> 1.2.1a0- etc
For more details, you can read https://python-poetry.org/docs/cli/#version.
Publish your Package to PyPi
You can publish your Python package so others can use it as any other Python libraries that you usually install with pip install <package-name>
.
Settings of PyPi
You need to sign up https://test.pypi.org/account/register/ and https://pypi.org/account/register for testing and prod, respectively. You can skip this step if you already have your account.
For testing, you can add the source test-pypi
to poetry configuration:
poetry source add test-pypi https://test.pypi.org/simple/
And generate API key from https://test.pypi.org/manage/account/ for test account and set it to pypi-token.test-pypi
poetry config pypi-token.test-pypi pypi-xxxxxx
Now you’re ready to publish your Package to test-pypi.
You need to do the same thing for pypi https://pypi.org/ by setting your API key:
poetry config pypi-token.pypi pypi-xxxxx
(Be careful, this command sets pypi-token.pypi
!)
Publish your package to test pypi
Build your package by the following command:
poetry build
Publish by the poetry publish
command by specifying test-pypi
:
poetry publish -r test-pypi
You’ll be able to check your package with the url: https://test.pypi.org/project/<yourpackage>/
Check your package published to test pypi
You can install your package from test pypi
poetry add --source test-pypi <your package>
And you can check the behavior as a user.
After confirming everything’s fine, now it’s time to publish to Pypi.
Publish your package to Pypi
You can publish your package almost same way as you did it for test pypi:
poetry build
poetry publish
Release by Python Semantic Release (PSR)
Python Semantic Release (PSR) is a tool to automate releasing based on semantic commit message.
To configure PSR, you need to install it by the command:
poetry add --dev python-semantic-release
And update pyproject.toml:
[tool.semantic_release]
version_variable = "pyproject.toml:version"
version_source = "tag"
There are several command options:
- Check the next version:
semantic-release print-version
- Check the current version:
semantic-release print-version --current
- Create a new version, commit it, and create a tag (only in local):
semantic-release version
- Publish your package (including updating changelog, push to git, build and upload dist to github release):
semantic-release publish
Lint your Python codes
You can use isort
, black
, and flake8
poetry run isort --check --diff .
poetry run black --check --diff .
poetry run flake8 .
If you need extra configuration for flake8
such as excluding some directories or files, you can configure it in .flake8
file.
[flake8]
exclude =
# No need to traverse our git directory
.git,
# There's no value in checking cache directories
__pycache__,
# The conf file is mostly autogenerated, ignore it
docs/source/conf.py,
# The old directory contains Flake8 2.0
old,
# This contains our built documentation
build,
# This contains builds of flake8 that we don't want to check
dist
.venv
GitHub Actions for your CI
I splitted the ci-cd from the cookiecutter template as it publishes a new release every time a new PR is merged to the main branch.
GitHub Actions — CI
This workflow is executed when creating/updating a PR and pushing a new commit to the main branch. This workflow include test, lint, codecov, docs, etc.
name: dev
on:
pull_request:
push:
branches:
- main
jobs:
ci:
# Set up operating system
runs-on: ubuntu-latest
# Define job steps
steps:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Check-out repository
uses: actions/checkout@v3
- name: Load cached Poetry installation
id: cached-poetry
uses: actions/cache@v3
with:
path: ~/.local # the path depends on the OS
key: poetry # increment to reset cache
- name: Install poetry
if: steps.cached-poetry.outputs.cache-hit != 'true'
uses: snok/install-poetry@v1
- name: Restore cached dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Install package
run: poetry install
- name: Lint
run: |
poetry run isort --check --diff .
poetry run black --check --diff .
poetry run flake8 .
- name: Test with pytest
run: poetry run pytest tests/ --cov=<package_name> --cov-report=xml
- name: Use Codecov to track coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml # coverage report
- name: Build documentation
run: poetry run make html --directory docs/
Please replace the package name with your own.
GitHub Actions — semantic-pull-request
As semantic-release decides version tag based on the commits in each release, it’s better to check if the title of a pull request matches semantic commit message (if you use pull request title for each commit to the main branch.)
name: semantic-pull-request
on:
pull_request:
types:
- opened
- edited
- synchronize
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GitHub Actions — release
Lastly, I’m too lazy to manually create a new release on GitHub so I created a GitHub Actions workflow with dispatch_workflow
type, which execute semantic-release publish
.
To use trusted publishers, you need to follow the configuration here: https://docs.pypi.org/trusted-publishers/adding-a-publisher/. With this configuration, you don’t need to set any password in your GitHub repository.
name: release
on:
workflow_dispatch:
jobs:
release:
if: github.ref == 'refs/heads/main'
environment:
name: pypi
url: https://pypi.org/p/autonote
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
runs-on: ubuntu-latest
steps:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Check-out repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Load cached Poetry installation
id: cached-poetry
uses: actions/cache@v3
with:
path: ~/.local # the path depends on the OS
key: poetry # increment to reset cache
- name: Install poetry
if: steps.cached-poetry.outputs.cache-hit != 'true'
uses: snok/install-poetry@v1
- name: "Restore cached dependencies"
uses: actions/cache@v3
with:
path: ~/.cache/pypoetry
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Install package
run: poetry install
- name: Use Python Semantic Release to prepare release
env:
# This token is created automatically by GH Actions
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config user.name github-actions
git config user.email github-actions@github.com
poetry run semantic-release publish
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
- name: Test install from TestPyPI
run: |
pip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple \
<package_name>
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
Summary
This post covered basic steps to create your own package, publish to PyPi with CI/CD pipelines using GitHub Actions.
For future work, I’d try to find a better release flow and write about documentation.