Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ jobs:
working-directory: example_project
run: uv run mypy .

- name: Mypy (Example project async)
working-directory: example_project_async
run: uv run mypy .

plugin-tests:
name: Plugin tests
runs-on: ubuntu-latest
Expand Down
24 changes: 22 additions & 2 deletions .github/workflows/dialect-mariadb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,27 @@ jobs:
matrix:
python_version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
mariadb_version: ["10.6", "10.11", "11.8", "12.3"]
drivername: ["mariadb+mysqldb", "mariadb+pymysql", "mariadb+mariadbconnector", "mariadb+cymysql"]
drivername:
- "mariadb+mysqldb"
- "mariadb+pymysql"
- "mariadb+mariadbconnector"
- "mariadb+cymysql"
- "mariadb+asyncmy"
- "mariadb+aiomysql"

include:
- drivername: "mariadb+mysqldb"
project_directory: example_project
- drivername: "mariadb+pymysql"
project_directory: example_project
- drivername: "mariadb+mariadbconnector"
project_directory: example_project
- drivername: "mariadb+cymysql"
project_directory: example_project
- drivername: "mariadb+asyncmy"
project_directory: example_project_async
- drivername: "mariadb+aiomysql"
project_directory: example_project_async

services:
mariadb:
Expand Down Expand Up @@ -52,5 +72,5 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y libmariadb-dev

- name: Run example project tests
working-directory: example_project
working-directory: ${{ matrix.project_directory }}
run: uv run --extra mariadb pytest
24 changes: 22 additions & 2 deletions .github/workflows/dialect-mysql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,27 @@ jobs:
matrix:
python_version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
mysql_version: ["8.0", "8.4", "9.7"]
drivername: ["mysql+mysqldb", "mysql+pymysql", "mysql+mysqlconnector", "mysql+cymysql"]
drivername:
- "mysql+mysqldb"
- "mysql+pymysql"
- "mysql+mysqlconnector"
- "mysql+cymysql"
- "mysql+asyncmy"
- "mysql+aiomysql"

include:
- drivername: "mysql+mysqldb"
project_directory: example_project
- drivername: "mysql+pymysql"
project_directory: example_project
- drivername: "mysql+mysqlconnector"
project_directory: example_project
- drivername: "mysql+cymysql"
project_directory: example_project
- drivername: "mysql+asyncmy"
project_directory: example_project_async
- drivername: "mysql+aiomysql"
project_directory: example_project_async

services:
mysql:
Expand Down Expand Up @@ -46,5 +66,5 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y libmysqlclient-dev

- name: Run example project tests
working-directory: example_project
working-directory: ${{ matrix.project_directory }}
run: uv run --extra mysql pytest
21 changes: 19 additions & 2 deletions .github/workflows/dialect-postgresql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,24 @@ jobs:
matrix:
python_version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
postgres_version: ["14", "15", "16", "17", "18"]
drivername: ["postgresql+psycopg", "postgresql+psycopg2", "postgresql+pg8000", "postgresql+psycopg2cffi"]
drivername:
- "postgresql+psycopg"
- "postgresql+psycopg2"
- "postgresql+pg8000"
- "postgresql+psycopg2cffi"
- "postgresql+asyncpg"

include:
- drivername: "postgresql+psycopg"
project_directory: example_project
- drivername: "postgresql+psycopg2"
project_directory: example_project
- drivername: "postgresql+pg8000"
project_directory: example_project
- drivername: "postgresql+psycopg2cffi"
project_directory: example_project
- drivername: "postgresql+asyncpg"
project_directory: example_project_async

services:
postgres:
Expand Down Expand Up @@ -49,5 +66,5 @@ jobs:
cache-suffix: postgres-${{ matrix.python_version }}

- name: Run example project tests
working-directory: example_project
working-directory: ${{ matrix.project_directory }}
run: uv run --extra postgres pytest
9 changes: 7 additions & 2 deletions .github/workflows/dialect-sqlite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ jobs:
fail-fast: false
matrix:
python_version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
drivername: ["sqlite+pysqlite", "sqlite+pysqlcipher"]
drivername: ["sqlite+pysqlite", "sqlite+pysqlcipher", "sqlite+aiosqlite"]

