Contributing to python-libphash¶
Thanks for considering a contribution. This file covers building from source, what a pull request needs before it can be reviewed, and how versions and releases work. The C library itself is libphash: a change to a hash, a decoder or the C API belongs there, and reaches this binding with the next libphash release.
Getting started¶
The project is developed with uv. A C compiler is needed; CMake and Ninja come from PyPI. On Windows the compiler is MSVC (Visual Studio Build Tools).
git clone https://github.com/gudoshnikovn/python-libphash.git
cd python-libphash
git submodule update --init --recursive # libphash and its vendored decoders
uv sync --all-extras --inexact # builds the extension in editable mode
uv run pytest
native/libphash is libphash as a git submodule, pinned to one of its release tags. The
build runs libphash's own CMake project and links the static library into the extension
module, so an installed wheel needs no system libphash. After a change to the C side or
to tools/, rebuild with uv sync --all-extras --inexact --reinstall-package
python-libphash.
uv sync without --inexact removes from .venv the packages the lock file does not
list.
Before opening a pull request¶
Each of these must pass; CI runs them all on every pull request:
uv run pytest
uv run ruff check . && uv run ruff format --check .
uv run mypy && uv run basedpyright # both strict, over src/, tests/ and tools/
uv run python tools/stub.py --check # src/libphash/_native.pyi matches the module
uv run typos
- Coverage is 100 % of lines and branches, combined over the platforms CI runs on:
A line that runs only on Windows or only on POSIX counts once combined, so a local
report on one platform can show it as missed.
- Every mutant is caught. CI runs mutmut over
src/libphash/ weekly and on pull requests that change the package. A surviving
mutant fails the run unless tests/equivalent_mutants.json lists it with the reason
it cannot be caught. Locally (Linux and macOS):
- The binding matches C.
tests/test_c_parity.pybuilds a small C program against the same libphash install and requires the binding's hashes and error codes to equal it for every fixture, setting and pixel layout. It needs CMake; without it the tests skip locally and fail in CI. - A change to the stub's inputs regenerates it:
uv run python tools/stub.py. - No new copy in Python, no slower call. A pull request that changes the package
or the build runs the benchmarks of its base and of its head on one runner
(
.github/workflows/bench.yml). The Python allocations of every load must stay as they are, and a batch within libphash's memory bound; a call whose cost over C grows is reported as a warning. Locally, against a checkout of the base:
uv run python -m benchmarks --blocks overhead,memory --name head --out results
uv run python -m benchmarks.gate results/base.json results/head.json
Sanitizer builds (AddressSanitizer with UndefinedBehaviorSanitizer, ThreadSanitizer) and
the fuzz targets need Linux with Clang; tools/sanitizers.py and tools/fuzz.py say
how to run them.
Style¶
- Code, comments, docstrings and documentation are in English, with American spelling.
- ruff checks and formats the code. An exception to a
rule is a per-file ignore with a comment, or
# noqa: CODE -- reason; never a barenoqa. - Docstrings follow PEP 257: a one-line summary in the imperative, then the details.
- Public signatures are fully typed and use protocols rather than
Any. A type a caller needs to annotate their own code is exported fromlibphash. - The public API is what
libphash/__init__.pyexports; every other module is private (_name.py) exceptexceptions.py. - Comments and documentation describe the code as it is, in the present tense. A test comment states the contract the test holds.
Commits and pull requests¶
- Commit messages: a short summary line, in English, prefixed with the kind of change
as in the repository's history (
fix:,feat:,test:,docs:,ci:,build:,refactor:,perf:), and a body when the reason is not obvious from the diff. - A change a user of the package can observe (new API, changed behavior, a fixed bug, a
changed default) adds a line to
CHANGELOG.mdunder[Unreleased]. A change that breaks existing callers goes underBREAKING CHANGES, with the way to adapt or a link toMIGRATION.md. - Pull requests target
main, one logical change each. Fork the repository, branch offmain, and open the pull request from your branch.
Versions¶
The version has four numbers, X.Y.Z.N. X.Y.Z is the version of the bundled
libphash; N is the revision of the binding on top of it, starting at 0 for each
libphash release and always written out (2.0.1.0, then 2.0.1.1). A change to the
binding alone raises N; a new libphash release starts a new X.Y.Z.0. Tags are
vX.Y.Z.N, and src/libphash/_version.py is the only place the version is written.
Hash values depend only on X.Y.Z and the configuration: a binding revision never
changes a hash.
Supported platforms and Python versions¶
- Wheels are built for the platforms libphash's own CI tests: Linux x86-64 and arm64 (manylinux, glibc), macOS arm64 and Windows x86-64. Elsewhere pip builds the sdist, which needs a C compiler. x86-64 wheels need a CPU with SSE4.2 and POPCNT (x86-64-v2).
- Python: every CPython version from the oldest in
requires-pythonto the newest release, the free-threaded builds included. A Python version that reaches its end of life is dropped in the nextX.Y.Z.0. - Releases: only the newest release receives fixes. A fix to libphash or one of its
vendored decoders reaches this package as the next
X.Y.Z.0, or as a binding revision when the binding alone changes.
Releases¶
A release is cut from a release/vX.Y.Z.N branch. The version in _version.py, the
CHANGELOG.md entry with its date and the tag must agree; the release workflow
(release.yml) checks them, and that the tag is signed and the submodule sits on
libphash's tag, before it uploads anything.
Each libphash release gets its binding:
- Move
native/libphashto the new tag (git -C native/libphash checkout vX.Y.Z, with its submodules updated). - Set the version to
X.Y.Z.0and add theCHANGELOG.mdentry, taking from libphash's changelog what a Python user observes. - Regenerate the stub (
uv run python tools/stub.py) and adapt the binding to any new or changed C API;tests/test_cdef.pyfails for a header function that is neither wrapped nor listed as deliberately unwrapped. - Run CI, the wheels workflow included (it runs when the submodule changes).
Reporting a security issue¶
Do not open a public issue or pull request for a vulnerability. See
SECURITY.md for the private channel.
License¶
By contributing, you agree that your contribution is licensed under the project's MIT license.