Thanks for your interest in improving strawberry-orm. The package is in alpha, so contributions that tighten correctness — especially around row scoping, the optimizer, and backend parity — are particularly welcome.
- Ground rules
- Development setup
- Running tests
- Linting and formatting
- Project layout
- Backend parity
- Writing tests
- Inspecting schemas locally
- Coding conventions
- Documentation
- Commits and pull requests
- Releasing
- Reporting bugs and security issues
- Open an issue first for anything larger than a bug fix. Public API changes, new backend support, and optimizer behavior changes benefit from a design discussion before the code exists.
- Tests are mandatory. The suite enforces 100% line coverage of
strawberry_orm; CI fails below that. - Behavior changes must land on every backend, or be explicitly excluded in
tests/test_backend_parity.pywith a reason. - Be respectful in issues and reviews. Assume good intent.
The project uses uv for dependency management and builds. Python 3.12 or newer is required.
git clone https://github.com/strawberry-graphql/strawberry-orm
cd strawberry-orm
uv sync --group dev --extra django --extra sqlalchemy --extra tortoiseInstalling all three extras matches CI. Skipping one will make that backend's tests fail to import, and coverage will drop below the required threshold.
uv run pytest # full suite with coverage gate
uv run pytest tests/backends/sqlalchemy # one backend
uv run pytest tests/backends/django/test_query_optimizer.py::TestQueryOptimizerEagerLoading
uv run pytest -k filter_object_traversal # by nameCoverage options live in pyproject.toml (--cov=strawberry_orm --cov-fail-under=100), so every invocation reports coverage. When iterating on a subset, pass --no-cov to skip the gate that a partial run can never satisfy:
uv run pytest tests/backends/tortoise -q --no-covAlways run the full suite without --no-cov before pushing.
Notes:
asyncio_mode = "auto", soasync deftests need no marker.- Django settings are configured in
tests/backends/django/conftest.pyagainst in-memory SQLite; there is no project to migrate. - SQLAlchemy and Tortoise tests also use in-memory SQLite. No external services are needed.
If a line genuinely cannot be covered, prefer restructuring the code. Only as a last resort add # pragma: no cover with a comment explaining why the branch is unreachable.
Ruff config lives in pyproject.toml (line length 88, target py312, rules E/F/I/UP/B/SIM). Ruff is not pinned as a dependency, so run it via uvx:
uvx ruff check .
uvx ruff format .Run both before opening a pull request. E501 is deliberately ignored — let the formatter handle line length.
Because Ruff is unpinned, a newer release may surface findings unrelated to your change (new rules land regularly). Fix what your diff introduces; leave pre-existing findings to a dedicated cleanup pull request.
src/strawberry_orm/
core.py StrawberryORM facade: type/input/filter/order/group factories, schema()
fields.py orm.field(), make_field(), field hints
filters.py lookup input types and filter tree construction
types.py auto, Ordering, group-by and aggregate types
mutations.py refs, apply_ref_list, recursive node mutations
policy.py mutation projection / _meta policy resolution
repo.py AbstractRepo authorization and lifecycle hooks
lazy_resolution.py lazy-load detection and warning/error modes
optimizer/ selection-set walking, prefetch store, Strawberry extension
backends/
_base.py shared backend behavior
protocol.py backend interface (excluded from coverage)
django.py sqlalchemy.py tortoise.py
relay/ ORMListConnection and orm.connection()
tests/
abstract/ backend-agnostic test bodies, shared by all backends
backends/{django,sqlalchemy,tortoise}/
fixtures, models, and thin subclasses of the abstract tests
test_backend_parity.py
cli.py GraphiQL inspector for the test schemas
Backend-specific behavior belongs in backends/*.py. If you find yourself writing the same logic in two backends, move it to backends/_base.py.
tests/test_backend_parity.py uses AST introspection to assert that Django, SQLAlchemy, and Tortoise expose the same test files, classes, and methods. Adding a test to one backend without the others fails this check.
When a test truly cannot apply to a backend (for example SQLAlchemy session resolution), add the file to EXCLUDED_FILES or the specific test to EXCLUDED_METHODS in that module. Keep exclusions narrow — prefer a per-method exclusion over an entire file.
Most behavior is tested once, in tests/abstract/, and inherited per backend. The abstract class receives execute and seed fixtures supplied by each backend's fixtures.py:
# tests/abstract/query_basic.py
class AbstractTestQueryBasic:
def test_list_all_users(self, execute, seed):
data = execute("{ users { id name email } }")
assert data == {"users": [...]}# tests/backends/django/test_query_basic.py
from tests.abstract.query_basic import AbstractTestQueryBasic
class TestQueryBasic(AbstractTestQueryBasic):
passGuidelines:
- Write GraphQL-level assertions on the full response dict rather than checking one key. It catches accidental extra fields.
- Assert on SQL query counts for anything touching the optimizer — see the
test_query_n_plus_one.pyfiles for the pattern. - Scoping behavior needs a test proving rows are hidden, not just that the hook ran.
- Naming follows
test_<area>_<topic>.pywith a matchingtests/abstract/<area>_<topic>.pymodule. Keep the names identical across backends so parity passes.
cli.py serves the test schemas in GraphiQL, which is the fastest way to see how a change affects the generated SDL:
uv run python cli.py # list available schemas
uv run python cli.py main --backend sqlalchemy
uv run python cli.py get_queryset -b django -p 8500Schemas are seeded with demo data by default; pass --no-seed for an empty database. Django and SQLAlchemy are the supported backends here; SCHEMA_NAMES in cli.py is the list of exposed schemas.
- Target Python
3.12+. Use modern syntax (X | None, builtin generics,matchwhere it reads well). from __future__ import annotationsat the top of new modules.- Type-annotate public functions. Keep runtime-introspected generics intact —
UP046is ignored becauseGeneric[M]is needed forget_original_bases. - Public API additions must be exported from
strawberry_orm/__init__.pyand documented in the README's Public Exports section. - Keep backend-facing changes behind the interface in
backends/protocol.py. - Comments should explain non-obvious constraints or trade-offs. Don't narrate what the code does.
- Prefer safe defaults. New options that widen what clients can read or write should default to off (as
enable_regex_filtersdoes).
The README is the single source of user documentation. If your change affects behavior a user can observe, update the relevant section in the same pull request — including the option tables under Backends and, for anything scoping-related, the Security chapter.
- Write commit subjects in the imperative mood, describing the change in one sentence:
Add relation FK presence filters and non-id primary key support. Commits that also bump the version carry the version in parentheses, matching existing history:Add totalCount to Relay connections and release v0.13.0. - Keep pull requests focused. Unrelated refactors make review harder and bisecting worse.
- Before pushing, confirm:
uvx ruff check .,uvx ruff format ., anduv run pytest(full suite, coverage at 100%). - In the pull request description, state what changed, why, and how you verified it. Call out any backend that behaves differently and why.
- CI runs the full suite on Python 3.12 with all three backends and uploads coverage to Codecov.
Releases are cut by maintainers:
- Bump
project.versioninpyproject.tomlonmain. - Run the Release and Publish workflow (
workflow_dispatch). It must be dispatched frommain.
The workflow reads the version from pyproject.toml, refuses to run if the v<version> tag already exists, builds with uv build, validates the artifacts with twine check --strict, creates a GitHub release with generated notes, and publishes to PyPI via trusted publishing.
For bugs, open an issue on the issue tracker with the backend, the relevant type and schema definitions, the GraphQL query, and what you expected versus what happened. A minimal reproduction using the test fixtures is ideal.
For anything that could expose data — a query that returns rows get_queryset should have hidden, a filter that escapes its scope, a sensitive field leaking into a generated input — do not open a public issue. Email the maintainers (see authors in pyproject.toml) so a fix can ship before disclosure.
By contributing, you agree that your contributions are licensed under the MIT License.