Packaging your code
Publishing your core rust code to crates.io is pretty straightforward, ensuring that your python code can be used by people on different systems and with multiple versions of python needs more work.
I'll assume you are happy publishing via github actions in general and may add an excursion with more detail later.
Note:
I've used azure devops in the place of PyPi and crates.io to host the final packages (the world really doesn't need another fizzbuzz implementation spamming public package repositories!), publishing to the main package stores is easier.
Crates.io
Packaging for crates.io
You can follow the normal process for publishing to crates.io:
/projectroot$ cargo build --release -p fizzbuzz
/projectroot$ cargo package -p fizzbuzz
/projectroot$ cargo publish --package fizzbuzz
Just remember to add -p fizzbuzz
so you don't accidentally also try to publish your wrapped code!
Publishing from github actions
Here is the workflow I used to publish (to ADO):
.github/workflows/deploy-rust.yml
name: Deploy Rust
concurrency: ADO-Cargo
on:
workflow_dispatch:
push:
tags: "rust-v*"
jobs:
quality-check:
uses: ./.github/workflows/rust.yml
publish:
environment: ADO-Packages
needs: quality-check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Build and package
run: |
cargo build --release -p fizzbuzz
cargo package -p fizzbuzz
- name: Login to ADO
env:
PAT: ${{ secrets.ADO_TOKEN }}
run: echo -n Basic $(echo -n PAT:$PAT | base64) | cargo login --registry FizzBuzz
- name: Publish to ADO
run: cargo publish --package fizzbuzz --registry FizzBuzz
TODO - publishing to ADO
.cargo/config.toml
[registries]
FizzBuzz = { index = "sparse+https://pkgs.dev.azure.com/MusicalNinjas/FizzBuzz/_packaging/FizzBuzz/Cargo/index/" }
# Priority given to wincred if running on WSL (later entries have higher precedence)
[registry]
global-credential-providers = ["cargo:token"]
Pypi
The python distribution is significantly more tricky than the rust one in this situation.
Distribution formats - pre-built vs build from source
Python offers two distribution formats for packages:
- Source distributions which require the user to have a rust toolchain in place and to compile your code themselves when they install it.
- Binary distributions which require you to compile your code for a specific combination of operating system, architecture and python version.
Here's how to successfully provide both and support the widest range of users possible:
Supporting many linux distros
Each version of each linux distro has different standard libraries available. To ensure compatibility with as many as possible PyPa defined manylinux
with clear restrictions on build environments. cibuildhweel
(see below)makes it easy to meet these requirements.
Use cibuildwheel
from PyPa
PyPA provide cibuildwheel
to make the process of building wheels for different versions of python and different OS & architectures reasonably simple.
- Configure cibuildwheel to install rust and clean any existing build artefacts before beginning builds, via
./pyproject.toml
:... [tool.cibuildwheel.linux] before-all = "just clean" archs = "all" [tool.cibuildwheel.macos] before-all = "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal && cargo install just && just clean" [tool.cibuildwheel.windows] before-all = "rustup target add aarch64-pc-windows-msvc i586-pc-windows-msvc i686-pc-windows-msvc x86_64-pc-windows-msvc && cargo install just && just clean" ...
- Test each wheel after building, you'll also need
delvewheel
on windows builds (./pyproject.toml
):... [tool.cibuildwheel] # `test-command` will FAIL if only passed `pytest {package}` # as this tries to resolve imports based on source files (where there is no `.so` for rust libraries), not installed wheel test-command = "pytest {package}/tests" test-extras = "test" ... [tool.cibuildwheel.windows] ... before-build = "pip install delvewheel" repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}" ...
- Skip linux architecture / library combinations with no rust version available (
./pyproject.toml
):... [tool.cibuildwheel] skip = [ "*-musllinux_i686", # No musllinux-i686 rust compiler available. "*-musllinux_ppc64le", # no rust target available. "*-musllinux_s390x", # no rust target available. ] ...
- Set up a build pipeline with a job to run cibuildwheel for you on Windows, Macos & Linux and to create a source distribution. Run the various linux builds using a matrix strategy to reduce your build time significantly.
.github/workflows/deploy-python.yml
- full source
name: Deploy Python
concurrency: ADO-twine
on:
workflow_dispatch:
push:
tags: "python-v*"
jobs:
python-checks:
uses: ./.github/workflows/python.yml
rust-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Test fizzbuzzo3
run: cargo test --workspace
wheels:
name: Build wheels on ${{ matrix.os }} - ${{ matrix.target }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
# macos-13 is an intel runner, macos-14 is apple silicon
os: [ubuntu-latest, windows-latest, macos-13, macos-14]
target: ['*']
include:
- os: ubuntu-latest
target: '*x86*'
- os: ubuntu-latest
target: '*i686*'
- os: ubuntu-latest
target: '*manylinux_aarch64*'
- os: ubuntu-latest
target: '*musllinux_aarch64*'
- os: ubuntu-latest
target: '*ppc64le*'
- os: ubuntu-latest
target: '*s390x*'
exclude:
- os: ubuntu-latest
target: '*'
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
if: runner.os == 'Linux'
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Build wheels
uses: musicalninjadad/cibuildwheel@MusicalNinjaDad/issue1850
env:
CIBW_BUILD: ${{ matrix.target }}
# with:
# package-dir: .
# output-dir: wheelhouse
# config-file: "{package}/pyproject.toml"
- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: ./wheelhouse/*.whl
sdist:
name: Make SDist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build SDist
run: pipx run build --sdist
- uses: actions/upload-artifact@v4
with:
name: cibw-sdist
path: dist/*.tar.gz
publish:
environment: ADO-Packages
needs: [python-checks, rust-checks, wheels, sdist]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
pattern: cibw-*
path: dist
merge-multiple: true
- name: Install tooling
run: python -m pip install twine keyring artifacts-keyring
- name: Upload to ADO
env:
TWINE_REPOSITORY_URL: https://pkgs.dev.azure.com/MusicalNinjas/FizzBuzz/_packaging/FizzBuzz/pypi/upload/
TWINE_USERNAME: FizzBuzz
TWINE_PASSWORD: ${{ secrets.ADO_TOKEN }}
run: twine upload --non-interactive dist/*
./pyproject.toml
- full source
[project]
name = "fizzbuzz"
description = "An implementation of the traditional fizzbuzz kata"
# readme = "README.md"
license = { text = "MIT" }
authors = [{ name = "Mike Foster" }]
dynamic = ["version"]
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules",
"Intended Audience :: Developers",
"Natural Language :: English",
]
[project.urls]
# Homepage = ""
# Documentation = ""
Repository = "https://github.com/MusicalNinjaDad/fizzbuzz"
Issues = "https://github.com/MusicalNinjaDad/fizzbuzz/issues"
# Changelog = ""
[tool.setuptools.dynamic]
version = { file = "__pyversion__" }
# ===========================
# Build, package
# ===========================
[build-system]
requires = ["setuptools", "setuptools-rust"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["python"]
[[tool.setuptools-rust.ext-modules]]
# The last part of the name (e.g. "_lib") has to match lib.name in Cargo.toml,
# but you can add a prefix to nest it inside of a Python package.
target = "fizzbuzz.fizzbuzzo3"
path = "rust/fizzbuzzo3/Cargo.toml"
binding = "PyO3"
features = ["pyo3/extension-module"] # IMPORTANT!!!
debug = false # Adds `--release` to `pip install -e .` builds, necessary for performance testing
[tool.cibuildwheel]
skip = [
"*-musllinux_i686", # No musllinux-i686 rust compiler available.
"*-musllinux_ppc64le", # no rust target available.
"*-musllinux_s390x", # no rust target available.
]
# Use prebuilt copies of pypa images with rust and just ready compiled (saves up to 25 mins on emulated archs)
manylinux-x86_64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_x86_64-rust"
manylinux-i686-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_i686-rust"
manylinux-aarch64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_aarch64-rust"
manylinux-ppc64le-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_ppc64le-rust"
manylinux-s390x-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_s390x-rust"
manylinux-pypy_x86_64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_x86_64-rust"
manylinux-pypy_i686-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_i686-rust"
manylinux-pypy_aarch64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_aarch64-rust"
musllinux-x86_64-image = "ghcr.io/musicalninjas/quay.io/pypa/musllinux_1_2_x86_64-rust"
musllinux-aarch64-image = "ghcr.io/musicalninjas/quay.io/pypa/musllinux_1_2_aarch64-rust"
# `test-command` will FAIL if only passed `pytest {package}`
# as this tries to resolve imports based on source files (where there is no `.so` for rust libraries), not installed wheel
test-command = "pytest {package}/tests"
test-extras = "test"
[tool.cibuildwheel.linux]
before-all = "just clean"
archs = "all"
[tool.cibuildwheel.macos]
before-all = "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal && cargo install just && just clean"
[tool.cibuildwheel.windows]
before-all = "rustup target add aarch64-pc-windows-msvc i586-pc-windows-msvc i686-pc-windows-msvc x86_64-pc-windows-msvc && cargo install just && just clean"
before-build = "pip install delvewheel"
repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}"
# ===========================
# Test, lint, documentation
# ===========================
[project.optional-dependencies]
stubs = ["pyo3-stubgen"]
lint = ["ruff"]
test = ["pytest", "pytest-doctest-mkdocstrings"]
cov = ["fizzbuzz[test]", "pytest-cov"]
doc = ["black", "mkdocs", "mkdocstrings[python]", "mkdocs-material", "fizzbuzz[stubs]"]
dev = ["fizzbuzz[stubs,lint,test,cov,doc]"]
[tool.pytest.ini_options]
xfail_strict = true
addopts = [
"--doctest-modules",
"--doctest-mdcodeblocks",
"--doctest-glob=*.pyi",
"--doctest-glob=*.md",
]
testpaths = ["tests", "python"]
[tool.coverage.run]
branch = true
omit = ["tests/*"]
dynamic_context = "test_function"
[tool.ruff]
line-length = 120
format.skip-magic-trailing-comma = false
format.quote-style = "double"
[tool.ruff.lint]
select = ["ALL"]
flake8-pytest-style.fixture-parentheses = false
flake8-annotations.mypy-init-return = true
extend-ignore = [
"E701", # One-line ifs are not great, one-liners with suppression are worse
"ANN101", # Type annotation for `self`
"ANN202", # Return type annotation for private functions
"ANN401", # Using Any often enough that supressing individually is pointless
"W291", # Double space at EOL is linebreak in md-docstring
"W292", # Too much typing to add newline at end of each file
"W293", # Whitespace only lines are OK for us
"D401", # First line of docstring should be in imperative mood ("Returns ..." is OK, for example)
"D203", # 1 blank line required before class docstring (No thank you, only after the class docstring)
"D212", # Multi-line docstring summary should start at the first line (We start on new line, D209)
"D215", # Section underline is over-indented (No underlines)
"D400", # First line should end with a period (We allow any punctuation, D415)
"D406", # Section name should end with a newline (We use a colon, D416)
"D407", # Missing dashed underline after section (No underlines)
"D408", # Section underline should be in the line following the section's name (No underlines)
"D409", # Section underline should match the length of its name (No underlines)
"D413", # Missing blank line after last section (Not required)
]
[tool.ruff.lint.per-file-ignores]
# Additional ignores for tests
"**/test_*.py" = [
"INP001", # Missing __init__.py
"ANN", # Missing type annotations
"S101", # Use of `assert`
"PLR2004", # Magic number comparisons are OK in tests
"D1", # Don't REQUIRE docstrings for tests - but they are nice
]
"**/__init__.py" = [
"D104", # Don't require module docstring in __init__.py
"F401", # Unused imports are fine: using __init__.py to expose them with implicit __ALL__
]
Saving hours by using pre-built images
If you are wondering why there is no "install rust" command for the linux builds; I created dedicated container images for of pypa's base images with rust, just etc. pre-installed. This saves a load of time during the build, as installing rust and compiling the various components can be very slow under emulation.
The dockerfile and build process are at MusicalNinjas/cibuildwheel-rust, They get bumped by dependabot every time pypa release a new image for manylinux2014_x86_64
so should be nice and up to date.
The images can be used by including the following in your ./pyproject.toml
:
[tool.cibuildwheel]
...
# Use prebuilt copies of pypa images with rust and just ready compiled (saves up to 25 mins on emulated archs)
manylinux-x86_64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_x86_64-rust"
manylinux-i686-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_i686-rust"
manylinux-aarch64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_aarch64-rust"
manylinux-ppc64le-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_ppc64le-rust"
manylinux-s390x-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_s390x-rust"
manylinux-pypy_x86_64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_x86_64-rust"
manylinux-pypy_i686-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_i686-rust"
manylinux-pypy_aarch64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_aarch64-rust"
musllinux-x86_64-image = "ghcr.io/musicalninjas/quay.io/pypa/musllinux_1_2_x86_64-rust"
musllinux-aarch64-image = "ghcr.io/musicalninjas/quay.io/pypa/musllinux_1_2_aarch64-rust"
...
MusicalNinjas/cibuildwheel-rust/Dockerfile
- full source
# Dependabot can't decode the yyyy-mm-dd-sha format used by cibuildwheel, so no point in restricting tags here.
# (See https://github.com/pypa/cibuildwheel/discussions/1858)
ARG baseImage=quay.io/pypa/manylinux2014_x86_64
FROM ${baseImage}
ARG rustup_options=
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal ${rustup_options}
ENV PATH=/root/.cargo/bin:$PATH
RUN cargo install just
MusicalNinjas/cibuildwheel-rust/.github/workflows/publish_docker.yml
- full source
name: Build and publish Docker images for devcontainer
on:
push:
branches:
- "main"
paths:
- "Dockerfile"
- ".github/workflows/publish_docker.yml"
jobs:
build-and-publish:
name: build ${{ matrix.baseImage }}
permissions:
packages: write
runs-on: ubuntu-latest
strategy:
matrix:
include:
- baseImage: "manylinux2014_x86_64"
platform: "linux/amd64"
- baseImage: "manylinux2014_i686"
rustup_options: "--default-host i686-unknown-linux-gnu" # not correctly identified by rustup under QEMU on x64
platform: "linux/386"
- baseImage: "manylinux2014_aarch64"
platform: "linux/arm64/v8"
- baseImage: "manylinux2014_ppc64le"
platform: "linux/ppc64le"
- baseImage: "manylinux2014_s390x"
platform: "linux/s390x"
- baseImage: "musllinux_1_2_x86_64"
platform: "linux/amd64"
- baseImage: "musllinux_1_2_aarch64"
platform: "linux/arm64/v8"
steps:
- name: checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: ${{ matrix.platform }}
build-args: |
baseImage=quay.io/pypa/${{ matrix.baseImage }}
rustup_options=${{ matrix.rustup_options }}
tags: ghcr.io/musicalninjas/${{ matrix.baseImage }}-rust
avoiding a 6 hour build
At one point I had a build run for nearly 6 hours before I cancelled it. The problem was that I use ruff
for linting, and ruff doesn't provide pre-built wheels for some manylinux architectures. That meant that I was building ruff from scratch, under emulation for each wheel I built on these architectures!
Separating the [test]
, [lint]
, [doc]
and [cov]
erage dependencies solved this; after a lot of searching I also found the way to provide a single [dev]
set of dependencies combing them all.
In ./pyproject.toml
:
...
[tool.cibuildwheel]
...
test-extras = "test"
...
[project.optional-dependencies]
lint = ["ruff"]
test = ["pytest", "pytest-doctest-mkdocstrings"]
cov = ["fizzbuzz[test]", "pytest-cov"]
doc = ["black", "mkdocs", "mkdocstrings[python]", "mkdocs-material"]
dev = ["fizzbuzz[lint,test,cov,doc]"]
...
failing wheel tests due to namespace collisions
I lost over half a day at one point trying to track down the reason that my wheel builds were failing final testing. There were two problems:
- Make sure you clean any pre-compiled
.so
files before building a wheel, otherwisepip wheel
may decide that you already have a valid.so
and use that, with the wrong versions of the C-libraries. This is only a problem when doing local wheel builds for debugging, because you're not checking the.so
files into git are you? (I created ajust clean
command for this) - Make sure you directly specify the test path for wheel tests in
./pyproject.toml
:otherwise the directive... [tool.cibuildwheel] # `test-command` will FAIL if only passed `pytest {package}` # as this tries to resolve imports based on source files (where there is no `.so` for rust libraries), not installed wheel test-command = "pytest {package}/tests" ...
tool.pytest.ini_options.testpaths = ["tests", "python"]
will cause pytest to look inpython
, find your namespace, and ignore the installed wheel!
./pyproject.toml
- full source
[project]
name = "fizzbuzz"
description = "An implementation of the traditional fizzbuzz kata"
# readme = "README.md"
license = { text = "MIT" }
authors = [{ name = "Mike Foster" }]
dynamic = ["version"]
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries :: Python Modules",
"Intended Audience :: Developers",
"Natural Language :: English",
]
[project.urls]
# Homepage = ""
# Documentation = ""
Repository = "https://github.com/MusicalNinjaDad/fizzbuzz"
Issues = "https://github.com/MusicalNinjaDad/fizzbuzz/issues"
# Changelog = ""
[tool.setuptools.dynamic]
version = { file = "__pyversion__" }
# ===========================
# Build, package
# ===========================
[build-system]
requires = ["setuptools", "setuptools-rust"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["python"]
[[tool.setuptools-rust.ext-modules]]
# The last part of the name (e.g. "_lib") has to match lib.name in Cargo.toml,
# but you can add a prefix to nest it inside of a Python package.
target = "fizzbuzz.fizzbuzzo3"
path = "rust/fizzbuzzo3/Cargo.toml"
binding = "PyO3"
features = ["pyo3/extension-module"] # IMPORTANT!!!
debug = false # Adds `--release` to `pip install -e .` builds, necessary for performance testing
[tool.cibuildwheel]
skip = [
"*-musllinux_i686", # No musllinux-i686 rust compiler available.
"*-musllinux_ppc64le", # no rust target available.
"*-musllinux_s390x", # no rust target available.
]
# Use prebuilt copies of pypa images with rust and just ready compiled (saves up to 25 mins on emulated archs)
manylinux-x86_64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_x86_64-rust"
manylinux-i686-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_i686-rust"
manylinux-aarch64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_aarch64-rust"
manylinux-ppc64le-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_ppc64le-rust"
manylinux-s390x-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_s390x-rust"
manylinux-pypy_x86_64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_x86_64-rust"
manylinux-pypy_i686-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_i686-rust"
manylinux-pypy_aarch64-image = "ghcr.io/musicalninjas/quay.io/pypa/manylinux2014_aarch64-rust"
musllinux-x86_64-image = "ghcr.io/musicalninjas/quay.io/pypa/musllinux_1_2_x86_64-rust"
musllinux-aarch64-image = "ghcr.io/musicalninjas/quay.io/pypa/musllinux_1_2_aarch64-rust"
# `test-command` will FAIL if only passed `pytest {package}`
# as this tries to resolve imports based on source files (where there is no `.so` for rust libraries), not installed wheel
test-command = "pytest {package}/tests"
test-extras = "test"
[tool.cibuildwheel.linux]
before-all = "just clean"
archs = "all"
[tool.cibuildwheel.macos]
before-all = "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal && cargo install just && just clean"
[tool.cibuildwheel.windows]
before-all = "rustup target add aarch64-pc-windows-msvc i586-pc-windows-msvc i686-pc-windows-msvc x86_64-pc-windows-msvc && cargo install just && just clean"
before-build = "pip install delvewheel"
repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}"
# ===========================
# Test, lint, documentation
# ===========================
[project.optional-dependencies]
stubs = ["pyo3-stubgen"]
lint = ["ruff"]
test = ["pytest", "pytest-doctest-mkdocstrings"]
cov = ["fizzbuzz[test]", "pytest-cov"]
doc = ["black", "mkdocs", "mkdocstrings[python]", "mkdocs-material", "fizzbuzz[stubs]"]
dev = ["fizzbuzz[stubs,lint,test,cov,doc]"]
[tool.pytest.ini_options]
xfail_strict = true
addopts = [
"--doctest-modules",
"--doctest-mdcodeblocks",
"--doctest-glob=*.pyi",
"--doctest-glob=*.md",
]
testpaths = ["tests", "python"]
[tool.coverage.run]
branch = true
omit = ["tests/*"]
dynamic_context = "test_function"
[tool.ruff]
line-length = 120
format.skip-magic-trailing-comma = false
format.quote-style = "double"
[tool.ruff.lint]
select = ["ALL"]
flake8-pytest-style.fixture-parentheses = false
flake8-annotations.mypy-init-return = true
extend-ignore = [
"E701", # One-line ifs are not great, one-liners with suppression are worse
"ANN101", # Type annotation for `self`
"ANN202", # Return type annotation for private functions
"ANN401", # Using Any often enough that supressing individually is pointless
"W291", # Double space at EOL is linebreak in md-docstring
"W292", # Too much typing to add newline at end of each file
"W293", # Whitespace only lines are OK for us
"D401", # First line of docstring should be in imperative mood ("Returns ..." is OK, for example)
"D203", # 1 blank line required before class docstring (No thank you, only after the class docstring)
"D212", # Multi-line docstring summary should start at the first line (We start on new line, D209)
"D215", # Section underline is over-indented (No underlines)
"D400", # First line should end with a period (We allow any punctuation, D415)
"D406", # Section name should end with a newline (We use a colon, D416)
"D407", # Missing dashed underline after section (No underlines)
"D408", # Section underline should be in the line following the section's name (No underlines)
"D409", # Section underline should match the length of its name (No underlines)
"D413", # Missing blank line after last section (Not required)
]
[tool.ruff.lint.per-file-ignores]
# Additional ignores for tests
"**/test_*.py" = [
"INP001", # Missing __init__.py
"ANN", # Missing type annotations
"S101", # Use of `assert`
"PLR2004", # Magic number comparisons are OK in tests
"D1", # Don't REQUIRE docstrings for tests - but they are nice
]
"**/__init__.py" = [
"D104", # Don't require module docstring in __init__.py
"F401", # Unused imports are fine: using __init__.py to expose them with implicit __ALL__
]
./justfile
- full source
# list available recipes
list:
@just --list --justfile {{justfile()}}
# remove pre-built rust and python libraries (excluding .venv)
clean:
- cargo clean
rm -rf .pytest_cache
rm -rf build
rm -rf dist
rm -rf wheelhouse
rm -rf .ruff_cache
find . -depth -type d -not -path "./.venv/*" -name "__pycache__" -exec rm -rf "{}" \;
find . -depth -type d -path "*.egg-info" -exec rm -rf "{}" \;
find . -type f -name "*.egg" -delete
find . -type f -name "*.so" -delete
# clean out coverage files
clean-cov:
find . -type f -name "*.profraw" -delete
rm -rf pycov
rm -rf rustcov
# clean, remove existing .venv and rebuild the venv with pip install -e .[dev]
reset: clean clean-cov
rm -rf .venv
python -m venv .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/pip install -e .[dev]
# lint rust files with fmt & clippy
lint-rust:
- cargo fmt --check
- cargo clippy --workspace
# test rust workspace
test-rust:
- cargo test --quiet --workspace
# lint and test rust
check-rust: lint-rust test-rust
# lint python with ruff
lint-python:
- .venv/bin/ruff check .
# test python
test-python:
- .venv/bin/pytest
# lint and test python
check-python: lint-python test-python
# lint and test both rust and python
check: check-rust check-python
#run coverage analysis on rust & python code
cov:
RUSTFLAGS="-Cinstrument-coverage" cargo build
RUSTFLAGS="-Cinstrument-coverage" LLVM_PROFILE_FILE="tests-%p-%m.profraw" cargo test
-mkdir rustcov
grcov . -s . --binary-path ./target/debug/ -t html,lcov --branch --ignore-not-existing --excl-line GRCOV_EXCL_LINE --output-path ./rustcov
pytest --cov --cov-report html:pycov --cov-report term
# serve rust coverage results on localhost:8002 (doesn't run coverage analysis)
rust-cov:
python -m http.server -d ./rustcov/html 8002
# serve python coverage results on localhost:8003 (doesn't run coverage analysis)
py-cov:
python -m http.server -d ./pycov 8003
# serve python docs on localhost:8000
py-docs:
mkdocs serve
#build and serve rust API docs on localhost:8001
rust-docs:
cargo doc
python -m http.server -d target/doc 8001
# build and test a wheel (a suitable venv must already by active!)
test-wheel: clean
cibuildwheel --only cp312-manylinux_x86_64