include:
- drivername: "sqlite+pysqlite"
database_url: sqlite:///./database.db
project_directory: example_project
- drivername: "sqlite+pysqlcipher"
database_url: sqlite+pysqlcipher://:example@/database.db
project_directory: example_project
- drivername: "sqlite+aiosqlite"
database_url: sqlite+aiosqlite:///./database.db
project_directory: example_project_async

env:
UV_PYTHON: ${{ matrix.python_version }}
Expand All @@ -39,5 +44,5 @@ jobs:
cache-suffix: sqlite-${{ matrix.python_version }}

- name: Run example project tests
working-directory: example_project
working-directory: ${{ matrix.project_directory }}
run: uv run --extra sqlite pytest
24 changes: 19 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,27 @@ repos:
- id: ruff-format
name: "Ruff format"

# Not using https://github.com/pre-commit/mirrors-mypy as it requires stating dependencies manually
# Ref https://stackoverflow.com/a/75003826/2807941
- repo: local
hooks:
- id: mypy
name: mypy
language: system
entry: uv run mypy
require_serial: true # Single process mode to avoid fork bomb
language: python
types: [python]
args: ["src"]
pass_filenames: false

- repo: local
hooks:
- id: mypy-example-project
name: mypy for example_project
language: system
entry: bash -c 'cd example_project && exec uv run mypy .' --
pass_filenames: false

- repo: local
hooks:
- id: mypy-example-project-async
name: mypy for example_project_async
language: system
entry: bash -c 'cd example_project_async && exec uv run mypy .' --
pass_filenames: false
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,11 @@ It automatically provisions a dedicated test database per worker and runs Alembi
* `alembic` >= 1.16
* `pytest` >= 8.4
* `pytest-xdist` >= 3.0 (Optional)
* `pytest-asyncio` >= 1.3.0 (Optional)


## Supported dialects

> [!WARNING]
> Async engines are not yet supported

| Driver | Status |
| ------ | ------ |
| `sqlite+pysqlite` | ✅ Supported
Expand All @@ -45,18 +43,18 @@ It automatically provisions a dedicated test database per worker and runs Alembi
| `postgresql+psycopg2` | ✅ Supported
| `postgresql+pg8000` | ✅ Supported
| `postgresql+psycopg2cffi` | ✅ Supported
| `postgresql+asyncpg` |
| `postgresql+asyncpg` | ✅ Supported
| `mariadb+mysqldb` | ✅ Supported
| `mariadb+pymysql` | ✅ Supported
| `mariadb+mariadbconnector` | ✅ Supported
| `mariadb+asyncmy` |
| `mariadb+aiomysql` |
| `mariadb+asyncmy` | ✅ Supported
| `mariadb+aiomysql` | ✅ Supported
| `mariadb+cymysql` | ✅ Supported
| `mysql+mysqldb` | ✅ Supported
| `mysql+pymysql` | ✅ Supported
| `mysql+mysqlconnector` | ✅ Supported
| `mysql+asyncmy` |
| `mysql+aiomysql` |
| `mysql+asyncmy` | ✅ Supported
| `mysql+aiomysql` | ✅ Supported
| `mysql+cymysql` | ✅ Supported

> [!NOTE]
Expand Down Expand Up @@ -114,7 +112,7 @@ def sqlalchemy_alembic_plugin_config() -> PluginConfig:
| `sqlalchemy_engine_url` | SQLAlchemy engine URL. Extracted from `engine` if empty
| `sqlalchemy_engine_kwargs` | Import path to a dict containing engine kwargs
| `sqlalchemy_orm_loader` | Import path to module or callable that loads all ORM necessary models

| `sqlalchemy_engine_scope` | Defines at which scope test engine should be activated (`session` or `function`). <br> Default: `session`

