Skip to main content

Pytest plugin that gives SQLAlchemy async engines the green light - automatically fixes MissingGreenlet errors

Project description

pytest-green-light

Python 3.8+ License: MIT Code style: black Type checking: ty

A pytest plugin that gives SQLAlchemy async engines the green light to work seamlessly in pytest fixtures. Solves the MissingGreenlet error automatically.

Status: Working (Beta)
Version: 0.2.0
Test Coverage: 100%
Python: 3.8+

This package is functional and ready to use! It automatically establishes greenlet context for SQLAlchemy async engines in pytest fixtures. Version 0.2.0 includes a fix for greenlet context persistence in async test execution.

The Problem

SQLAlchemy's async engines require a greenlet context to be established before async operations can be performed. When using pytest fixtures with async SQLAlchemy code, you encounter:

sqlalchemy.exc.MissingGreenlet: greenlet_spawn has not been called; can't call await_only() here.

This happens because pytest's async fixtures don't automatically establish the greenlet context that SQLAlchemy async requires.

The Solution

This plugin automatically establishes greenlet context before async tests run, allowing SQLAlchemy async engines to work seamlessly in pytest fixtures. Just install it and your async SQLAlchemy tests will work!

How It Works

The plugin uses pytest's pytest_pyfunc_call hook to wrap async test execution. When an async test function is detected, the plugin wraps it to establish greenlet context in the exact same async context where the test runs. This ensures that:

  1. Greenlet context is established right before the test executes
  2. Context is available in the same async context where SQLAlchemy operations occur
  3. Context persists for the entire test execution
  4. No manual intervention is required - it works automatically

This approach solves the context persistence issue that can occur when greenlet context is established in a fixture's async context but the test runs in a different async context.

Quick Start

pip install pytest-green-light

That's it! The plugin automatically activates when pytest runs. No configuration needed.

Installation

From PyPI

pip install pytest-green-light

Development Installation

git clone https://github.com/eddiethedean/pytest-green-light.git
cd pytest-green-light
pip install -e ".[dev]"

Usage

Basic Usage

Just install the plugin and use async SQLAlchemy in your tests. The plugin automatically establishes greenlet context, so you don't need to do anything special:

import pytest
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker

@pytest.fixture
async def async_engine():
    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
    yield engine
    await engine.dispose()

@pytest.fixture
async def async_session(async_engine):
    async_session_maker = sessionmaker(
        async_engine, class_=AsyncSession, expire_on_commit=False
    )
    async with async_session_maker() as session:
        yield session

async def test_my_async_code(async_session):
    # This works now! No more MissingGreenlet errors
    from sqlalchemy import text
    result = await async_session.execute(text("SELECT 1"))
    assert result.scalar() == 1

With pytest-asyncio

This plugin works alongside pytest-asyncio:

pip install pytest-asyncio pytest-green-light
import pytest

pytestmark = pytest.mark.asyncio

async def test_async_sqlalchemy(async_session):
    # Works perfectly!
    pass

Helper Fixtures

The plugin provides convenient fixtures for common patterns:

from pytest_green_light.fixtures import (
    async_engine_factory,
    async_session_factory,
    async_db_transaction,
)

@pytest.fixture
async def engine(async_engine_factory):
    async for eng in async_engine_factory("sqlite+aiosqlite:///:memory:"):
        yield eng

@pytest.fixture
async def session(async_session_factory, engine):
    async for sess in async_session_factory(engine):
        yield sess

async def test_with_session(session):
    # Session automatically has greenlet context
    from sqlalchemy import text
    result = await session.execute(text("SELECT 1"))
    assert result.scalar() == 1

Transaction Management

Automatic transaction rollback for clean test isolation:

from pytest_green_light.fixtures import async_db_transaction
from sqlalchemy.orm import declarative_base, Mapped, mapped_column
from sqlalchemy import Integer, String

Base = declarative_base()

class MyModel(Base):
    __tablename__ = "my_model"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String)

async def test_with_rollback(session):
    # All changes automatically rolled back after test
    async with async_db_transaction(session):
        obj = MyModel(name="test")
        session.add(obj)
        await session.commit()
        # Changes are visible during the transaction
        result = await session.get(MyModel, obj.id)
        assert result is not None
    
    # After transaction, changes are rolled back
    result = await session.get(MyModel, obj.id)
    assert result is None  # Rolled back

