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_factoryfixture is available:- Option 1 (recommended): in
tests/conftest.pyaddpytest_plugins = "fbsa.pytest". - Option 2: run pytest with
-p fbsa.pytestor set it inpytest.iniviaaddopts = -p fbsa.pytest.
- Option 1 (recommended): in
-
Provide an
async_sessionfixture (typesqlalchemy.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 vanillafactory_boyfactory class, it returns an async‑enabled factory bound to the currentAsyncSession.async_factory_cache: an internal per‑test cache so the same base factory maps to a single bound async factory.semaphore: anasyncio.Semaphore(default 1) to serializeflush()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.modelset to your SQLAlchemy model. - Only async usage is supported at the moment. There is no
make_sync_sqlalchemy_factoryin this package. - Plugin module name is
fbsa.pytest; enabling it is required to usesqlalchemy_async_factory.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e6c1037efbf8a12c6c35ba3b86e0ea99ca5bf9d3950d3111fd8e80d268cb205
|
|
| MD5 |
2c11b1aade3d502147abb70cdcd29ed1
|
|
| BLAKE2b-256 |
f9a6919e0ed2eca380e5c862786a2db0b0fece9b4be32e4a2fb8f1d623473af5
|
Provenance
The following attestation bundles were made for factory_boy_sqlalchemy-0.0.3.tar.gz:
Publisher:
publish.yml on dswistowski/factory-boy-sqlalchemy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
factory_boy_sqlalchemy-0.0.3.tar.gz -
Subject digest:
7e6c1037efbf8a12c6c35ba3b86e0ea99ca5bf9d3950d3111fd8e80d268cb205 - Sigstore transparency entry: 2189226893
- Sigstore integration time:
-
Permalink:
dswistowski/factory-boy-sqlalchemy@9abf4670b290b4a59a9e97ff27a62895798ddc7c -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/dswistowski
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9abf4670b290b4a59a9e97ff27a62895798ddc7c -
Trigger Event:
release
-
Statement type:
File details
Details for the file factory_boy_sqlalchemy-0.0.3-py3-none-any.whl.
File metadata
- Download URL: factory_boy_sqlalchemy-0.0.3-py3-none-any.whl
- Upload date:
- Size: 5.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
643f08c88ac759fbabc4c0924718266bb0963b317242d501fa884c89fe84f54f
|
|
| MD5 |
32051258d48e91c67c588071a122d7f8
|
|
| BLAKE2b-256 |
17c506a7bd10632b18a4eb44dcad5878b353561c64b7478f6ce27d29bb3a0ff9
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
factory_boy_sqlalchemy-0.0.3-py3-none-any.whl -
Subject digest:
643f08c88ac759fbabc4c0924718266bb0963b317242d501fa884c89fe84f54f - Sigstore transparency entry: 2189226916
- Sigstore integration time:
-
Permalink:
dswistowski/factory-boy-sqlalchemy@9abf4670b290b4a59a9e97ff27a62895798ddc7c -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/dswistowski
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9abf4670b290b4a59a9e97ff27a62895798ddc7c -
Trigger Event:
release
-
Statement type: