Skip to content

Contributing to Ferro

We welcome contributions to Ferro! This guide will help you get started with developing Ferro locally.

Prerequisites

Before starting, ensure you have:

  • Python 3.13+: Ferro requires Python 3.13 or later
  • Rust toolchain: Required for building the Rust core
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
  • UV: Fast Python package manager
    curl -LsSf https://astral.sh/uv/install.sh | sh
    

Getting Started

1. Clone the Repository

git clone https://github.com/syn54x/ferro-orm.git
cd ferro-orm

2. Install Dependencies

uv sync --group dev

This will install all development dependencies including: - Testing tools (pytest, pytest-asyncio, pytest-cov) - Linting and formatting tools (ruff, prek) - Build tools (maturin) - Documentation tools (mkdocs-material) - Release tools (commitizen, python-semantic-release)

3. Install Pre-commit Hooks

# Install all hooks (file checks, linting, formatting)
uv run prek install

# Install commit message validation hook
uv run prek install --hook-type commit-msg

These hooks will automatically: - Check for trailing whitespace - Fix end-of-file issues - Validate YAML, TOML, and JSON files - Format Python code with Ruff - Format Rust code with rustfmt - Lint Rust code with clippy - Validate conventional commit messages

4. Build the Rust Extension

uv run maturin develop

This compiles the Rust core and installs it in development mode. You'll need to re-run this command after making changes to Rust code.

Development Workflow

Running Tests

# Run all tests with coverage
uv run pytest

# Run specific test file
uv run pytest tests/test_models.py

# Run with verbose output
uv run pytest -v

# Run tests and generate coverage report
uv run pytest --cov=src --cov-report=html

Running Linters

# Run all pre-commit hooks
uv run prek run --all-files

# Run specific hooks
uv run ruff check .        # Python linting
uv run ruff format .       # Python formatting
cargo fmt                  # Rust formatting
cargo clippy               # Rust linting

Building Documentation

# Serve documentation locally (with live reload)
uv run mkdocs serve

# Build documentation
uv run mkdocs build

# Documentation will be available at http://127.0.0.1:8000/

Testing Your Changes

Before submitting a PR, ensure:

  1. All tests pass:

    uv run pytest
    

  2. All linters pass:

    uv run prek run --all-files
    

  3. Rust tests pass:

    cargo test
    

  4. Code builds successfully:

    uv run maturin develop
    

Conventional Commits

Ferro uses Conventional Commits for automated version bumping and changelog generation. All commit messages must follow this format:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Commit Types

  • feat: New feature (triggers minor version bump)
  • fix: Bug fix (triggers patch version bump)
  • docs: Documentation changes only
  • refactor: Code refactoring (no functional changes)
  • test: Adding or updating tests
  • perf: Performance improvements (triggers patch version bump)
  • build: Build system changes
  • ci: CI/CD configuration changes
  • chore: Other changes that don't modify src or test files

Examples

# Feature commits
git commit -m "feat: add support for many-to-many relations"
git commit -m "feat(queries): implement OR operator for filters"

# Bug fix commits
git commit -m "fix: resolve connection pool deadlock"
git commit -m "fix(migrations): handle nullable foreign keys correctly"

# Documentation commits
git commit -m "docs: update installation instructions"
git commit -m "docs(api): add examples for transaction usage"

# Breaking changes (triggers major version bump)
git commit -m "feat!: change Model.create() to require explicit save()"
# OR
git commit -m "feat: redesign query API

BREAKING CHANGE: Query.filter() now requires Q objects instead of kwargs"

Commit Validation

The pre-commit hook will automatically validate your commit message format. Invalid commits will be rejected with an error message.

If you need to bypass the hook (not recommended), use:

git commit --no-verify -m "message"

Pull Request Process

  1. Create a feature branch:

    git checkout -b feat/my-new-feature
    

  2. Make your changes and commit:

    git add .
    git commit -m "feat: add my new feature"
    

  3. Push to your fork:

    git push origin feat/my-new-feature
    

  4. Open a Pull Request on GitHub

  5. Wait for CI checks to pass:

  6. All linters must pass
  7. All tests must pass
  8. Code must build on all platforms

  9. Address review feedback if any

  10. Merge once approved!

PR Requirements

  • ✅ All CI checks pass
  • ✅ Conventional commit format followed
  • ✅ Tests added for new features
  • ✅ Documentation updated
  • ✅ No merge conflicts with main

Release Process

Ferro uses automated releases. You don't need to manually bump versions or update the changelog.

How Releases Work

  1. Commits are merged to main
  2. Changelog is automatically updated with unreleased changes

  3. Maintainer triggers the release workflow

The release process is fully automated. To trigger a new release:

  • Via GitHub CLI (Recommended):
    gh workflow run release.yml
    
  • Via GitHub Web UI: Go to the Actions tab, select the Release workflow, and click Run workflow.

This will: - Automatically determine the next version from conventional commits. - Update pyproject.toml and Cargo.toml. - Finalize CHANGELOG.md. - Create and push the Git tag. - Trigger the Build & Publish workflow to upload wheels to PyPI.

  1. Package is automatically published to PyPI
  2. Cross-platform wheels are built
  3. Package is uploaded using trusted publishing

Version Bumping

Version bumps are determined by commit types:

  • Major (1.0.0 → 2.0.0): Commits with BREAKING CHANGE: in body or ! after type
  • Minor (1.0.0 → 1.1.0): Commits with feat: type
  • Patch (1.0.0 → 1.0.1): Commits with fix: or perf: type

Code Style

Python

  • Follow PEP 8 style guide
  • Use type hints for all functions
  • Maximum line length: 100 characters (enforced by Ruff)
  • Use Pydantic for data validation
  • Write docstrings for all public APIs

Rust

  • Follow Rust style guidelines (enforced by rustfmt)
  • Use cargo clippy warnings as errors
  • Write documentation for public APIs
  • Use descriptive variable names
  • Prefer explicit types over inference in function signatures

Testing

Python Tests

Located in tests/ directory. Use pytest with async support:

import pytest
from ferro import Model, FerroField, connect

@pytest.mark.asyncio
async def test_create_model():
    await connect("sqlite::memory:")
    user = await User.create(name="Alice")
    assert user.name == "Alice"

Rust Tests

Located alongside Rust code with #[cfg(test)]:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sql_generation() {
        let query = generate_select_query("users");
        assert_eq!(query, "SELECT * FROM users");
    }
}

Project Structure

ferro/
├── src/
│   ├── ferro/           # Python package
│   │   ├── __init__.py
│   │   ├── models.py
│   │   ├── queries.py
│   │   └── ...
│   └── lib.rs           # Rust core
├── tests/               # Python tests
├── docs/                # Documentation
├── .github/
│   └── workflows/       # CI/CD workflows
├── Cargo.toml           # Rust dependencies
├── pyproject.toml       # Python dependencies
└── README.md

Getting Help

License

By contributing to Ferro, you agree that your contributions will be licensed under the same license as the project (Apache 2.0 OR MIT).