Skip to content

Contributing Guide

We welcome contributions to django-graphex! This guide will help you get started.

Quick Start

  1. Fork the repository on GitHub
  2. Clone your fork locally:
    git clone https://github.com/your-username/django-graphex.git
    cd django-graphex
    
  3. Install development dependencies:
    uv sync
    
  4. Create a branch for your changes:
    git checkout -b feature/your-feature-name
    

Development Setup

Prerequisites

  • Python 3.12, 3.13, or 3.14
  • uv for dependency management
  • Git

Python Version Requirement

The project requires Python 3.12 or higher. Make sure you have one of the supported versions installed.

Installation

# Clone the repository
git clone https://github.com/eamigo86/django-graphex.git
cd django-graphex

# Install dependencies (into a managed venv)
uv sync

# Install pre-commit hooks (optional but recommended)
uv run pre-commit install

Development Commands

We use make commands for common development tasks:

# Run tests
make test

# Run all tests across Python/Django versions
make test-all

# Code quality checks
make quality

# Security checks (Bandit + the frozen runtime dependency closure)
make security

# Audit an existing wheel without rebuilding it (CI supplies the wheel path)
uv run tox -e release-audit -- /path/to/django_graphex-*.whl

# Format code
make format

# Type checking
make type-check

# Build documentation
make docs

# Serve documentation locally
make docs-serve

# Clean build artifacts
make clean

The security environment exports the frozen runtime dependency closure from uv.lock before running pip-audit. Development-only packages are excluded, so findings describe what applications install rather than tox's audit tools.

Code Standards

Code Style

We use two tools to maintain code quality:

  • ruff for formatting, import sorting and linting (ruff format + ruff check)
  • mypy for type checking

Format and autofix locally, then run all quality checks:

make format    # ruff format + ruff check --fix
make quality   # ruff format --check + ruff check + mypy

Documentation Standards

The permanent repository contract has three parts:

  • A. Complete public Google style. Every public module, class, function, and method has a complete Google-style docstring with exact, non-empty sections. Public includes importable modules, non-underscore or __all__ exports, and non-private class members; Args: also covers *args and **kwargs.
  • B. Signature-owned types. Parameters and returns have type hints; Args:, Returns: or Yields:, and Raises: describe behavior without repeating types.
  • C. Plain-text docstrings. No docstring contains a backtick, including private, nested, and dunder owners.

Run the same full-tree gate CI uses:

python3 scripts/check_docstrings.py . --strict-public --strict-content

The . scope includes runtime, scripts, tests, benchmarks, and examples/playground; environments, caches, site output, and migrations are excluded. It also scans eligible local Python files that Git ignores. A clean CI checkout contains tracked files, not ad hoc ignored scripts. When a local result differs from CI, identify the input files before attributing findings to the committed project; do not weaken the checker or add exclusions to hide them.

Strict result sections

Use Returns: for ordinary non-None results and Yields: for generators. Functions annotated as None, NoReturn, or Never have neither section. Required result and Raises: sections must be non-empty; exceptions raised by nested functions or classes belong to those nested owners. Keep every type in the signature onlyโ€”section entries describe names and behavior:

def iter_ids(limit: int) -> Iterator[int]:
    """Yield identifiers up to a limit.

    Args:
        limit: Maximum number of identifiers.

    Yields:
        item: One identifier.
    """
    yield from range(limit)