### Fixture override options

Expand All @@ -123,11 +121,12 @@ Arguments of `PluginConfig.build` class method to use in the `sqlalchemy_alembic
| Argument | Type | Description
| -------- | ---- | -----------
| `session_maker` | `sa.orm.sessionmaker[Session]` | Instance of a sessionmaker
| `metadata` | `sa.MetaData | Sequence[sa.MetaData]` | SQLAlchemy metadata
| `metadata` | `sa.MetaData \| Sequence[sa.MetaData]` | SQLAlchemy metadata
| `engine` | `sa.Engine` | Sqlalchemy engine instance
| `engine_url` | `str` | SQLAlchemy engine URL
| `engine_kwargs` | `dict[str, Any]` | `dict` with kwargs for `sa.create_engine` function. E.g. `{'json_serializer': my_json_serializer}`
| `orm_loader` | `Callable[[], Any]` | Callable, that will load all ORM necessary models
| `engine_scope` | `Literal['session', 'function']` | Defines at which scope test engine should be activated


## Combining file configuration and fixture override
Expand Down Expand Up @@ -188,5 +187,6 @@ def test_my_service():

| Fixture | Scope | Output | Description |
| ------- | ----- | ------ | ----------- |
| `sqlalchemy_alembic_setup` | session | Test SQLAlchemy engine | Autouse fixture that creates and migrates the test database and rebinds the configured sessionmaker
| `sqlalchemy_alembic_plugin_config` | session | This plugin's config | Extension point to override config values from python context
| `sqlalchemy_alembic_setup_session` | session | Test SQLAlchemy engine | Session-scoped fixture to set up test database
| `sqlalchemy_alembic_setup_session` | function | Test SQLAlchemy engine or `None` | Function-scoped fixture to set up test database. <br>Used only in case of `engine_scope='function'`
2 changes: 1 addition & 1 deletion example_project/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from alembic import context

__import__('example_project.db')
__import__('example_project.models')

config = context.config

Expand Down
2 changes: 0 additions & 2 deletions example_project/alembic/script.py.mako
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}


def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}


def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
7 changes: 4 additions & 3 deletions example_project/example_project/db.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from collections.abc import Generator
from contextlib import contextmanager

from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker

from .config import settings

Expand All @@ -14,7 +15,7 @@ class Base(DeclarativeBase):
pass


def get_db():
def get_db() -> Generator[Session]:
db = SessionLocal()
try:
yield db
Expand All @@ -23,5 +24,5 @@ def get_db():


@contextmanager
def db_ctx():
def db_ctx() -> Generator[Session]:
yield from get_db()
1 change: 0 additions & 1 deletion example_project/example_project/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# models.py
from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column

Expand Down
5 changes: 4 additions & 1 deletion example_project/example_project/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from collections.abc import Generator

import pytest
from sqlalchemy.orm import Session


@pytest.fixture
def db():
def db() -> Generator[Session]:
from example_project.db import SessionLocal

db = SessionLocal()
Expand Down
10 changes: 6 additions & 4 deletions example_project/example_project/tests/test_create_user.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
from uuid import uuid4

import sqlalchemy as sa
from sqlalchemy.orm import Session

from example_project.main import create_user, db_ctx
from example_project.models import User
from ..db import db_ctx
from ..main import create_user
from ..models import User


def test_create_user(db: Session):
def test_create_user(db: Session) -> None:
name = str(uuid4())
email = f'{uuid4()}@example.com'

with db_ctx() as app_db:
create_user(app_db, name, email)

user = db.query(User).where(User.username == name).one_or_none()
user = db.scalar(sa.select(User).where(User.username == name))
assert user is not None
assert user.username == name
assert user.email == email
2 changes: 1 addition & 1 deletion example_project/example_project/tests/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlalchemy.orm import Session


def test_engine(db: Session):
def test_engine(db: Session) -> None:
assert isinstance(db.bind, Engine)
assert db.bind.url.database
assert 'test' in db.bind.url.database
Loading
Loading