Skip to content

Contributing

YAMLRocks is an open-source project, and contributions are welcome, whether that is a bug report, a documentation fix, or a pull request. This page covers the local development workflow. The full guidelines also live in CONTRIBUTING.md in the repository.

The fastest way to start is the dev container: open the repository in a GitHub Codespace or in VS Code with the Dev Containers extension, and the toolchain is set up for you.

To set things up by hand you need a Rust toolchain and uv. uv manages the Python version (3.12 or newer), the virtual environment, and every dependency from pyproject.toml:

Terminal window
# Install all development dependencies into a managed virtual environment.
uv sync
# Activate it so `just` (shipped as a dev dependency) is on PATH.
source .venv/bin/activate
# Build and install the extension (release mode; rerun after Rust changes).
just develop

just develop compiles the Rust crate and installs yamlrocks into the managed environment as an editable build. Re-run it after changing any Rust code. For a faster, less-optimized build during tight iteration loops, use just develop-debug.

just is the primary interface for everyday work: every common task (build, test, lint, type-check, docs) is a recipe that wraps the exact command CI runs, so you rarely need to remember the underlying invocation. It ships with the dev dependencies, so there is nothing extra to install.

Terminal window
just # list every recipe
just develop # build the extension (rerun after Rust changes)
just test # run the Python suite (just test -k anchors to filter)
just check # the full local gate: build, lint, types, clippy, tests, docs

If you would rather not activate the venv, prefix any recipe with uv run --no-sync, for example uv run --no-sync just test. Every recipe maps to the raw commands shown throughout this page, so either style works.

The suite is grouped by capability under tests/ and always runs under a memory cap (configured in tests/conftest.py). just test runs it; during development it is good practice to add a shell-level guard as well, so a parser bug can never exhaust the host:

Terminal window
just test # or: uv run --no-sync pytest
just test -k anchors # extra args pass straight through to pytest
# with an explicit shell guard:
timeout 120 bash -c 'ulimit -v 3000000; uv run --no-sync pytest'

It includes the YAML test suite (a git submodule), golden-file snapshot tests, fuzz tests, security tests, and memory/refcount checks. See tests/README.md for the layout.

The pure-Rust engine (scanner, parser, resolver, decode, encode, and the include/schema helpers) carries direct unit tests alongside the Python suite, which covers the PyO3 boundary on top. They need no Python and run fast:

Terminal window
just test-rust # or: cargo test --lib

A dedicated realworld category parses large public configurations across ecosystems (Home Assistant, ESPHome, Ansible, Kubernetes, Docker Compose) and asserts every file parses and round-trips byte-for-byte. The configs are git submodules, so they are opt-in and the category auto-skips when they are absent:

Terminal window
git submodule update --init # fetch the config repos once
uv run --no-sync pytest tests/realworld -m realworld # run just this category

The Rust core is exercised both by its own Rust unit tests and, end-to-end, through the Python suite. Coverage is measured by instrumenting the build with cargo-llvm-cov and running both cargo test and pytest against it. The whole flow is wrapped in one recipe:

Terminal window
rustup component add llvm-tools-preview # one-time: the llvm coverage tools
cargo install cargo-llvm-cov # one-time
just coverage # instrument, run both suites, print a summary

just coverage runs the instrumented cargo test --lib and pytest under a fresh profile and prints a line-coverage summary. Check out the real-world submodules first (git submodule update --init) for a representative number; CI runs the same flow and fails when line coverage drops below 90% (cargo llvm-cov report --fail-under-lines 90).

There are two layers of performance tooling. just bench runs a one-off report comparing YAMLRocks against PyYAML, ruamel.yaml, and yamlium, while just codspeed runs YAMLRocks’s own operations through CodSpeed, which also runs on every pull request and comments on any regression. Build in release mode (just develop) before measuring; debug builds are far slower.

Terminal window
just bench # comparison report vs other libraries
just codspeed # YAMLRocks's own operations (local walltime; CI instruments)

See the performance guide for the headline numbers and how to read them.

tests/robustness/test_fuzz.py is a fast, always-on property check. For deeper, coverage-guided fuzzing of the Rust parser there are four cargo-fuzz targets under fuzz/:

TargetWhat it drivesContract
parsescanner → parser → composer (the round-trip AST)never panic or hang
decodethe fast loads path and fast dumps, under both schemasnever panic or hang
roundtripcompose → emit → re-compose (the round-trip emitter)never panic or hang
differentialloads(dumps(loads(x))) must equal loads(x)never silently corrupt data
Terminal window
cargo install cargo-fuzz # once; needs a nightly toolchain
just fuzz 60 # fuzz `parse` for 60s (the default target)
just fuzz 60 differential # fuzz any target by name

The first three targets check that no input crashes the parser. differential checks something a crash-only target cannot: that dumps never emits YAML which loads reads back as different data (a mis-quoted string re-resolving to a bool, a float losing precision). That shows up as wrong values, not a panic, so only comparing the two trees surfaces it. ClusterFuzzLite builds and runs every target for a short batch on each pull request. A parse/decode/roundtrip crash is a parser bug; a differential failure is a correctness bug.

All checks are wired into prek (a drop-in pre-commit runner). just precommit runs the exact hook set CI runs, and just check runs the full local gate (build, every hook, the Rust and Python suites, and the documented examples) in one go:

Terminal window
just precommit # every pre-commit hook (the set CI runs)
just check # the full gate: build, hooks, tests, examples

You can also run each linter on its own:

Terminal window
just lint # ruff check + ruff format --check (Python lint + format)
just typecheck # mypy and ty (Python typing)
just clippy # cargo clippy --all-targets -D warnings (Rust lint)
just fmt # auto-format Python and Rust in place
just spellcheck # codespell

CI enforces all of the above, so make sure they pass before opening a pull request.

Advisory and supply-chain checks run on a schedule and whenever a lockfile changes. Run them locally with just audit (all ecosystems at once), or one at a time with just audit-rust, just audit-python, and just audit-docs.

  • Match the style of the surrounding code; the codebase favors small, well-named functions and explicit error handling.
  • Add tests for new behavior. Round-trip changes must preserve byte-for-byte fidelity for unmodified documents (the YAML test suite enforces this).
  • Update the documentation under docs/ when adding or changing a user-facing feature, and keep its examples runnable: just examples runs every documented example and verifies its output, and just docs-dev serves the site locally with live reload.
  • There is no changelog to edit: release notes are drafted automatically by Release Drafter from merged pull requests, grouped by label. A clear pull request title becomes the release-note line.
  • Architecture: how the parser is put together.
  • Security: the threat model and how to report issues.
  • License: the terms YAMLRocks is distributed under.