Testing Standards

  • Follow strict RED โ†’ GREEN โ†’ REFACTOR for code, documentation, workflows and benchmark-harness changes.
  • Write tests for every feature and regression.
  • When a test deliberately triggers a warning, assert its category and stable message with pytest.warns, and check the captured count when the scenario emits more than one. Keep the behavior assertions alongside the warning contract. Do not suppress warnings through global filters: an unexpected warning must still fail a focused run with -W error.
  • Keep branch coverage strictly above 95%; the root and changed-line floor is 95.01%. The combined line-plus-branch percentage is not the pure branch percentage. After a fresh full-suite run, check the latter separately with python scripts/check_branch_coverage.py coverage.xml. This command reads the XML root's covered and valid branch counts, compares their exact fraction with 95.01%, and rejects absent or invalid reports; it does not trust the rounded branch-rate attribute. CI runs it after the full suite has written fresh XML, alongside the unchanged combined and changed-line 95.01% gates.
  • Use descriptive test names
  • Follow the existing test structure
  • When a test calls a view directly with a RequestFactory multipart request, close the test-owned request after dispatch so Django can close temporary uploaded files. An ASGIRequest built over a test-owned spooled body also needs that original spool closed: request.close() closes uploaded files, not the body stream. Use try/finally around dispatch so assertions and exceptions cannot leave these test fixtures open. Django's test client owns its own request lifecycle; do not close borrowed requests.
  • Capability-path tests may use strict legacy signatures to verify that a newer GraphQL-core engine does not receive keywords unsupported by an older API. They do not extend the declared runtime support range.
  • Runtime identity guards for local audits belong in external verification runners, not shipped tests. They may assert a checkout or overlay path for that run; shipped tests must remain usable with any supported GraphQL-core 3.3 installation. Keep behavioral assertions in the tests.
  • Date-part behavior tests verify SDL, HTTP responses, filters, and database results without requiring a specific GraphQL-core 3.3 patch, checkout path, or working directory. Verify the active runtime in an external runner before the test call rather than asserting its identity inside the test.
  • Tests that temporarily replace the default schema registries should use isolated_default_pair() from tests._schema_isolation. It creates a fresh default pair within the scope and restores the exact previous cached pair, global Registry, output companion, and output entries on exit, including nested scopes and exceptions. Do not replace those globals manually: a cached pair can otherwise retain a temporary Registry and make later tests depend on execution order. This is test isolation, not a claim that the production Registry is generally thread-safe.

Resolver fixture ownership

Assign a functools.partial resolver to the field definition instance, as GraphQLField does, not to its class. Class-owned partials can change binding behavior across Python versions, warning on Python 3.13 and binding on Python 3.14. A defensive helper returning None is not sufficient evidence that its intended paginator type guard was reached.

For a synchronous resolver-shape regression, assert that the field instance owns the exact partial, its func is the original bound method and __self__ is the original field object. Observe the paginator_instance property read and the exact non-paginator sentinel before asserting the result is None. The shape-inspection helper must not execute a resolver query. Keep native and custom-resolver companion tests intact; do not change the production helper or filter warnings to accommodate a malformed fixture. Run the focused contract with --no-cov, then the related/full suite and fresh supported-Python CI.

Codecov upload policy

The coverage job keeps the commit-pinned Codecov action and installs the fixed codecov-cli 11.3.1 release from official PyPI instead of fetching latest from the standalone-binary host. Its acquisition/failure inputs are:

use_pypi: true
version: v11.3.1
fail_ci_if_error: true

The pinned action wrapper uses pip's exact-version requirement for this route. This pins the CLI, not its transitive dependencies or the entire hosted runner environment. The official PyPI release metadata is available for that version; local source checks do not prove a hosted runner can install it or send a report.

Distribution-trust tradeoff: the PyPI route uses verified HTTPS and skips the wrapper's standalone-binary GPG/checksum verification. It is not equivalent signature proof; the accepted trust boundary is PyPI package distribution and pip's normal TLS verification. Do not add insecure TLS, alternate download hosts, validation-disable inputs or error-swallowing overrides. Existing token, coverage.xml report and all 95.01% coverage gates remain unchanged.

Installation or upload failure must fail the coverage job. Before release, verify the installed CLI version and actual upload, plus both Codecov project and patch checks on the exact commit. A green job or structural workflow test alone is not upload acceptance.

