Skip to main content

More convinient use of factory boy with sqlalchemy models

Project description

factory-boy-sqlalchemy

Utilities to use factory_boy with SQLAlchemy models without hard‑binding a session in your factory definitions. You write plain, session‑less factories and bind them to an async SQLAlchemy session at test time.

This package ships a pytest plugin that provides the sqlalchemy_async_factory fixture to quickly create async factories bound to your test AsyncSession.

Note: The plugin module to enable is fbsa.pytest (not fbas.pytest).

Why? Many factory_boy+SQLAlchemy integrations require putting a session on each factory’s Meta, which couples factories to a specific session and makes reuse harder. Here, factories stay vanilla and you opt‑in to a bound async factory per test.

Install

pip install factory-boy-sqlalchemy

Requires Python 3.12+, SQLAlchemy 2.x, and factory_boy 3.x.

Quick Start (pytest)

  • Add the plugin to pytest so the sqlalchemy_async_factory fixture is available:

    • Option 1 (recommended): in tests/conftest.py add pytest_plugins = "fbsa.pytest".
    • Option 2: run pytest with -p fbsa.pytest or set it in pytest.ini via addopts = -p fbsa.pytest.
  • Provide an async_session fixture (type sqlalchemy.ext.asyncio.AsyncSession).

  • Define plain factory_boy factories (no session in Meta).

Minimal example

# tests/test_user.py
import factory as fb
import sqlalchemy as sa
import pytest
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

# Enable plugin-provided fixtures
pytest_plugins = "fbsa.pytest"


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "user"
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    email: Mapped[str]


class UserFactory(fb.Factory):
    email = fb.Faker("email")

    class Meta:
        model = User


@pytest.fixture
async def async_session(async_engine: AsyncEngine) -> AsyncSession:
    # Create/drop tables around the session as needed
    session_maker = async_sessionmaker(async_engine, expire_on_commit=False)
    async with async_engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    async with session_maker.begin() as session:
        yield session
    async with async_engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)


@pytest.fixture
def async_user_factory(sqlalchemy_async_factory):
    # Bind our vanilla UserFactory to the async session for this test
    return sqlalchemy_async_factory(UserFactory)


@pytest.mark.asyncio
async def test_creates_user(async_user_factory, async_session: AsyncSession):
    user = await async_user_factory(email="alice@example.com")

    db_user = await async_session.get(User, user.id)
    assert db_user and db_user.email == "alice@example.com"

What the plugin provides

  • sqlalchemy_async_factory: a callable fixture. Given a vanilla factory_boy factory class, it returns an async‑enabled factory bound to the current AsyncSession.
  • async_factory_cache: an internal per‑test cache so the same base factory maps to a single bound async factory.
  • semaphore: an asyncio.Semaphore (default 1) to serialize flush() calls; override to control concurrency.

The plugin requires an async_session fixture in your tests. It does not create a database engine or session for you.

Enable it with pytest_plugins = "fbsa.pytest" (or -p fbsa.pytest). This is required to provide the sqlalchemy_async_factory fixture.

SubFactory support

Vanilla SubFactory fields are evaluated synchronously while factory_boy builds the object graph. The completed root graph is then added to the async session and flushed, so related SQLAlchemy models are persisted through the relationship's normal save-update cascade. Overrides work through double-underscore syntax as usual.

Example:

class Organization(Base):
    __tablename__ = "organization"
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    name: Mapped[str]
    owner_id: Mapped[int] = mapped_column(sa.ForeignKey("user.id"))
    owner: Mapped[User] = sa.orm.relationship()


class OrganizationFactory(fb.Factory):
    name = fb.Faker("company")
    owner = fb.SubFactory(UserFactory)

    class Meta:
        model = Organization


@pytest.fixture
def async_org_factory(sqlalchemy_async_factory):
    return sqlalchemy_async_factory(OrganizationFactory)


@pytest.mark.asyncio
async def test_nested_overrides(async_org_factory):
    org = await async_org_factory(owner__email="boss@example.com")
    assert org.owner.email == "boss@example.com"

Creating batches and concurrency

The async factory returns awaitables. create_batch(n) returns a list of awaitables you can await concurrently, e.g. with asyncio.gather.

import asyncio

users = await asyncio.gather(*async_user_factory.create_batch(5))
assert len(users) == 5

The plugin's semaphore fixture serializes each session.add() and flush() pair. This is required because SQLAlchemy's AsyncSession is not safe for concurrent mutation. Batch tasks may be awaited together, but their database writes run one at a time through the shared session.

You can override the fixture to coordinate with an application-owned lock, but its limit should remain 1 while the factories share one AsyncSession:

import asyncio

@pytest.fixture
def semaphore() -> asyncio.Semaphore:
    return asyncio.Semaphore(1)

Using without pytest

You can bind a factory manually via the helper:

import asyncio
from sqlalchemy.ext.asyncio import AsyncSession
from fbsa.factory import make_async_sqlalchemy_factory

async def make_user(session: AsyncSession):
    async_factory = make_async_sqlalchemy_factory(
        UserFactory,
        session=session,
        cache={},
        semaphore=asyncio.Semaphore(),
    )
    return await async_factory()

Notes

  • Factories must have Meta.model set to your SQLAlchemy model.
  • Only async usage is supported at the moment. There is no make_sync_sqlalchemy_factory in this package.
  • Plugin module name is fbsa.pytest; enabling it is required to use sqlalchemy_async_factory.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

factory_boy_sqlalchemy-0.0.3.tar.gz (33.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

factory_boy_sqlalchemy-0.0.3-py3-none-any.whl (5.9 kB view details)

Uploaded Python 3

File details

Details for the file factory_boy_sqlalchemy-0.0.3.tar.gz.

File metadata

  • Download URL: factory_boy_sqlalchemy-0.0.3.tar.gz
  • Upload date:
  • Size: 33.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for factory_boy_sqlalchemy-0.0.3.tar.gz
Algorithm Hash digest
SHA256 7e6c1037efbf8a12c6c35ba3b86e0ea99ca5bf9d3950d3111fd8e80d268cb205
MD5 2c11b1aade3d502147abb70cdcd29ed1
BLAKE2b-256 f9a6919e0ed2eca380e5c862786a2db0b0fece9b4be32e4a2fb8f1d623473af5

See more details on using hashes here.

Provenance

The following attestation bundles were made for factory_boy_sqlalchemy-0.0.3.tar.gz:

Publisher: publish.yml on dswistowski/factory-boy-sqlalchemy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file factory_boy_sqlalchemy-0.0.3-py3-none-any.whl.

File metadata

File hashes

Hashes for factory_boy_sqlalchemy-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 643f08c88ac759fbabc4c0924718266bb0963b317242d501fa884c89fe84f54f
MD5 32051258d48e91c67c588071a122d7f8
BLAKE2b-256 17c506a7bd10632b18a4eb44dcad5878b353561c64b7478f6ce27d29bb3a0ff9

See more details on using hashes here.

Provenance

The following attestation bundles were made for factory_boy_sqlalchemy-0.0.3-py3-none-any.whl:

Publisher: publish.yml on dswistowski/factory-boy-sqlalchemy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page