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
38 changes: 34 additions & 4 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,46 @@ Changelog

0.26.0 (unreleased)
-------------------

.. warning::

This release contains **breaking changes** as part of the context-first architecture migration.
Please read the :ref:`migration_guide` before upgrading.

Breaking Changes
^^^^^^^^^^^^^^^^
- **Context-first architecture**: All ORM state now lives in ``TortoiseContext`` instances
- **Removed** legacy test classes: ``test.TestCase``, ``test.IsolatedTestCase``, ``test.TruncationTestCase``, ``test.SimpleTestCase``
- **Removed** legacy test helpers: ``initializer()``, ``finalizer()``, ``env_initializer()``, ``getDBConfig()``
- **Changed** ``Tortoise.init()`` now returns ``TortoiseContext`` (previously returned ``None``)
- **Changed** Multiple separate ``asyncio.run()`` calls in sequence require explicit context management due to ContextVar scoping (uncommon pattern, see migration guide). The typical single ``asyncio.run(main())`` pattern continues to work unchanged.

Added
^^^^^
- ``TortoiseContext`` - explicit context manager for ORM state
- ``tortoise_test_context()`` - modern pytest fixture helper for test isolation
- ``get_connection(alias)`` - function to get connection by alias from current context
- ``get_connections()`` - function to get the ConnectionHandler from current context
- ``Tortoise.close_connections()`` - class method to close all connections
- ``Tortoise.is_inited()`` - explicit method version of ``Tortoise._inited`` property

