Skip to content

Latest commit

 

History

History
326 lines (246 loc) · 10.1 KB

File metadata and controls

326 lines (246 loc) · 10.1 KB

Contributing to cachekit

Thank you for your interest in contributing to cachekit! This document provides guidelines and instructions for contributing to the project.

Code of Conduct

This project follows a standard code of conduct. Please be respectful and professional in all interactions.

Getting Started

Prerequisites

  • Python 3.10 or higher
  • Rust 1.80 or higher
  • Redis 5.0 or higher (for testing)
  • uv (recommended for dependency management)
  • pytest-redis 3.0+ (automatically installed with uv sync)

Development Setup

  1. Clone the repository

    git clone https://github.com/cachekit-io/cachekit-py.git
    cd cachekit-py
  2. Install dependencies

    uv sync && make install
  3. Run tests to verify setup

    make test

Development Workflow

Before You Start

  1. Check existing issues to avoid duplicate work
  2. For major changes, open an issue first to discuss the approach
  3. Fork the repository and create a feature branch

Making Changes

  1. Create a feature branch

    git checkout -b feature/your-feature-name
  2. Make your changes

    • Write clear, focused commits
    • Follow the code style guidelines (see below)
    • Add tests for new functionality
    • Update documentation as needed
  3. Run quality checks

    make quick-check  # format + lint + critical tests
  4. Run full test suite

    make check  # format + lint + type-check + all tests
  5. Commit your changes

    git add <files>
    git commit -m "feat: add new feature"

Code Style Guidelines

Python

  • Line length: 129 characters maximum
  • Formatter: Ruff (runs automatically with make format)
  • Linter: Ruff (runs automatically with make lint)
  • Type checker: basedpyright (standard mode, zero errors enforced)
  • Type hints: Required for all public APIs
  • Python 3.10+ compatibility: Use from __future__ import annotations for modern union syntax
  • Docstrings: Google style for public functions/classes
  • Imports: Absolute imports only

Example:

from __future__ import annotations

from cachekit import cache

@cache
def example_function(param: str) -> dict[str, str]:
    """Brief description of what this does.

    Args:
        param: Description of parameter

    Returns:
        Description of return value
    """
    return {"result": param}

Rust

  • Formatter: rustfmt (standard settings)
  • Linter: clippy
  • Documentation: Required for public APIs

Code Principles

Follow KISS, DRY, YAGNI, SOLID:

  • Keep It Simple - Prefer simple solutions over clever ones
  • Don't Repeat Yourself - Extract common patterns (2+ uses minimum)
  • You Aren't Gonna Need It - Build only what's needed now
  • Single Responsibility - Each component has one clear purpose

Prefer:

  • Guard clauses over deep nesting
  • Early returns for readability
  • Explicit over implicit
  • Composition over inheritance

Testing

Writing Tests

  • Critical tests: Essential functionality in tests/critical/ (must always pass)
  • Unit tests: Fast, isolated tests in tests/unit/
  • Integration tests: Tests requiring Redis in tests/integration/
  • Performance tests: Benchmarks in tests/performance/

Test markers:

import pytest

@pytest.mark.critical
def test_cache_basic_functionality():
    """Critical test - must pass for library to be usable"""
    pass

@pytest.mark.unit
def test_cache_configuration():
    """Test cache configuration validation"""
    pass

@pytest.mark.integration
def test_redis_connection():
    """Test actual Redis connection (requires Redis running)"""
    pass

Redis Test Isolation: All tests requiring Redis must use pytest-redis for proper isolation:

from ..utils.redis_test_helpers import RedisIsolationMixin

class TestMyCacheFeature(RedisIsolationMixin):
    def test_feature(self):
        # Test automatically gets isolated Redis instance
        pass

Running Tests

# All tests
make test

# Critical tests only (fastest, must pass before commit)
make test-critical

# Specific test file
uv run pytest tests/unit/test_decorators.py -v

# Specific test function
uv run pytest tests/unit/test_decorators.py::test_cache_basic -v

# Run by marker
uv run pytest -m critical -v     # Critical tests
uv run pytest -m unit -v         # Unit tests
uv run pytest -m integration -v  # Integration tests

# With coverage
make test-cov

Important: pytest-redis is required for all Redis-dependent tests. If tests fail with "pytest-redis is required", run uv sync to install dependencies.

