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 URLsCode 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 . --checkRun 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-pyyamltypes-requeststypes-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 autoupdateOnce 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.
Recommended Hook Configuration
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-ujsonRunning 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-missingTest 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 likefrom 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 resultBranch Strategy
Singularity follows a branch-based development workflow:
| Branch | Purpose | Merges Into |
|---|---|---|
master | Stable, production-ready code | (deployment target) |
FEAT/<description> | New features | master via PR |
FIX/<description> | Bug fixes | master via PR |
REFACTOR/<description> | Code refactoring (no behavior change) | master via PR |
DOCS/<description> | Documentation changes | master 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-referencePull Request Guidelines
Before Submitting
-
Create a feature branch from
master:git checkout master git pull origin master git checkout -b FEAT/my-feature -
Make your changes with clear, atomic commits.
-
Run all quality checks:
uv run ruff check . --fix uv run ruff format . uv run mypy . uv run pytest -
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
masterfor a clean history.
Project Conventions
File Organization
- Services live in
services/with each service in its own directory containing aservice.pyfile. - 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/orsingularity/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 AcquireNaming Conventions
| Item | Convention | Example |
|---|---|---|
| Service classes | PascalCase ending in Service | PaymentsService |
| Service directories | snake_case | v1/user_profiles/ |
| Task names | snake_case | send_email |
| Script names | snake_case | seed_initial_data |
| CLI commands | kebab-case | sync-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.
"""