Reputation: 981
When creating a Python package, you can simply write the code, build the package, and share it on PyPI. But how do you do that?
And then, what if you want to go further?
Upvotes: 6
Views: 930
Reputation: 981
When you've published dozens of packages, you know how to answer these questions in ways that suit your workflow(s) and taste. But answering these questions for the first time can be quite difficult, time consuming, and frustrating!
That's why I spent days researching ways of doing these things, which I then published as a blog article called How to create a Python package in 2022.
That article, and this answer, document my findings for when I wanted to publish my package extendedjson
Here is an overview of some tools you can use and the steps you can take, in the order I followed them while discovering all of this.
Disclaimer: other alternative tools exist (usually) & most of the steps here are not mandatory.
pip
)Here is an overview of the things you can do and more or less how to do it. Again, thorough instructions plus the rationale of why I picked certain tools, methods, etc, can be found in the reference article.
Use Poetry for dependency management.
poetry init
initialises a project in a directory or poetry new dirname
creates a new directory structure for youpoetry install
to install all your dependenciespoetry add packagename
can be used to add packagename
as a dependency, use -D
if it's a development dependency (i.e., you need it while developing the package, but the package users won't need it. For example, black
is a nice example of a development dependency)Set up a repository on GitHub to host your code.
Set up pre-commit hooks to ensure your code is always properly formatted and it passes linting. This goes on .pre-commit-config.yaml
. E.g., the YAML below checks TOML and YAML files, ensures all files end with a newline, makes sure the end-of-line marker is consistent across all files, and then runs black and isort on your code.
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:
- id: isort
args: ["--profile", "black"]
Configure Poetry to use the Test PyPI to make sure you can publish a package and it is downloadable & installable.
poetry config repositories.testpypi https://test.pypi.org/legacy/
poetry config http-basic.testpypi __token__ pypi-your-api-token-here
(the __token__
is a literal and shouldn't be replaced, your token goes after that).poetry build
and upload your package poetry publish -r testpypi
Manage your CHANGELOG with Scriv
scriv create
before any substantial commit and edit the file that pops upscriv collect
before any release to collect all fragments into one changelogConfigure Poetry to use PyPI
poetry config pypi-token.pypi pypi-your-token-here
poetry publish --build
Do a victory lap: try pip install yourpackagename
to make sure everything is going great ;)
Publish a GH release that matches what you uploaded to PyPI
Write tests. There are many options out there. Pytest is simple, versatile, and not too verbose.
tests/
test_...
test_...
assert
) to check for things (tests fail when asserting something Falsy); notice sometimes you don't even need to import pytest
in your test files; e.g.:# In tests/test_basic_example.py
def this_test_would_definitely_fail():
assert 5 > 10
def this_test_would_definitely_pass():
assert 5 > 0
run tests with the command pytest
Automate testing, linting, and formatting, with tox.
tox.ini
. You can also embed it in the file pyproject.toml
, but as of writing this, that's only supported if you add a string that actually represents the .ini
configuration, which is ugly. Example tox.ini
:[tox]
isolated_build = True
envlist = py38,py39,py310
[testenv]
deps =
black
pytest
commands =
black --check extendedjson
pytest .
The environments py38
to py310
are automatically understood by tox to represent different Python versions (you guess which ones). The header [testenv]
defines configurations for all those environments that tox knows about. We install the dependencies listed in deps = ...
and then run the commands listed in commands = ...
.
run tox with tox
for all environments or tox -e py39
to pick a specific environment
Add code coverage with coverage.py
coverage run --source=yourpackage --branch -m pytest .
coverage html
Create a GitHub action that runs linting and testing on commits and pull requests
.github/workflows
# .github/workflows/build.yaml
name: Your amazing CI name
# Run automatically on...
on:
push: # pushes...
branches: [ main ] # to the main branch... and
pull_request: # on pull requests...
branches: [ main ] # against the main branch.
# What jobs does this workflow run?
jobs:
build: # There's a job called “build” which
runs-on: ubuntu-latest # runs on an Ubuntu machine
strategy:
matrix: # that goes through
python-version: ["3.8", "3.9", "3.10"] # these Python versions.
steps: # The job “build” has multiple steps:
- name: Checkout sources
uses: actions/checkout@v2 # Checkout the repository into the runner,
- name: Setup Python
uses: actions/setup-python@v2 # then set up Python,
with:
python-version: ${{ matrix.python-version }} # with the version that is currently “selected”...
- name: Install dependencies
run: | # Then run these commands
python -m pip install --upgrade pip
python -m pip install tox tox-gh-actions # install two dependencies...
- name: Run tox
run: tox # and finally run tox.
Notice that, above, we installed tox and a plugin called tox-gh-actions
.
This plugin will make tox aware of the Python version that is set up in the GH action runner, which will allow us to specify which environments tox will run in that case.
We just need to set a correspondence in the file tox.ini
:
# tox.ini
# ...
[gh-actions]
python =
3.8: py38
3.9: py39
3.10: py310
xml
report (it's a format that Codecov understands)# ...
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: true
name: Publish to PyPI
on:
release:
types: [ published ]
branches: [ main ]
workflow_dispatch:
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
# Checkout and set up Python
- name: Install poetry and dependencies
run: |
python -m pip install --upgrade pip
python -m pip install poetry
- name: Configure poetry
env:
pypi_token: ${{ secrets.PyPI_TOKEN }} # You set this manually as a secret in your repository
run: poetry config pypi-token.pypi $pypi_token
- name: Build and publish
run: poetry publish --build
Tidy up a bit
Upvotes: 8