Executable docs are tests. Every PR also runs the docstring examples in src/, the tests in tests/docs/, and the code blocks in docs/. A behaviour change must update the affected examples in the same PR. Check them locally with make test-doctest (with REDIS_URL unset, see #225), uv run pytest tests/docs/ and make test-docs-quick.

Test Coverage

  • Aim for >85% coverage for new code
  • All public APIs must have tests
  • Edge cases and error conditions must be tested
  • CI enforces a total-coverage floor inside pytest on every PR (--cov-fail-under, value in .github/workflows/ci.yml), independent of the Codecov upload — a PR whose combined coverage falls below it fails the Tests job. make test-cov enforces the same floor locally. Codecov still reports the finer-grained new-code (patch) coverage on top of this.

Rust Test Coverage:

  • ByteStorage module: 82% coverage (measured via LLVM source-based coverage)
  • Encryption modules: Integration tests only (PyO3 cdylib limitation prevents coverage measurement)
  • All Rust functionality validated via Python integration tests in tests/critical/
  • PyO3's cdylib architecture prevents LLVM coverage tracking across module boundaries
  • This is a known limitation, not a code quality issue

Review Guidance

This project uses documentation-only code ownership routing. GitHub's .github/CODEOWNERS file is not enforced in this repository (tracked internally as LAB-1151). The guidance below documents which paths benefit from security and maintainer review:

Security-sensitive paths — consider requesting review from maintainers:

  • /rust/ — memory safety, FFI boundaries, cryptography
  • /src/cachekit/serializers/ and /src/cachekit/reliability/ — serialization and fault tolerance
  • /.github/workflows/, /pyproject.toml, /rust/Cargo.toml, /.pre-commit-config.yaml — supply chain configuration
  • /tests/security/, /tests/fuzz/, /SECURITY.md — security documentation and testing

This is a single-maintainer org using documentation instead of enforcement. GitHub's review rulesets on this repo require zero code-owner approvals, so the path guidance above is a routing suggestion for pull requests, not a GitHub-enforced gate.

Pull Request Process

  1. Ensure all checks pass

    make check  # Must pass before PR
  2. Update documentation

    • Update README.md if adding user-facing features
    • Add/update docstrings
    • Update CHANGELOG.md (if exists)
  3. Write clear PR description

    • What problem does this solve?
    • What changes were made?
    • How was it tested?
    • Any breaking changes?
  4. PR Title Format

    feat: add distributed cache warming
    fix: resolve connection pool leak
    docs: update getting started guide
    test: add integration tests for encryption
    refactor: simplify serializer interface
    
  5. Review process

    • Address reviewer feedback
    • Keep PR focused and reasonably sized
    • Rebase on main if needed

Common Tasks

Building Rust Extension

make build  # Standard build
make build-pgo  # Profile-Guided Optimization (5-8% faster)

Running Benchmarks

make perf               # Full battery: env fingerprint + timer calibration, serializer benchmarks, GIL scaling
make perf-compare       # Regression gate: fail on >10% median serializer regression vs baseline

make benchmark          # Serializer benchmarks only + save a local baseline
make benchmark-compare  # Re-run and fail on >10% median regression vs that baseline
make benchmark-gil      # Serializer thread-scaling under the current interpreter (GIL)

All perf tests live in one folder, tests/performance/; the pytest-benchmark ones are selected by --benchmark-only and skipped elsewhere via the --benchmark-skip default. Baselines are written to .benchmarks/ (gitignored, per-machine), so regression comparison is a local developer tool — wall-clock benchmarks deliberately do not gate CI. make perf first prints a system fingerprint + environment verdict and self-calibrates the timer, so numbers come with the context needed to trust them.

Formatting Code

make format  # Auto-format Python and Rust

Type Checking

make type-check  # Run basedpyright type checker (zero errors)

Project Structure

cachekit/
├── src/cachekit/        # Python package
│   ├── decorators/      # Cache decorators
│   ├── serializers/     # Serialization engines
│   ├── reliability/     # Circuit breaker, etc.
│   └── ...
├── rust/                # Rust extensions
│   └── src/
│       ├── serialization/
│       ├── compression.rs
│       └── ...
├── tests/               # Test suite
│   ├── critical/        # Critical path tests
│   ├── unit/
│   ├── integration/
│   └── performance/
├── docs/                # Documentation
└── pyproject.toml       # Project metadata

Need Help?

License

By contributing to cachekit, you agree that your contributions will be licensed under the MIT License.