Changed
^^^^^^^
- Framework integrations (FastAPI, Starlette, Sanic, etc.) now use ``Tortoise.close_connections()`` internally
- ``ConnectionHandler`` now uses instance-based ContextVar storage (each context has isolated connections)
- ``Tortoise.apps`` and ``Tortoise._inited`` now use ``classproperty`` descriptor (no metaclass)
- feat: foreignkey to model type (#2027)

Deprecated
^^^^^^^^^^
- ``from tortoise import connections`` - use ``get_connection()`` / ``get_connections()`` functions instead (still works but deprecated)

Fixed
^^^^^
- Fix ``AttributeError`` when using ``tortoise-orm`` with Nuitka-compiled Python code (#2053)
- Fix 'Self' in python standard library typing.py, but tortoise/model.py required it in 'typing_extensions' (#2051)
- Fix annotations being selected in ValuesListQuery despite not specified in `.values_list` fields list (#2059)

Changed
^^^^^
- feat: foreignkey to model type (#2027)

0.25
====

Expand Down
206 changes: 201 additions & 5 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,216 @@
"""
Pytest configuration for Tortoise ORM tests.

Uses function-scoped fixtures for true test isolation.
"""

import os

import pytest
import pytest_asyncio

from tortoise.contrib.test import finalizer, initializer
from tortoise.context import tortoise_test_context


@pytest.fixture(scope="session", autouse=True)
def initialize_tests(request):
# Reduce the default timeout for psycopg because the tests become very slow otherwise
def configure_psycopg():
"""Configure psycopg timeout for faster tests."""
try:
from tortoise.backends.psycopg import PsycopgClient

PsycopgClient.default_timeout = float(os.environ.get("TORTOISE_POSTGRES_TIMEOUT", "15"))
except ImportError:
pass


# ============================================================================
# HELPER FUNCTIONS
# ============================================================================


async def _truncate_all_tables(ctx) -> None:
"""Truncate all tables in the given context."""
if ctx.apps:
for model in ctx.apps.get_models_iterable():
quote_char = model._meta.db.query_class.SQL_CONTEXT.quote_char
await model._meta.db.execute_script(
f"DELETE FROM {quote_char}{model._meta.db_table}{quote_char}" # nosec
)


# ============================================================================
# PYTEST FIXTURES FOR TESTS
# These fixtures provide different isolation patterns for test scenarios
# ============================================================================


@pytest_asyncio.fixture(scope="module")
async def db_module():
"""
Module-scoped fixture: Creates TortoiseContext once per test module.

This is the base fixture that creates the database schema once per module.
Other fixtures build on top of this for different isolation strategies.

Note: Uses connection_label="models" to match standard test infrastructure.
"""
db_url = os.getenv("TORTOISE_TEST_DB", "sqlite://:memory:")
initializer(["tests.testmodels"], db_url=db_url)
request.addfinalizer(finalizer)
async with tortoise_test_context(
modules=["tests.testmodels"],
db_url=db_url,
app_label="models",
connection_label="models",
) as ctx:
yield ctx


@pytest_asyncio.fixture(scope="function")
async def db(db_module):
"""
Function-scoped fixture with transaction rollback cleanup.

Each test runs inside a transaction that gets rolled back at the end,
providing isolation without the overhead of schema recreation.

For databases that don't support transactions (e.g., MySQL MyISAM),
falls back to truncation cleanup.

This is the FASTEST isolation method - use for most tests.

Usage:
@pytest.mark.asyncio
async def test_something(db):
obj = await Model.create(name="test")
assert obj.id is not None
# Changes are rolled back after test
"""
# Get connection from the context using its default connection
conn = db_module.db()

# Check if the database supports transactions
if conn.capabilities.supports_transactions:
# Start a savepoint/transaction
transaction = conn._in_transaction()
await transaction.__aenter__()

try:
yield db_module
finally:
# Rollback the transaction (discards all changes made during test)
class _RollbackException(Exception):
pass

await transaction.__aexit__(_RollbackException, _RollbackException(), None)
else:
# For databases without transaction support (e.g., MyISAM),
# fall back to truncation cleanup
yield db_module
await _truncate_all_tables(db_module)


@pytest_asyncio.fixture(scope="function")
async def db_simple(db_module):
"""
Function-scoped fixture with NO cleanup between tests.

Tests share state - data from one test persists to the next within the module.
Use ONLY for read-only tests or tests that manage their own cleanup.

Usage:
@pytest.mark.asyncio
async def test_read_only(db_simple):
# Read-only operations, no writes
config = get_config()
assert "host" in config
"""
yield db_module


@pytest_asyncio.fixture(scope="function")
async def db_isolated():
"""
Function-scoped fixture with full database recreation per test.

Creates a completely fresh database for EACH test. This is the SLOWEST
method but provides maximum isolation.

Use when:
- Testing database creation/dropping
- Tests need custom model modules
- Tests must have completely clean state

Usage:
@pytest.mark.asyncio
async def test_with_fresh_db(db_isolated):
# Completely fresh database
...
"""
db_url = os.getenv("TORTOISE_TEST_DB", "sqlite://:memory:")
async with tortoise_test_context(
modules=["tests.testmodels"],
db_url=db_url,
app_label="models",
connection_label="models",
) as ctx:
yield ctx


@pytest_asyncio.fixture(scope="function")
async def db_truncate(db_module):
"""
Function-scoped fixture with table truncation cleanup.

After each test, all tables are truncated (DELETE FROM).
Faster than db_isolated but slower than db (transaction rollback).

Use when testing transaction behavior (can't use rollback for cleanup).

Usage:
@pytest.mark.asyncio
async def test_with_transactions(db_truncate):
async with in_transaction():
await Model.create(name="test")
# Table truncated after test
"""
yield db_module
await _truncate_all_tables(db_module)


# ============================================================================
# HELPER FIXTURES
# ============================================================================


def make_db_fixture(
modules: list[str], app_label: str = "models", connection_label: str = "models"
):
"""
Factory function to create custom db fixtures with different modules.

Use this in subdirectory conftest.py files for tests that need
custom model modules.

Example usage in tests/fields/conftest.py:
db_array = make_db_fixture(["tests.fields.test_array"])

Args:
modules: List of module paths to discover models from.
app_label: The app label for the models, defaults to "models".
connection_label: The connection alias name, defaults to "models".

Returns:
An async fixture function.
"""

@pytest_asyncio.fixture(scope="function")
async def _db_fixture():
db_url = os.getenv("TORTOISE_TEST_DB", "sqlite://:memory:")
async with tortoise_test_context(
modules=modules,
db_url=db_url,
app_label=app_label,
connection_label=connection_label,
) as ctx:
yield ctx

return _db_fixture
1 change: 0 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import json

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
Expand Down
Loading