Contributing Guide¶
We welcome contributions to django-graphex! This guide will help you get started.
Quick Start¶
- Fork the repository on GitHub
- Clone your fork locally:
- Install development dependencies:
- Create a branch for your changes:
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:
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*argsand**kwargs. - B. Signature-owned types. Parameters and returns have type hints;
Args:,Returns:orYields:, andRaises: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:
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
RequestFactorymultipart request, close the test-owned request after dispatch so Django can close temporary uploaded files. AnASGIRequestbuilt over a test-owned spooled body also needs that original spool closed:request.close()closes uploaded files, not the body stream. Usetry/finallyaround 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()fromtests._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:
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:
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:
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 issuefor 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¶
-
Push your changes:
-
Open a pull request on GitHub
-
Describe your changes clearly in the PR description
-
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¶
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! ๐