Transaction Options

# Commit changes instead of rolling back
async with async_db_transaction(session, rollback=False):
    session.add(obj)
    await session.commit()
    # Changes persist after context

# Nested transactions (savepoints)
async with async_db_transaction(session, nested=True):
    # Creates a savepoint
    session.add(obj)
    await session.commit()
    # Changes rolled back when context exits

Configuration Options

The plugin works automatically with no configuration needed! It automatically:

  • Detects async test functions
  • Establishes greenlet context before tests run
  • Works with any async testing plugin (pytest-asyncio, alt-pytest-asyncio, etc.)

You can also customize behavior with command-line options:

# Disable automatic greenlet context establishment
pytest --green-light-no-autouse

# Enable debug logging for greenlet context
pytest --green-light-debug

How It Works

The plugin uses pytest hooks and an auto-use fixture (ensure_greenlet_context) to:

  1. Automatic Detection: Detects when async tests are about to run
  2. Context Establishment: Calls SQLAlchemy's greenlet_spawn to establish the greenlet context
  3. Persistence: Ensures the context is available throughout the test execution
  4. Error Handling: Provides helpful diagnostics if MissingGreenlet errors still occur

The plugin works by:

  • Registering an auto-use async fixture that runs before every test
  • Calling await greenlet_spawn(_noop) to establish the greenlet context
  • Making the context available to all SQLAlchemy async operations
  • Intercepting MissingGreenlet exceptions to provide helpful diagnostics

Requirements

  • Python 3.8+
  • pytest 7.0+
  • SQLAlchemy 2.0+
  • greenlet 2.0+

Features

  • Automatic greenlet context establishment - No configuration needed
  • Helper fixtures - Easy engine and session creation with async_engine_factory and async_session_factory
  • Transaction management - Automatic rollback for test isolation with async_db_transaction
  • Full test coverage - 100% code coverage with 74+ tests
  • Multiple database support - SQLite, PostgreSQL, MySQL
  • Enhanced error messages - Helpful diagnostics when issues occur
  • Python 3.8+ support - Works with modern Python versions
  • Type checking - Fully type-checked with ty
  • Well-tested - Comprehensive test suite covering all features

Troubleshooting

MissingGreenlet Error Still Occurs

If you still see MissingGreenlet errors:

  1. Verify the plugin is installed and loaded:

    pip list | grep pytest-green-light
    pytest --version  # Should show 'green-light' plugin
    
  2. Enable debug mode:

    pytest --green-light-debug
    
  3. Check your async fixtures: Make sure you're using async def for fixtures and tests:

    @pytest.fixture
    async def my_fixture():  # Must be async
        ...
    
  4. Ensure pytest-asyncio is installed if using async test markers:

    pip install pytest-asyncio
    

The plugin's error handler will automatically provide detailed diagnostics if a MissingGreenlet error occurs.

Common Issues

Issue: Plugin doesn't seem to be working

  • Solution: Ensure you're using async def for fixtures and tests

Issue: Tests are slow

  • Solution: The plugin has minimal overhead. If you experience slowness, check your database connections

Issue: Want to disable the plugin for specific tests

  • Solution: Use --green-light-no-autouse flag or configure via pytest.ini

Examples

See the examples/ directory for integration examples with FastAPI and other frameworks.

Real-World Example

import pytest
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import declarative_base, Mapped, mapped_column
from sqlalchemy import Integer, String
from pytest_green_light.fixtures import async_db_transaction

Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    username: Mapped[str] = mapped_column(String(50))

@pytest.fixture
async def engine():
    engine = create_async_engine("sqlite+aiosqlite:///:memory:")
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield engine
    await engine.dispose()

@pytest.fixture
async def session(engine):
    from sqlalchemy.orm import sessionmaker
    async_session_maker = sessionmaker(
        engine, class_=AsyncSession, expire_on_commit=False
    )
    async with async_session_maker() as session:
        yield session

async def test_create_user(session):
    # Plugin ensures greenlet context is available
    user = User(username="testuser")
    session.add(user)
    await session.commit()
    
    result = await session.get(User, user.id)
    assert result.username == "testuser"