The tool dependencies in tox.ini deliberately use bounded compatibility ranges. Keep those ranges identical to their entries in dependency-groups.dev in pyproject.toml; the dependency-contract test rejects missing bounds and drift between local and CI environments. Add new standalone CI tools, such as diff-cover, to the development group with both a minimum supported version and an upper major-version bound.

Test contract

The historical django60 tox factor is a name, not an exact-version pin: its dependency range is Django>=6.0,<7.0, so a fresh matrix environment may resolve a later 6.x release. Likewise, django52 uses Django>=5.2,<6.0. The separately locked coverage environment currently uses Django 6.0.8; passing there does not replace testing the versions actually resolved by tox. Record installed dependency versions with each matrix result rather than inferring them from the factor name. These ranges state the tested compatibility policy, not a guarantee about future releases.

Focused RED/GREEN commands must disable the repository-wide coverage options:

uv run pytest -q --no-cov tests/test_your_feature.py

Never erase addopts with -o addopts="". After the focused loop, run the full suite and enforce changed-line coverage from its report:

uv run pytest -q
uv run python scripts/check_branch_coverage.py coverage.xml --threshold 95.01
uvx 'diff-cover>=10.5.1,<11' coverage.xml \
  --compare-branch=origin/main --fail-under=95.01

Tests that assert a model relation appears in a compiled output type must register the related model's output type in the same test-local registry. Use an allowlisted projection for sensitive models such as Django's User; do not rely on another test module importing a global type first. This keeps isolated test runs and shuffled full-suite runs equivalent without changing production's rule that unregistered relation targets are omitted.

Assert the exact exception class and a stable message with match=; broad pytest.raises(Exception) checks can hide unrelated failures:

with pytest.raises(ImproperlyConfigured, match="permission hook returned"):
    build_schema()
def test_django_list_object_type_pagination():
    """Test that DjangoListObjectType properly paginates results."""
    # Test implementation
    pass

Making Changes

1. Choose What to Work On

  • Check open issues
  • Look for issues labeled good first issue for beginners
  • Propose new features by opening an issue first

2. Write Code

  • Follow the code standards above
  • Add tests for your changes
  • Update documentation if needed
  • Keep commits small and focused

3. Test Your Changes

# Run tests for your Python version
make test

# Run tests for all supported versions (takes longer)
make test-all

# Check code quality
make quality

# Check security
make security

4. Update Documentation

  • Update docstrings for any changed APIs
  • Add examples if you're adding new features
  • Update the changelog if your change is user-facing

5. Submit a Pull Request

  1. Push your changes:

    git push origin feature/your-feature-name
    

  2. Open a pull request on GitHub

  3. Describe your changes clearly in the PR description

  4. Wait for review and address any feedback

Pull Request Guidelines

Title Format

Use conventional commit format: - feat: add new pagination option - fix: resolve issue with nested mutations - docs: improve installation guide - test: add tests for directive validation

Description Template

## Description
Brief description of what this PR does.

## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation update

## Testing
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] All new and existing tests pass locally
- [ ] I have run the full test suite (`make test-all`)

## Documentation
- [ ] I have updated the documentation accordingly
- [ ] I have updated the changelog if needed

## Checklist
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] My changes generate no new warnings
- [ ] Any dependent changes have been merged and published

Types of Contributions

Bug Fixes

  • Always include a test that reproduces the bug
  • Explain the root cause in the PR description
  • Consider edge cases

New Features

  • Discuss the feature in an issue first
  • Include comprehensive tests
  • Update documentation and examples
  • Consider backward compatibility

Documentation

  • Keep language clear and concise
  • Include practical examples
  • Test code examples to ensure they work
  • Update related documentation

Performance Improvements

  • Include benchmarks showing the improvement
  • Ensure the change doesn't break existing functionality
  • Consider memory usage as well as speed

Development Tips

Running Specific Tests

# Run tests for a specific file
uv run pytest tests/test_fields.py

# Run tests matching a pattern
uv run pytest -k "test_pagination"

