This guide covers the development setup, build system, and workflows for contributing to PyEMD.
- uv >= 0.9.18 - Fast Python package installer and resolver
- Git
- C++ compiler (gcc, clang, or MSVC depending on platform)
- Ninja build system (usually auto-installed by meson-python)
-
Clone the repository with full history (needed for version detection):
git clone https://github.com/wmayner/pyemd.git cd pyemd -
Install uv (if not already installed):
curl -LsSf https://astral.sh/uv/install.sh | sh -
Set up the development environment:
uv sync --all-extras
This installs all dependencies (including test and distribution tools) and creates a virtual environment at .venv/.
PyEMD uses modern Python packaging tools:
- Modern build backend for Python packages with compiled extensions
- Replaced setuptools for better performance and cleaner configuration
- Automatically handles Cython → C++ compilation
- Configuration in
meson.build
- Cross-platform build system that handles the actual C++/Cython compilation
- Fast, parallel builds with Ninja backend
- Cleaner than setuptools for compiled extensions
- Fast Python package installer and resolver
- Manages dependencies via
pyproject.tomland locks them inuv.lock - Replaces pip/conda for development
pyproject.toml: Python package metadata, dependencies, and build configurationmeson.build: Build instructions for Meson (how to compile the extension)uv.lock: Locked dependency versions for reproducible buildsMakefile: Convenient shortcuts for common development taskssetup.py.deprecated: Old setuptools config (kept for reference only)
Build wheels and source distributions:
uv buildThis creates:
dist/pyemd-*.whl- Binary wheel for your platformdist/pyemd-*.tar.gz- Source distribution (requires git)
Use cibuildwheel to build wheels for Linux, macOS, and Windows:
uv run cibuildwheel --platform linuxOr use the Makefile shortcut:
make dist-build-wheelsuv sync --all-extrasThis installs:
- Runtime dependencies (numpy)
- Test dependencies (pytest)
- Distribution tools (build, cibuildwheel, twine)
- Development tools (ipython)
This project uses a wheel-based development workflow. The Makefile automates the build and installation process:
# One-time setup: build and install
make develop
# Run tests
make test
# Or run tests manually
.venv/bin/pytestYou can also use uv run for quick commands:
uv run python -c "import pyemd; print(pyemd.__version__)"
uv run pytestAfter making code changes:
make develop # Rebuild and reinstall
make test # Run testsOr manually:
make clean
uv build --wheel
uv pip install --force-reinstall --no-deps dist/pyemd-*.whl
.venv/bin/pytestWhy wheel-based development?
For packages with C++ extensions like pyemd, wheel-based development is the most reliable approach because:
- No rebuild-on-import overhead
- No dependency on build directory structure at runtime
- Clearer separation between build and runtime
- Avoids PATH issues with build tools in subprocesses
Meson-python's editable installs use a rebuild-on-import mechanism that requires build tools (cython, ninja) to be available in the system PATH whenever Python starts. This creates environment issues because the rebuild runs in a subprocess that doesn't inherit the virtualenv's PATH.
We've configured package = false in pyproject.toml to prevent automatic editable installs, which allows uv run to work with the installed wheel.
make cleanThis removes:
__pycache__directories- Compiled extensions (
.so,.dylib) - Build directories (
build/,.mesonpy-*) - Egg info
Dependencies are managed in pyproject.toml and locked in uv.lock.
-
Edit
pyproject.toml:[project] dependencies = ["numpy >= 1.9.0", "new-package >= 1.0"]
-
Update the lockfile:
uv sync
-
Commit both
pyproject.tomlanduv.lock
Update all dependencies to their latest compatible versions:
uv sync --upgradeUpdate a specific package:
uv sync --upgrade-package numpyAlways commit uv.lock after updating dependencies to ensure reproducible builds.
uv run pytestuv run pytest test/test_pyemd.py::test_emd_1 -vuv run pytest --cov=pyemd --cov-report=htmlPython changes in src/pyemd/__init__.py or src/pyemd/emd.pyx require rebuilding:
uv build --wheel
uv pip install --force-reinstall dist/pyemd-*.whl
uv run pytestChanges to C++ headers in src/pyemd/lib/ require a full rebuild:
make clean
uv build --wheel
uv pip install --force-reinstall dist/pyemd-*.whl
uv run pytestChanges to meson.build or pyproject.toml [build-system] section:
make clean
uv sync # Rebuild with new configPyEMD uses setuptools_scm for git-based versioning:
- Version is automatically derived from git tags
- Development versions include commit hash (e.g.,
0.5.1.dev87+g255824f) - Release versions match git tags (e.g.,
0.5.1)
To create a release:
git tag v0.5.2
git push --tags-
Ensure all changes are committed:
git status
-
Run tests:
make test -
Build locally to verify:
make dist-build-local
-
Tag the release:
git tag v0.5.2 git push --tags
-
GitHub Actions will automatically build wheels for all platforms
-
Download artifacts from GitHub Actions
-
Sign and upload to PyPI:
make dist-sign make dist-upload
build_wheels.yml:
- Builds wheels for Linux, Windows, and macOS
- Uses
cibuildwheelwith uv as build frontend - Runs on every push and pull request
- Artifacts uploaded as build artifacts
make_sdist.yml:
- Builds source distribution
- Requires git history for version detection
- Uploads to GitHub Actions artifacts
Test wheel building locally:
uv run cibuildwheel --platform linux --config-file pyproject.toml"No module named 'pyemd'":
- The package needs to be rebuilt and installed after changes
- Run:
uv build --wheel && uv pip install --force-reinstall dist/pyemd-*.whl
"meson-python editable install failed":
- Editable installs with meson-python have limitations
- Use wheel installs for development instead
- See: https://mesonbuild.com/meson-python/how-to-guides/editable-installs.html
"setuptools_scm version not found":
- Ensure you cloned with full git history:
git fetch --unshallow - Or set an environment variable:
SETUPTOOLS_SCM_PRETEND_VERSION=0.5.1
Compiler warnings about sign comparison:
- These are from the upstream C++ library code
- They're non-fatal and don't affect functionality
- Can be safely ignored
"Command 'uv' not found":
curl -LsSf https://astral.sh/uv/install.sh | sh
# Then restart your terminal"ninja not found":
- Usually auto-installed by meson-python
- Manual install:
brew install ninja(macOS) orapt install ninja-build(Ubuntu)
- Issues: https://github.com/wmayner/pyemd/issues
- meson-python docs: https://mesonbuild.com/meson-python/
- uv docs: https://docs.astral.sh/uv/
pyemd/
├── src/pyemd/
│ ├── __init__.py # Python API
│ ├── emd.pyx # Cython wrapper
│ └── lib/ # C++ implementation (header-only)
│ ├── emd_hat.hpp # Main EMD algorithm
│ └── ... # Supporting headers
├── test/ # Test suite
├── meson.build # Build configuration
├── pyproject.toml # Project metadata & dependencies
└── uv.lock # Locked dependencies
Build flow:
1. meson-python reads meson.build
2. Meson compiles emd.pyx → emd.cpp (via Cython)
3. C++ compiler builds emd.cpp + lib/*.hpp → emd.so
4. Wheel packaged with Python files
- Scientific Python Development Guide - Compiled Packaging
- NumPy 1.26.0 Release Notes (meson-python migration)
- Meson-Python Documentation
- uv Documentation
- PEP 517 - Build system interface
- PEP 518 - pyproject.toml specification