Singularity

Contributing

Guidelines for contributing to the Singularity project — code formatting, type checking, testing, branch strategy, and pull request workflow.

Thank you for your interest in contributing to Singularity. This guide covers the development workflow, code quality tools, and the process for submitting changes.


Prerequisites

Before contributing, make sure you have:

  • Python 3.12+ installed
  • uv as the package manager (installation)
  • Redis running locally (for Celery and RPC)
  • PostgreSQL running locally (for database operations)
  • Git for version control

Initial Setup

# Clone the repository
git clone https://github.com/pratyush618/singularity-fm.git
cd singularity-fm

# Install all dependencies (including dev group)
uv sync --all-groups

# Copy environment file
cp .env.example .env
# Edit .env with your local database and Redis URLs

Code Quality Tools

Ruff

Linting and formatting. Runs fast, replaces flake8 + isort + black.

Mypy

Static type checking for Python type annotations.

Pytest

Test runner with async support via pytest-asyncio.

Code Formatting and Linting

Singularity uses Ruff for both linting and formatting.

# Check for linting issues
uv run ruff check .

# Automatically fix linting issues
uv run ruff check . --fix

# Format code (consistent style)
uv run ruff format .

# Check formatting without modifying files
uv run ruff format . --check

Run ruff check . --fix before committing to automatically resolve most linting issues. This handles import sorting, unused imports, and common style issues.

Type Checking

Mypy is used for static type analysis.

# Run type checks on the entire project
uv run mypy .

# Check a specific module
uv run mypy services/
uv run mypy singularity/rpc/

Type stubs for third-party libraries are included in the dev dependencies:

  • types-pyyaml
  • types-requests
  • types-ujson

Pre-Commit Hooks

Singularity includes pre-commit for automated checks before each commit.

Setup

# Install pre-commit hooks (one-time setup)
uv run pre-commit install

# Run all hooks manually against all files
uv run pre-commit run --all-files

# Update hooks to latest versions
uv run pre-commit autoupdate

Once installed, pre-commit will automatically run configured checks every time you run git commit. If any check fails, the commit is aborted and you can fix the issues before retrying.

If a .pre-commit-config.yaml does not already exist, create one with:

repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.15.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.19.0
    hooks:
      - id: mypy
        additional_dependencies:
          - types-requests
          - types-pyyaml
          - types-ujson

Running Tests

Tests are located in the tests/ directory and use pytest with pytest-asyncio for async support.

# Run the full test suite
uv run pytest

# Run with verbose output
uv run pytest -v

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

# Run a specific test function
uv run pytest tests/test_services.py::test_user_service_get

# Run tests matching a keyword
uv run pytest -k "rpc"

# Run with coverage report
uv run pytest --cov=src --cov-report=term-missing

Test Configuration

Test settings are defined in pyproject.toml:

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
pythonpath = ["src", "tests"]
filterwarnings = [
  "ignore::pytest.PytestCollectionWarning",
]

Key points:

  • asyncio_mode = "auto" -- async test functions are automatically detected (no need for @pytest.mark.asyncio).
  • pythonpath = ["src", "tests"] -- allows imports like from services.x import ... without path manipulation.

Writing Tests

# tests/test_example_service.py
from unittest.mock import AsyncMock, MagicMock


def mock_acquire():
    """Create a mock Acquire container."""
    acquire = MagicMock()
    acquire.async_session = AsyncMock()
    acquire.settings = MagicMock(jwt_secret="test-secret")
    acquire.logger = MagicMock()
    return acquire


async def test_service_get():
    from services.v1.example.service import ExampleService

    acquire = mock_acquire()
    service = ExampleService(acquire=acquire)
    result = await service.get()
    assert "status" in result

Branch Strategy

Singularity follows a branch-based development workflow:

BranchPurposeMerges Into
masterStable, production-ready code(deployment target)
FEAT/<description>New featuresmaster via PR
FIX/<description>Bug fixesmaster via PR
REFACTOR/<description>Code refactoring (no behavior change)master via PR
DOCS/<description>Documentation changesmaster via PR

Branch Naming

Use descriptive, hyphenated names after the prefix:

FEAT/webhook-interface
FEAT/task-notifications
FIX/rpc-heartbeat-timeout
REFACTOR/service-discovery
DOCS/api-reference

Pull Request Guidelines

Before Submitting

  1. Create a feature branch from master:

    git checkout master
    git pull origin master
    git checkout -b FEAT/my-feature
  2. Make your changes with clear, atomic commits.

  3. Run all quality checks:

    uv run ruff check . --fix
    uv run ruff format .
    uv run mypy .
    uv run pytest
  4. Push and open a PR:

    git push -u origin FEAT/my-feature

PR Checklist

Before requesting review, verify:

  • All tests pass (uv run pytest)
  • No lint errors (uv run ruff check .)
  • Code is formatted (uv run ruff format . --check)
  • Type checks pass (uv run mypy .)
  • New features include tests
  • Public API changes include docstrings
  • CLI changes are reflected in help text and descriptions

PR Description

Include the following in your PR description:

  • Summary -- What changed and why.
  • Testing -- How you tested the changes.
  • Breaking changes -- Any changes that affect existing behavior.

Review Process

  • PRs require at least one approving review before merge.
  • Address review comments with new commits (do not force-push over review comments).
  • Squash-merge into master for a clean history.

Project Conventions

File Organization

  • Services live in services/ with each service in its own directory containing a service.py file.
  • Tasks live in tasks/executable/ as single files or directories.
  • Scripts live in scripts/executable/.
  • CLI tools live in singularity/cli/.
  • Shared utilities go in singularity/common/ or singularity/utils/.

Import Style

Modules are imported relative to src/ as the Python path root:

# Correct
from singularity.core.acquire import Acquire
from singularity.config import settings
from singularity.rpc import rpc
from singularity.common.logger import log as logger

# Incorrect (do not use src. prefix)
from src.services.__base.acquire import Acquire

Naming Conventions

ItemConventionExample
Service classesPascalCase ending in ServicePaymentsService
Service directoriessnake_casev1/user_profiles/
Task namessnake_casesend_email
Script namessnake_caseseed_initial_data
CLI commandskebab-casesync-hashes

Docstrings

All public classes and methods should include docstrings. Use the Google style:

def submit(self, task_name: str, **kwargs) -> Any:
    """Submit a task for background execution.

    Args:
        task_name: The unique task identifier.
        **kwargs: Arguments forwarded to the task function.

    Returns:
        Celery AsyncResult reference.

    Raises:
        ValueError: If task_name is not found.
    """