|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## What this package is |
| 6 | + |
| 7 | +`suitesparse-graphblas` is a low-level Python CFFI binding around the C library |
| 8 | +[SuiteSparse:GraphBLAS](https://github.com/DrTimothyAldenDavis/GraphBLAS). It exposes the |
| 9 | +raw `ffi` and `lib` symbols and, on top of them, a small **functional API** in |
| 10 | +`suitesparse_graphblas.matrix`, `suitesparse_graphblas.vector`, and |
| 11 | +`suitesparse_graphblas.scalar`. These three modules are the **main entry point for users |
| 12 | +of this library directly** — they are module-level functions operating on opaque CFFI |
| 13 | +handles, e.g. `A = matrix.new(lib.GrB_BOOL, 3, 3); matrix.set_bool(A, True, 2, 2)`. Higher-level |
| 14 | +syntax wrappers ([python-graphblas](https://github.com/python-graphblas/python-graphblas) |
| 15 | +and [pygraphblas](https://github.com/Graphegon/pygraphblas)) build on top of this same |
| 16 | +package for users who want a more Pythonic, OO-style interface. |
| 17 | + |
| 18 | +The currently targeted SuiteSparse:GraphBLAS version is pinned in `GB_VERSION.txt`. |
| 19 | + |
| 20 | +## Building from source |
| 21 | + |
| 22 | +Building requires a working SuiteSparse:GraphBLAS C library on the system. Point at it via |
| 23 | +`GraphBLAS_ROOT` (must contain `include/GraphBLAS.h` and `lib/`): |
| 24 | + |
| 25 | +```bash |
| 26 | +export GraphBLAS_ROOT="/path/to/graphblas" # or `$(brew --prefix suitesparse)` on macOS |
| 27 | +pip install -e . --no-deps # editable dev install |
| 28 | +``` |
| 29 | + |
| 30 | +If `GraphBLAS_ROOT` is unset, the build falls back to: |
| 31 | +- `C:\GraphBLAS` on Windows |
| 32 | +- `/usr/local` if `/usr/local/include/suitesparse` exists (the path used by `suitesparse.sh`) |
| 33 | +- `sys.prefix` (works for conda-installed `graphblas`) |
| 34 | + |
| 35 | +The CI build uses conda-forge `graphblas=$(cat GB_VERSION.txt)`. To build SuiteSparse from |
| 36 | +source instead, run `bash suitesparse.sh refs/tags/$(cat GB_VERSION.txt).0`. That script also |
| 37 | +honors `SUITESPARSE_FAST_BUILD` / `SUITESPARSE_FASTEST_BUILD` env vars to disable many type |
| 38 | +specializations for much faster local builds. |
| 39 | + |
| 40 | +## Tests, lint, and other commands |
| 41 | + |
| 42 | +Testing requires the compiled CFFI extension and the SuiteSparse:GraphBLAS C library, so |
| 43 | +tests should be run inside the Docker container. Build the image once (this compiles |
| 44 | +GraphBLAS from source and takes several minutes), then run tests against it: |
| 45 | + |
| 46 | +```bash |
| 47 | +# Build the test image (uses the psg stage which includes pytest) |
| 48 | +docker build --target psg \ |
| 49 | + --build-arg SUITESPARSE=v$(cat GB_VERSION.txt) \ |
| 50 | + --build-arg VERSION=99.0.0.0 \ |
| 51 | + -t psg-test . |
| 52 | + |
| 53 | +# Run the full test suite (unit tests + all doctests) |
| 54 | +docker run --rm -w /tmp psg-test pytest --pyargs suitesparse_graphblas --doctest-modules -v |
| 55 | + |
| 56 | +# Run only the unit tests (no doctests) |
| 57 | +docker run --rm -w /tmp psg-test pytest --pyargs suitesparse_graphblas.tests -v |
| 58 | + |
| 59 | +# Run a single test file |
| 60 | +docker run --rm -w /tmp psg-test pytest --pyargs suitesparse_graphblas.tests.test_scalar -v |
| 61 | + |
| 62 | +# Run by test name substring |
| 63 | +docker run --rm -w /tmp psg-test pytest --pyargs suitesparse_graphblas --doctest-modules -k test_print_jit_config -v |
| 64 | + |
| 65 | +# Run linters/formatters (locally, no C library needed) |
| 66 | +pre-commit run --all-files |
| 67 | +``` |
| 68 | + |
| 69 | +Rebuild the Docker image after making changes — the `ADD . /psg` layer picks up the |
| 70 | +current working tree. The SuiteSparse compilation layer is cached so rebuilds are fast. |
| 71 | + |
| 72 | +`conftest.py` calls `suitesparse_graphblas.initialize()` once per session — `GrB_init` may |
| 73 | +only be called once per process, so `test_initialize.py` is run as a separate process in CI: |
| 74 | + |
| 75 | +```bash |
| 76 | +docker run --rm -w /tmp psg-test python3 -m suitesparse_graphblas.tests.test_initialize |
| 77 | +``` |
| 78 | + |
| 79 | +Coverage runs in CI use `CYTHON_COVERAGE=true` so the Cython `utils.pyx` extension is |
| 80 | +recompiled with line tracing. |
| 81 | + |
| 82 | +## Architecture: how the binding is generated and assembled |
| 83 | + |
| 84 | +There are two layers of generated code, and understanding them is essential before touching |
| 85 | +anything related to types, defines, or the FFI surface. |
| 86 | + |
| 87 | +### 1. Header generation — `suitesparse_graphblas/create_headers.py` |
| 88 | + |
| 89 | +This script regenerates **`suitesparse_graphblas.h`**, **`suitesparse_graphblas_no_complex.h`**, |
| 90 | +and **`source.c`** from an upstream `GraphBLAS.h`. It: |
| 91 | + |
| 92 | +- Copies `GraphBLAS.h` from the install, runs the C preprocessor (using pycparser's |
| 93 | + `fake_libc_include`), and parses the result with pycparser. |
| 94 | +- Emits a cleaned-up header that cffi can `cdef()`, plus a complex-free variant for |
| 95 | + platforms (notably MSVC) where `_Complex` types don't work. |
| 96 | +- Manually tracks `DEFINES`, `CHAR_DEFINES`, `IGNORE_DEFINES`, and `DEPRECATED` sets. |
| 97 | + **When updating to a new SuiteSparse:GraphBLAS version, these lists are the things most |
| 98 | + likely to need editing.** New macros, new deprecations, or removed symbols all flow |
| 99 | + through here. |
| 100 | +- CI runs this script and `git diff --exit-code` to fail the build if the committed headers |
| 101 | + drift from upstream. Re-running it locally and committing the result is the standard fix. |
| 102 | + |
| 103 | +### 2. CFFI compilation — `build_graphblas_cffi.py` |
| 104 | + |
| 105 | +`ffibuilder` calls `set_source()` with `source.c` and `cdef()` with `suitesparse_graphblas.h` |
| 106 | +to produce the compiled extension `suitesparse_graphblas._graphblas` (which exposes `ffi` |
| 107 | +and `lib`). On Windows it instead emits a `_graphblas.c` file and runs a textual patch |
| 108 | +(`float _Complex` → `_Fcomplex`, `double _Complex` → `_Dcomplex`, `-DGxB_HAVE_COMPLEX_MSVC`) |
| 109 | +because cffi cannot represent MSVC's complex types — see `get_extension()` for the patching |
| 110 | +logic. `setup.py` chooses between the cffi-driven and Extension-driven paths based on |
| 111 | +`build_graphblas_cffi.is_win`. |
| 112 | + |
| 113 | +`setup.py` also cythonizes any `*.pyx` under `suitesparse_graphblas/` (currently |
| 114 | +`utils.pyx`). When Cython is unavailable, it falls back to checked-in `*.c` files; the |
| 115 | +build will refuse to proceed if any are missing. |
| 116 | + |
| 117 | +### 3. Python layer (`suitesparse_graphblas/__init__.py` and friends) |
| 118 | + |
| 119 | +The package re-exports `ffi`/`lib` from the compiled extension and adds only thin helpers: |
| 120 | + |
| 121 | +- `initialize(blocking=False, memory_manager="numpy")` — must be called exactly once before |
| 122 | + any GraphBLAS calls. The `numpy` memory manager routes allocation through |
| 123 | + `PyDataMem_NEW`/`FREE` (defined in `utils.pyx::call_gxb_init`) so buffers can be claimed |
| 124 | + zero-copy by NumPy and tracked by `tracemalloc`. |
| 125 | +- `check_status(obj, info)` — central error handler. Maps `GrB_Info` codes to exception |
| 126 | + classes in `exceptions.py` and pulls the human-readable message via the type-specific |
| 127 | + `*_error()` function, looked up by cdata cname in `_error_func_lookup`. |
| 128 | +- `vararg(val)` — a workaround for variadic GraphBLAS calls on `osx-arm64` and `ppc64le` |
| 129 | + where ARM64 calling conventions force variadic args onto the stack. Prefer the |
| 130 | + non-variadic typed variants (e.g. `GxB_Matrix_Option_get_INT32`) when they exist. |
| 131 | +- `libget(name)` — fallback that retries a `GrB_*` lookup as `GxB_*` when SuiteSparse moves |
| 132 | + a symbol between standard and extension namespaces. |
| 133 | +- `burble` — context manager / global toggle for `GxB_BURBLE` diagnostic output. |
| 134 | +- `matrix.py`, `vector.py`, `scalar.py` — **the functional API** of the package, and the |
| 135 | + primary user-facing surface for code that uses `suitesparse-graphblas` directly. Each |
| 136 | + module follows the same convention: `<module>.new(...)` returns an `ffi.gc`-managed |
| 137 | + cdata handle (`GrB_Matrix*`, `GrB_Vector*`, or `GxB_Scalar*`) and every other function |
| 138 | + takes that handle as its first argument and routes errors through `check_status`. The |
| 139 | + design is deliberately functional rather than class-based so the same handles can be |
| 140 | + passed through higher-level wrappers without object-identity friction. When adding |
| 141 | + features to this package, this is the layer where new user-facing helpers belong. |
| 142 | +- `io/serialize.py`, `io/binary.py` — supporting I/O helpers (compressed serialize / |
| 143 | + deserialize, binary format read/write). `matrix.py` and `vector.py` already re-export |
| 144 | + `serialize` / `deserialize` from `io/serialize.py` so callers can reach them as |
| 145 | + `matrix.serialize(A)` etc. |
| 146 | + |
| 147 | +### 4. `utils.pyx` and free-threading |
| 148 | + |
| 149 | +The Cython module exists primarily to (a) call `GxB_init` with NumPy's allocators by |
| 150 | +casting the cffi function pointer through `uintptr_t` into a real C function pointer, and |
| 151 | +(b) wrap NumPy buffers around GraphBLAS-allocated memory with `claim_buffer` / |
| 152 | +`unclaim_buffer`, transferring ownership of the underlying allocation to NumPy. |
| 153 | + |
| 154 | +The file is marked `freethreading_compatible=True`. The package does nothing special for |
| 155 | +free-threading itself — correctness depends on SuiteSparse:GraphBLAS being thread-safe, |
| 156 | +which it is required to be. |
| 157 | + |
| 158 | +## Style and tooling |
| 159 | + |
| 160 | +- Black, isort, flake8 (config in `.flake8`, line length 100, double quotes), pyupgrade |
| 161 | + (`--py311-plus`), autoflake, shellcheck — all wired through `pre-commit`. Run |
| 162 | + `pre-commit run --all-files` before pushing. |
| 163 | +- `pre-commit` also blocks direct commits to `main`. |
| 164 | +- Python ≥ 3.11. NumPy ≥ 2.0 is required at build time (CFFI extension), ≥ 1.24 at runtime. |
| 165 | +- Generated headers (`suitesparse_graphblas.h`, `suitesparse_graphblas_no_complex.h`, |
| 166 | + `source.c`) are checked in and **must be regenerated via `create_headers.py`** rather |
| 167 | + than hand-edited. CI enforces this. |
0 commit comments