Skip to content

Latest commit

 

History

History
397 lines (283 loc) · 10.5 KB

File metadata and controls

397 lines (283 loc) · 10.5 KB

Contributing to Fromager

Fromager thrives on practical, well-tested contributions. This guide summarizes how to set up a workspace, follow our standards, and submit polished changes. Skim it once, keep it handy, and refer back whenever you are unsure.

Note: If you're using AI coding assistants, also see AGENTS.md for AI-optimized quick reference.

Table of Contents


Quick Start

Prerequisites

  • Python 3.12 or newer

  • hatch for environment and task management

    pip install hatch
    # or
    pipx install hatch  # recommended

Initial Setup

# 1. Fork the repository on GitHub
# 2. Clone your fork
git clone https://github.com/<your-username>/fromager.git
cd fromager

# 3. Add upstream remote
git remote add upstream https://github.com/python-wheel-build/fromager.git

# 4. Create development environment
hatch env create

# 5. Install pre-commit hooks (optional but recommended)
hatch run lint:install-hooks

Contribution Workflow

# 1. Sync with upstream
git checkout main
git fetch upstream
git merge upstream/main

# 2. Create a feature branch
git checkout -b feat/<short-description>

# 3. Make changes and test as you go
hatch run test:test tests/test_<module>.py       # Test your specific changes

# 4. Before committing, run full quality checks
hatch run lint:precommit

# 5. Commit using Conventional Commits
git commit -m "feat(scope): short summary"

# 6. Push to your fork
git push origin feat/<short-description>

# 7. Create a pull request on GitHub

Pre-commit Hook (Recommended)

To ensure quality checks run automatically before each commit, install the pre-commit hook that runs both linting and mypy type checking:

# Install the pre-commit hook (run once after cloning)
hatch run lint:install-hooks

# The hook automatically runs a selection of linters before each commit.

# If the hook fails, it will prevent the commit and show helpful messages
# You can fix some issues automatically with:
hatch run lint:fix

The pre-commit hook prevents commits that would fail some CI quality checks, saving time and ensuring consistent code quality.


Coding Standards

Type Annotations

  • Every function (including tests) must annotate all parameters and return values.
  • Use modern X | None syntax instead of Optional[X] (PEP 604).
  • Prefer precise collection types (list[str], dict[str, int], etc.).
def process(data: str | None, count: int = 0) -> dict[str, int]:
    """Process the input and return aggregate counts."""
    return {}

Code Quality and Formatting

  • Ruff enforces both formatting and linting.
  • Run hatch run lint:fix to automatically format code.
  • See Quick Reference for additional commands.

Import Organization

  • PEP 8: imports should be at the top: All import statements must be placed at the top of the file, after module docstrings and before other code.
  • No local imports: Do not place import statements inside functions, methods, or conditional blocks.

Documentation Expectations

  • Add a module docstring describing purpose and high-level behavior.
  • Public functions and classes require docstrings that cover arguments, return values, and noteworthy behavior.
  • Keep prose short and imperative; explain "why" decisions when the code itself cannot.
def retry_on_exception(
    exceptions: tuple[type[Exception], ...],
    max_attempts: int = 5,
) -> typing.Callable:
    """Retry decorated call on the provided exception types.

    Args:
        exceptions: Exception types that trigger a retry.
        max_attempts: Maximum number of attempts.

    Returns:
        Decorator function.
    """
    ...

Commenting Guidelines

  • Write self-explanatory code; reserve comments for non-obvious intent or domain context.
  • Capture reasoning, invariants, or unexpected trade-offs—never repeat the code literally.
# Bad - comment just repeats the code
x = x + 1  # increment x

# Good - clear variable name makes the comment unnecessary
total_attempts += 1

# Good - comment explains the reasoning behind the approach
# Exponential backoff avoids thundering herd when many jobs retry at once.
wait_time = min(2**attempt + random.uniform(0, 1), max_backoff)

Logging

  • Use a module-level logger and the appropriate log level for the situation.
  • When processing per-requirement work, wrap nested calls in req_ctxvar_context() so log records automatically include the package (and optional version). This keeps CLI logs searchable even when work runs in parallel.
logger = logging.getLogger(__name__)

def sync_artifacts() -> None:
    logger.info("Starting artifact sync")
    logger.debug("Artifacts queued: %s", pending_jobs)

