Code contributions that improve the quality and reliability of BenchBox are always welcomed.
This document provides guidelines and instructions for contributing.
- Python 3.10+
- Git
- uv (fast Python package manager)
- GitHub CLI (
gh) — required for the one-shot PR flow below
-
Clone the repository:
git clone https://github.com/joeharris76/BenchBox.git cd BenchBox -
Install the package in development mode:
make develop # equivalent to: uv sync --group dev -
Install the pre-commit + pre-push hooks:
pre-commit install
This installs two hooks (configured in
.pre-commit-config.yaml):- pre-commit: ruff format/check, codespell, YAML / markdown lint, timing-policy check.
- pre-push:
pr-preflight-fast-tests— whenBENCHBOX_PREPUSH=1, runs the path-aware fast-test lane so code PR pushes don't discover failures via the CI roundtrip. Existing clones that installed hooks before the pre-push stage was added should re-runpre-commit installonce.
If
pre-commit installerrors withCowardly refusing to install hooks with core.hooksPath setand points at the default.git/hookslocation, rungit config --unset-all core.hooksPathand try again — that's a redundant override left over from earlier tooling.
develop is the long-lived development branch and the repository's default branch; all changes land via PR. release is release-only (handled by the version-branch flow — see docs/operations/release-guide.md). PRs target develop and squash-merge with linear history.
Required CI on develop reports through ci-required-result. The umbrella uses .github/path-filters.yml to classify each PR: content-only PRs run content validation and skip Python fast tests, while code, infra, workflow, tooling, and unknown paths run the post-Step-3 lint/type + Ubuntu 3.12 fast-test gate. Reviews are not required for solo-dev work; auto-merge handles landing.
The canonical loop is branch → edit → preflight → make pr-open. Auto-merge takes the PR over the line once CI is green; you don't poll.
-
Create a feature branch off
develop. For parallel work, prefer a worktree so the main clone keepsdevelopchecked out:# Single-branch (simplest): git checkout develop && git pull git checkout -b feat/your-thing # Or, parallel-friendly — claim a retained pool worktree off develop # (run `make worktree-pool-init` once first): WORKTREE_PATH=$(make -s worktree-claim BRANCH=feat/your-thing | sed -n 's/^WORKTREE_PATH=//p') cd "$WORKTREE_PATH" && uv sync --group dev
-
Make your changes. Iterate with the fast lane:
make test # fast lane (~1 min) make format # ruff format . make lint # ruff check . make typecheck # ty check
The pre-commit hook re-runs format/lint/etc. at commit time, so if you forget, the commit will fix or block as appropriate.
-
Commit using Conventional Commits (
feat:,fix:,docs:,test:,chore:,ci:,refactor:):git add path/to/file.py path/to/test.py # explicit paths, never -A git commit -m "fix: resolve race in foo loader"
-
Run the local preflight, then open the PR with auto-merge in one shot:
make pr-preflight # local lint + path-aware content guard / fast tests make pr-open # push + gh pr create --base develop + gh pr merge --auto --squash
make pr-openrefuses to run fromdeveloporrelease. The PR will squash-merge the moment the required checks turn green. Don't poll for CI — auto-merge handles it. -
After merge, the remote branch auto-deletes (repo setting
delete_branch_on_merge). Sweep any stale local branches and worktrees with:make worktree-prune # removes worktrees whose branches are gone on originInspect open PRs at any time with
make pr-status.
There are two layers, and you only need the first:
Required gate (mirrors what CI enforces):
make pr-preflightThis runs the local lint/type gate, then uses .github/path-filters.yml to decide the fast-test lane. Content-only branches run the cheap content guard and skip Python fast tests; code, infra, workflow, tooling, and unknown paths run the fast tests. This mirrors the ci-required-result umbrella, so a green preflight almost always means a green CI. The opt-in pre-push git hook runs the same path-aware test portion when BENCHBOX_PREPUSH=1.
Optional thoroughness check — only useful when you've changed cross-cutting things (CI workflows, packaging, docs build, integration paths):
make ci-localThis runs the broader CI mirror:
| Step | Target |
|---|---|
| Lint + format + type checking | make ci-lint |
| Fast tests with coverage | make ci-test |
| Integration smoke tests | make test-integration-smoke |
| Documentation build | make ci-docs |
| Package build + install test | make test-package |
Or run any of those individually. Additional one-offs: make security-audit, make spellcheck, make docstring-coverage.
Skip make ci-local for everyday changes — make pr-preflight is the right gate. Auto-merge will block on any non-required check failure that is surfaced (e.g. doc build), so the cost of being wrong is just a re-push.
We use pytest for testing. Common testing commands:
make test # Fast local run (no coverage)
make test-unit # Run unit tests only
make test-integration # Run integration tests only
make test-fast # Run fast tests (< 1 sec)
make test-ci # CI profile with coverage + reportsThe default pytest.ini is optimized for quick local feedback. Use the CI profile when you need coverage, HTML reports,
or JUnit XML output.
uv run -- python -m pytest # Fast local run
uv run -- python -m pytest -c pytest-ci.ini # CI-equivalent instrumentation
uv run -- python -m pytest -m fast # Fast tests onlyThe collection hook applies simple defaults: tests in tests/unit automatically receive the unit and fast markers, tests/integration receive integration with a default medium speed, and files in tests/performance are marked performance and slow. Add explicit markers to override those defaults for individual tests.
PySpark 4.x only supports Java 17 and 21, so we provide scripts/with_supported_java.sh to automatically pick a compatible JDK (it prefers macOS’ /usr/libexec/java_home -v 21, then 17, and falls back to whatever JAVA_HOME you already exported). No shims are added for older runtimes.
To run every PySpark unit test:
make test-pysparkThe target shells out to the helper script, which validates the Java version and executes tests/unit/platforms/dataframe/test_pyspark_df.py. You can also wrap any BenchBox command that touches PySpark - benchmarks, smoke runs, etc.-like this:
scripts/with_supported_java.sh benchbox run --platform pyspark-df --benchmark tpch --scale 0.01 --non-interactiveCI jobs install Temurin 21 and invoke the same helpers so PySpark stays active everywhere automatically.
To add a new benchmark, create a new file in the benchbox directory and implement the BaseBenchmark interface. See existing benchmarks for examples.
BenchBox uses a family-based architecture for DataFrame platforms. To add a new platform:
- Determine the family: Expression (Polars, PySpark) or Pandas (Pandas, Modin, cuDF)
- Implement DataFrameContext: Provides table access and family-specific helpers
- Implement platform adapter: Handles data loading and query execution
- Register the platform: Add to
DATAFRAME_ADAPTERSregistry - Add tests: Unit tests and integration tests
See Adding a DataFrame Platform for detailed instructions.
When implementing new DataFrame queries:
- Each query needs both
expression_implandpandas_implfunctions - Use the
DataFrameContextprotocol for table access - Use
ctx.colandctx.litfor expression family implementations - Use string-based column access for pandas family implementations
- Test both families independently
We use ruff for fast Python linting and formatting with a 120-character line length. Type hints are required and enforced with ty. Our pre-commit hooks will help enforce these standards.
- Line length: 120 characters
- Formatter: ruff (replaces black and isort)
- Linter: ruff (replaces flake8)
- Type checker: ty
Please update documentation when adding or modifying features. We use Sphinx for generating documentation.
External contributions land via PR against develop (squash-merge). Releases
are cut by maintainers via the version-branch flow documented in
docs/operations/release-guide.md:
make bump VERSION=X.Y.Zandmake changelog-draft VERSION=X.Y.Zondevelop.make release-prepare VERSION=X.Y.ZcutsvX.Y.Zfromdevelopwith a release-curated tree and opens a PR againstrelease.- Squash-merge the PR; tag
release;release.ymlpublishes to PyPI. make release-rebase-develop VERSION=X.Y.Zrebasesdeveloponto the release-shapedreleasebranch.
We follow semantic versioning.
By contributing to BenchBox, you agree that your contributions will be licensed under the project's MIT license.