async def test_transaction_rollback(session):
    # Use transaction management for clean test isolation
    async with async_db_transaction(session):
        user = User(username="temp")
        session.add(user)
        await session.commit()
        # User exists during transaction
        assert await session.get(User, user.id) is not None
    
    # After transaction, user is rolled back
    assert await session.get(User, user.id) is None

Development

Setup Development Environment

# Clone the repository
git clone https://github.com/eddiethedean/pytest-green-light.git
cd pytest-green-light

# Install with dev dependencies
pip install -e ".[dev]"

# Install all optional dependencies (for full test suite)
pip install -e ".[all]"

Running Tests

# Run all tests
pytest

# Run with coverage report
pytest --cov=pytest_green_light --cov-report=term-missing

# Run specific test file
pytest tests/test_plugin.py

# Run tests for specific database
pytest -m postgresql  # Requires PostgreSQL
pytest -m mysql      # Requires MySQL

Code Quality

# Type checking
python -m ty check src/

# Linting
ruff check .

# Format code
ruff format .

# Run all checks
ruff check . && ruff format . && python -m ty check src/

Project Status

  • Core functionality working - Plugin establishes greenlet context automatically
  • 100% test coverage - Comprehensive test suite with 74+ tests
  • Type checking - Fully type-checked with ty
  • Comprehensive documentation - README, examples, and inline docs
  • Multiple database support - SQLite, PostgreSQL, MySQL tested
  • Error diagnostics - Helpful error messages with troubleshooting steps

FAQ

Q: Do I need to configure anything?
A: No! Just install the plugin and it works automatically.

Q: Does this work with pytest-asyncio?
A: Yes! The plugin works alongside pytest-asyncio and other async testing plugins.

Q: Can I disable the plugin for specific tests?
A: Yes, use pytest --green-light-no-autouse to disable automatic context establishment.

Q: What Python versions are supported?
A: Python 3.8+ is supported and tested.

Q: Does this work with all SQLAlchemy versions?
A: Yes, it works with SQLAlchemy 2.0+ and handles different import paths automatically.

Q: What about performance?
A: The plugin has minimal overhead - it simply establishes the greenlet context once per test.

Issues & Support

If you encounter any issues or have questions, please file an issue on GitHub.

When reporting issues, please include:

  • Python version
  • SQLAlchemy version
  • pytest version
  • A minimal example that reproduces the issue
  • The output of pytest --green-light-debug if applicable

Contributing

Contributions welcome! This plugin was created to solve a real-world problem with testing async SQLAlchemy code.

We welcome:

  • Bug reports
  • Feature requests
  • Documentation improvements
  • Code contributions
  • Test coverage improvements

See CONTRIBUTING.md for detailed guidelines on how to contribute.

Development Workflow

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Run tests and ensure 100% coverage: pytest --cov=pytest_green_light
  5. Run type checking: python -m ty check src/
  6. Run linting: ruff check . && ruff format .
  7. Commit your changes (git commit -m 'Add amazing feature')
  8. Push to the branch (git push origin feature/amazing-feature)
  9. Open a Pull Request

License

MIT License - see LICENSE for details.

Author

Odos Matthews


Made with ❤️ for the Python async testing community

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

pytest_green_light-0.2.0.tar.gz (26.2 kB view details)

Uploaded Source

Built Distribution

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

pytest_green_light-0.2.0-py3-none-any.whl (11.9 kB view details)

Uploaded Python 3

File details

Details for the file pytest_green_light-0.2.0.tar.gz.

File metadata

  • Download URL: pytest_green_light-0.2.0.tar.gz
  • Upload date:
  • Size: 26.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.8.18

File hashes

Hashes for pytest_green_light-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ddf5a5001a9ef2486bfe8cb636ec82d74ee1d1e7e23b59b08a1eca433e05d687
MD5 acaf619038beb91794123a77da958541
BLAKE2b-256 7784525281fbc88ecb66e2ed5217a00054d703f7532699325142900e6d9a4e05

See more details on using hashes here.

File details

Details for the file pytest_green_light-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pytest_green_light-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9d6a4f4187f5910d7fff535335c1a94b420d56acea1f3398f4f0f6c733b2c384
MD5 90c511b15d2918dcc6e60e622cac8dad
BLAKE2b-256 44d6ea1f85cd1288fd58e97e8ef154302a94d6357ae3c28d794f6be0186d3a55

See more details on using hashes here.

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