Pytest plugin that gives SQLAlchemy async engines the green light - automatically fixes MissingGreenlet errors
Project description
pytest-green-light
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:
- Greenlet context is established right before the test executes
- Context is available in the same async context where SQLAlchemy operations occur
- Context persists for the entire test execution
- 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:
- Automatic Detection: Detects when async tests are about to run
- Context Establishment: Calls SQLAlchemy's
greenlet_spawnto establish the greenlet context - Persistence: Ensures the context is available throughout the test execution
- Error Handling: Provides helpful diagnostics if
MissingGreenleterrors 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
MissingGreenletexceptions 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_factoryandasync_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:
-
Verify the plugin is installed and loaded:
pip list | grep pytest-green-light pytest --version # Should show 'green-light' plugin
-
Enable debug mode:
pytest --green-light-debug -
Check your async fixtures: Make sure you're using
async deffor fixtures and tests:@pytest.fixture async def my_fixture(): # Must be async ...
-
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 deffor 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-autouseflag 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-debugif 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
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests and ensure 100% coverage:
pytest --cov=pytest_green_light - Run type checking:
python -m ty check src/ - Run linting:
ruff check . && ruff format . - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT License - see LICENSE for details.
Author
Odos Matthews
- GitHub: @eddiethedean
- Repository: pytest-green-light
Made with ❤️ for the Python async testing community
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ddf5a5001a9ef2486bfe8cb636ec82d74ee1d1e7e23b59b08a1eca433e05d687
|
|
| MD5 |
acaf619038beb91794123a77da958541
|
|
| BLAKE2b-256 |
7784525281fbc88ecb66e2ed5217a00054d703f7532699325142900e6d9a4e05
|
File details
Details for the file pytest_green_light-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pytest_green_light-0.2.0-py3-none-any.whl
- Upload date:
- Size: 11.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.8.18
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d6a4f4187f5910d7fff535335c1a94b420d56acea1f3398f4f0f6c733b2c384
|
|
| MD5 |
90c511b15d2918dcc6e60e622cac8dad
|
|
| BLAKE2b-256 |
44d6ea1f85cd1288fd58e97e8ef154302a94d6357ae3c28d794f6be0186d3a55
|