# Run with coverage
uv run pytest --cov=django_graphex

Debugging

# Run with ipdb for debugging
uv run pytest tests/test_fields.py -s --pdb

Database Setup

SQLite remains the zero-setup default. Before publication, CI also runs the transaction contract against PostgreSQL 17 with Python 3.12 and Django 6.0. To reproduce that gate against a local PostgreSQL service:

GDX_TEST_DATABASE=postgres \
POSTGRES_DB=django_graphex \
POSTGRES_USER=postgres \
POSTGRES_PASSWORD=postgres \
POSTGRES_HOST=127.0.0.1 \
POSTGRES_PORT=5432 \
uv run pytest -q --no-migrations --no-cov \
  tests/integration/test_postgresql_transactions.py

Every test in that module asserts connection.vendor == "postgresql", so a misconfigured run cannot silently pass on SQLite. MySQL is not part of this reduced release gate.

Release contract

Production publishing waits for the complete validation graph: the six Python/Django combinations, base install, quality/security, root and patch coverage, PostgreSQL 17, docs, playground and the release artifact.

The release-artifact job builds one immutable artifact containing the wheel, sdist and SHA256SUMS. It checks the installed wheel outside the checkout, including its site-packages origin and py.typed, then every publisher reuses those bytes and must never rebuild them. See the full release process.

workflow_dispatch publishes only to TestPyPI. Production PyPI accepts only refs/tags/v* whose version matches pyproject.toml; Pages deploys the docs artifact validated before publication. A failed post-PyPI job is rerun for the same immutable tagโ€”never move or recreate it.

Installed-wheel release gate

CI installs the candidate wheel under $RUNNER_TEMP and executes its smoke check from outside the repository checkout with PYTHONPATH removed. The gate requires an import from site-packages, matching package metadata, py.typed, a base install without Channels, django.setup(), schema compilation, and a real GraphQL query. The dependency audit receives that same prebuilt wheel and does not rebuild it.

Code Review Process

What We Look For

  • Correctness: Does the code work as intended?
  • Testing: Are there adequate tests?
  • Documentation: Is the code well-documented?
  • Performance: Does it maintain good performance?
  • Style: Does it follow our coding standards?
  • Compatibility: Does it work with all supported versions?

Timeline

  • Initial review: Within 1-2 weeks
  • Follow-up reviews: Within a few days
  • Complex changes may take longer

Getting Help

Discord/Slack

We don't currently have a Discord or Slack, but you can:

GitHub Discussions

Use GitHub Discussions for: - Questions about contributing - Feature discussions - General help

Issues

Use GitHub Issues for: - Bug reports - Feature requests - Documentation improvements

Recognition

Contributors are recognized in several ways:

  • Listed in the repository contributors
  • Mentioned in release notes for significant contributions
  • Can be added as maintainers for sustained contributions

Code of Conduct

Our Pledge

We are committed to making participation in our project a harassment-free experience for everyone, regardless of: - Age, body size, disability, ethnicity - Gender identity and expression - Level of experience, education - Nationality, personal appearance - Race, religion, sexual identity and orientation

Our Standards

Positive behavior includes: - Using welcoming and inclusive language - Being respectful of differing viewpoints - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members

Unacceptable behavior includes: - Trolling, insulting/derogatory comments, personal attacks - Public or private harassment - Publishing others' private information without permission - Other conduct which could reasonably be considered inappropriate

Enforcement

Project maintainers have the right to: - Remove, edit, or reject comments, commits, code, wiki edits, issues - Ban temporarily or permanently any contributor for behaviors deemed inappropriate

Report any issues to the project maintainers via GitHub issues or email.

Thank You! ๐ŸŽ‰

Your contributions help make django-graphex better for everyone. Whether it's:

  • Fixing a typo in documentation
  • Adding a new feature
  • Reporting a bug
  • Improving performance

Every contribution matters and is appreciated! ๐Ÿ™