# When processing a specific package, wrap calls to include package info in logs
# req: Requirement object for the package being processed
# version: Version string being built (optional)
with req_ctxvar_context(req, version):
    logger.info("Resolving build dependencies")  # Logs will include package name

Error Handling

  • Raise specific exceptions with actionable messages.
  • Chain exceptions (raise ... from e) so stack traces stay informative.
try:
    result = process_data(file_path)
except FileNotFoundError as err:
    raise ValueError(f"Cannot load config at {file_path}") from err

Testing

Structure

  • Place tests under tests/.
  • Name files test_<module>.py and functions test_<behavior>().
  • Keep tests small: arrange, act, assert.
def test_load_config(tmp_path: pathlib.Path) -> None:
    """Verify config loads with expected values."""
    # Arrange
    config_file = tmp_path / "config.txt"
    config_file.write_text("setting=value\n")
    # Act
    result = load_config(config_file)
    # Assert
    assert result["setting"] == "value"

Useful Commands

hatch run test:test                              # Full suite
hatch run test:test tests/test_context.py        # Specific file
hatch run test:test --log-level DEBUG            # Verbose output
hatch run test:coverage-report                   # Coverage summary

Commit Guidelines

Fromager follows Conventional Commits.

<type>(<scope>): <short summary>

<body explaining what + why>

<footer for metadata>

Common Types

  • feat: new functionality
  • fix: bug fix
  • docs: documentation only
  • test: tests only
  • refactor: behavioral no-op refactor
  • perf: performance improvement
  • chore: tooling or dependency change

Writing Tips

  • Subject ≤ 72 characters, imperative voice, no trailing period.
  • Describe motivation in the body if the diff is not self-explanatory.
  • Reference issues when relevant (Closes: #123).

Examples

feat(http_retry): add exponential backoff with jitter

Support transient failures more gracefully by adding jittered backoff.

Closes: #456
fix(constraints): handle missing constraint file gracefully

Validate file existence and emit a helpful message instead of crashing.

AI-Generated Code Attribution

When AI tools create or significantly modify code, add attribution:

feat(resolver): add exponential backoff for HTTP retries

Improves resilience when PyPI is under load by adding jittered backoff.

Co-Authored-By: Claude <claude@anthropic.com>
Closes: #456

Avoid vague messages like fix bug, update files, or WIP.


Before Submitting

Quality Checklist

  • hatch run lint:fix
  • hatch run test:test
  • hatch run mypy:check
  • hatch run lint:check

Code Review Checklist

  • Every function is fully typed.
  • Public APIs include docstrings.
  • Tests cover the change (positive + edge cases).
  • No trailing whitespace; files end with a newline.
  • Conventional Commit format followed.
  • Code aligns with existing patterns.

Design Patterns Used in Fromager

Context Managers

@contextlib.contextmanager
def resource_context() -> typing.Generator[None, None, None]:
    """Manage resource lifecycle."""
    setup_resource()
    try:
        yield
    finally:
        cleanup_resource()

Thread-Safe Decorators

def with_thread_lock() -> typing.Callable:
    """Create thread-safe wrapper."""
    lock = threading.Lock()

    def decorator(func: typing.Callable) -> typing.Callable:
        @functools.wraps(func)
        def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
            with lock:
                return func(*args, **kwargs)
        return wrapper
    return decorator

Pydantic Validation

def _validate_envkey(value: typing.Any) -> str:
    """Normalize environment variable keys."""
    if isinstance(value, bool):
        return "1" if value else "0"
    if isinstance(value, (int, float)):
        return str(value)
    if isinstance(value, str):
        return value.strip()
    raise TypeError(f"unsupported type {type(value)}: {value!r}")

EnvKey = typing.Annotated[str, BeforeValidator(_validate_envkey)]

Quick Reference

Commands

Task Command
Run tests hatch run test:test
Check code quality hatch run lint:check
Fix formatting hatch run lint:fix
Type checking hatch run mypy:check

Standards

Standard Requirement
Type annotations Required for every function
Docstrings Required on public APIs
Tests Required for new behavior
Trailing whitespace Forbidden
File endings Single newline
Commit format Conventional Commits

Getting Help

  • Open an issue for discussion or questions or design proposals.
  • File an issue with a minimal reproduction for bugs.
  • Ask for targeted feedback directly in your pull request.

Resources


Thanks for contributing to Fromager! If you spot gaps or have suggestions for this guide, open an issue or start a discussion—we love improving our contributor experience.