Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2a9f84e
feat: add async support for PostgreSQL janitor and loader
tboy1337 Mar 13, 2026
526ee6c
test: enhance async tests for Database Janitor and Loader
tboy1337 Mar 13, 2026
2258c81
fix: improve error handling for missing optional dependencies in asyn…
tboy1337 Mar 13, 2026
5c92310
fix: resolve double plugin registration, add asyncio_mode and default…
tboy1337 Mar 13, 2026
ea71a07
Revert "fix: resolve double plugin registration, add asyncio_mode and…
tboy1337 Mar 13, 2026
990496a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 13, 2026
fe8c553
fix: enhance async fixture and retry tests
tboy1337 Mar 13, 2026
1384100
Merge branch 'async' of https://github.com/tboy1337/pytest-postgresql…
tboy1337 Mar 13, 2026
f16583a
refactor: update connection handling in AsyncDatabaseJanitor
tboy1337 Mar 13, 2026
38a5ae4
fix: add space in SQL query for connection termination
tboy1337 Mar 13, 2026
bdca11e
refactor: improve PostgreSQLExecutor and AsyncDatabaseJanitor handling
tboy1337 Mar 14, 2026
2267c00
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 14, 2026
717e81f
revert: remove Windows-specific changes deferred to PR-1182
tboy1337 Mar 14, 2026
685cb57
fix: resolve merge conflicts by keeping HEAD (exclude Windows changes…
tboy1337 Mar 14, 2026
83c7bae
refactor: enhance SQL query handling in DatabaseJanitor and AsyncData…
tboy1337 Mar 14, 2026
e9911ea
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 14, 2026
c23b364
fix: improve error handling in postgresql_async fixture
tboy1337 Mar 14, 2026
17d37ea
Merge branch 'async' of https://github.com/tboy1337/pytest-postgresql…
tboy1337 Mar 14, 2026
bfbff75
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 14, 2026
e2d10f2
fix: enhance postgresql_async fixture to provide synchronous stub whe…
tboy1337 Mar 14, 2026
24ec05d
Resolve merge conflict in factories/client.py: drop redundant in-body…
tboy1337 Mar 14, 2026
b40ac9d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 14, 2026
257e61d
feat: add async testing support and improve documentation
tboy1337 Jun 21, 2026
16b8ce3
refactor: streamline temporary directory creation in postgresql_proc …
tboy1337 Jun 21, 2026
c633ff6
Merge branch 'main' from dbfixtures/pytest-postgresql into async
tboy1337 Jun 21, 2026
bab9403
Fix xdist CI failures and raise patch coverage for async changes.
tboy1337 Jun 21, 2026
f4bf1b9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 21, 2026
c6390a5
Replace mock coverage tests with integration tests for async paths.
tboy1337 Jun 21, 2026
4cf5706
Fix postgresql_oldest and Windows CI failures for async support.
tboy1337 Jun 21, 2026
50377e1
Replace mock AsyncDatabaseJanitor SQL tests with Postgres integration…
tboy1337 Jun 21, 2026
91b7305
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 21, 2026
96289b8
Fix mypy errors in janitor integration test helpers.
tboy1337 Jun 21, 2026
8c866fc
Update pytest-asyncio dependency to version 0.24 in Pipfile and pypro…
tboy1337 Jun 21, 2026
18f04d7
Enhance README and client.py documentation for async fixtures, specif…
tboy1337 Jun 21, 2026
7c28065
Update error message in test for pytest-asyncio version requirement t…
tboy1337 Jun 21, 2026
2a51cd2
Update pytest-postgresql plugin to set Windows event loop policy cond…
tboy1337 Jun 21, 2026
34c3d47
Refactor password handling in janitor tests to address linting warnin…
tboy1337 Jun 21, 2026
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
3 changes: 3 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ pytest-postgresql = {path = ".", editable = true}
[dev-packages]
towncrier = "==25.8.0"
psycopg-binary = {version = "==3.3.4", markers="implementation_name == 'cpython'"}
pytest-asyncio = ">=0.24"
aiofiles = ">=23.0"
types-aiofiles = ">=23.0"
coverage = ">=7.14.1"
pytest-xdist = "==3.8.0"
mock = "==5.2.0"
Expand Down
60 changes: 58 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ Quick Start

You will also need to install ``psycopg`` (version 3). See `its installation instructions <https://www.psycopg.org/psycopg3/docs/basic/install.html>`_.

For async tests with ``psycopg.AsyncConnection``, install the optional async extra:

.. code-block:: sh

pip install pytest-postgresql[async]

This installs:

* ``pytest-asyncio`` (>= 0.24) — required for ``@pytest.mark.asyncio`` and
``postgresql_async`` fixtures. Version 0.24 or newer is required because
scoped async fixtures rely on the ``loop_scope`` argument introduced in
that release.
* ``aiofiles`` (>= 23.0) — required only when loading SQL files via the
async loader (``sql_async``).

.. note::

While this plugin requires ``psycopg`` 3 to manage the database, your application code can still use ``psycopg`` 2.
Expand All @@ -54,6 +69,21 @@ Quick Start
cur.execute("CREATE TABLE test (id serial PRIMARY KEY, num integer, data varchar);")
postgresql.commit()

For async code, use ``postgresql_async`` with ``pytest.mark.asyncio``:

.. code-block:: python

import pytest

@pytest.mark.asyncio
async def test_example_async(postgresql_async):
"""Check main async postgresql fixture."""
async with postgresql_async.cursor() as cur:
await cur.execute(
"CREATE TABLE test (id serial PRIMARY KEY, num integer, data varchar);"
)
await postgresql_async.commit()

How to use
==========

Expand All @@ -73,13 +103,33 @@ The plugin provides two main types of fixtures:
**1. Client Fixtures**
These provide a connection to a database for your tests.

* **postgresql** - A function-scoped fixture. It returns a connected ``psycopg.Connection``.
* **postgresql** - A function-scoped fixture (by default). It returns a connected ``psycopg.Connection``.
After each test, it terminates leftover connections and drops the test database to ensure isolation.
* **postgresql_async** - The async counterpart. It returns a connected ``psycopg.AsyncConnection``.
Requires ``pytest-postgresql[async]`` (``pytest-asyncio`` >= 0.24), and each test must be
marked with ``@pytest.mark.asyncio``.

**Async fixtures**
``postgresql_async`` and custom factories created with ``factories.postgresql_async`` are
async generator fixtures. They use ``pytest_asyncio.fixture`` with matching ``scope`` and
``loop_scope`` so that non-function scopes (for example ``module`` or ``session``) share the
same event loop for the fixture lifetime.

Minimum versions when installing manually instead of via ``[async]``:

.. code-block:: text

pytest-asyncio >= 0.24
aiofiles >= 23.0 # only for async SQL file loading

If ``pytest-asyncio`` is missing, fixture setup raises ``ImportError``. If an older
``pytest-asyncio`` (< 0.24) is installed, plugin registration fails with
``TypeError: fixture() got an unexpected keyword argument 'loop_scope'``.

**2. Process Fixtures**
These manage the PostgreSQL server lifecycle.

* **postgresql_proc** - A session-scoped fixture that starts a PostgreSQL instance on its first use and stops it when all tests are finished.
* **postgresql_proc** - A session-scoped fixture (by default) that starts a PostgreSQL instance on its first use and stops it when all tests are finished.
* **postgresql_noproc** - A fixture for connecting to an already running PostgreSQL instance (e.g., in Docker or CI).

Customizing Fixtures
Expand All @@ -98,6 +148,12 @@ You can create additional fixtures using factories:
# Create a client fixture that uses the custom process
postgresql_my = factories.postgresql('postgresql_my_proc')

# Async client fixture (requires pytest-postgresql[async], pytest-asyncio >= 0.24)
postgresql_my_async = factories.postgresql_async('postgresql_my_proc')

All factories accept an optional ``scope`` parameter (``"session"``, ``"package"``, ``"module"``, ``"class"``, or ``"function"``).
Defaults are unchanged: ``"function"`` for client fixtures and ``"session"`` for process fixtures.

.. note::

Each process fixture can be configured independently through factory arguments.
Expand Down
3 changes: 3 additions & 0 deletions newsfragments/1235.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added async PostgreSQL fixture support via ``postgresql_async`` factory and ``AsyncDatabaseJanitor``.
Added configurable fixture ``scope`` parameter to ``postgresql``, ``postgresql_async``, ``postgresql_proc``, and ``postgresql_noproc`` factories (defaults preserved: ``"function"`` for client fixtures, ``"session"`` for process fixtures).
Added optional ``async`` extra (``pip install pytest-postgresql[async]``) providing ``pytest-asyncio`` (>= 0.24) and ``aiofiles`` dependencies.
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ dependencies = [
]
requires-python = ">= 3.10"

[project.optional-dependencies]
async = [
"pytest-asyncio >= 0.24",
"aiofiles >= 23.0"
]

[project.urls]
"Source" = "https://github.com/dbfixtures/pytest-postgresql"
"Bug Tracker" = "https://github.com/dbfixtures/pytest-postgresql/issues"
Expand Down
4 changes: 2 additions & 2 deletions pytest_postgresql/factories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
# along with pytest-postgresql. If not, see <http://www.gnu.org/licenses/>.
"""Fixture factories for postgresql fixtures."""

from pytest_postgresql.factories.client import postgresql
from pytest_postgresql.factories.client import postgresql, postgresql_async
from pytest_postgresql.factories.noprocess import postgresql_noproc
from pytest_postgresql.factories.process import PortType, postgresql_proc

__all__ = ("postgresql_proc", "postgresql_noproc", "postgresql", "PortType")
__all__ = ("postgresql_proc", "postgresql_noproc", "postgresql", "postgresql_async", "PortType")
103 changes: 98 additions & 5 deletions pytest_postgresql/factories/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,47 @@
# along with pytest-postgresql. If not, see <http://www.gnu.org/licenses/>.
"""Fixture factory for postgresql client."""

from typing import Callable, Iterator
from typing import Any, AsyncIterator, Callable, Iterator, cast

import psycopg
import pytest
from psycopg import Connection
from psycopg import AsyncConnection, Connection
from pytest import FixtureRequest

from pytest_postgresql.config import get_config
from pytest_postgresql.executor import PostgreSQLExecutor
from pytest_postgresql.executor_noop import NoopExecutor
from pytest_postgresql.janitor import DatabaseJanitor
from pytest_postgresql.janitor import AsyncDatabaseJanitor, DatabaseJanitor
from pytest_postgresql.types import FixtureScopeT

pytest_asyncio: Any = None
try:
import pytest_asyncio as _pytest_asyncio_module

pytest_asyncio = _pytest_asyncio_module
except ImportError:
pass


def postgresql(
process_fixture_name: str,
dbname: str | None = None,
isolation_level: "psycopg.IsolationLevel | None" = None,
scope: FixtureScopeT = "function",
) -> Callable[[FixtureRequest], Iterator[Connection]]:
"""Return connection fixture factory for PostgreSQL.

:param process_fixture_name: name of the process fixture
:param dbname: database name
:param isolation_level: optional postgresql isolation level
defaults to server's default
:param scope: fixture scope; by default "function" which is recommended.
:returns: function which makes a connection to postgresql
"""

@pytest.fixture
@pytest.fixture(scope=scope)
def postgresql_factory(request: FixtureRequest) -> Iterator[Connection]:
"""Fixture factory for PostgreSQL.
"""Fixture connection factory for PostgreSQL.

:param request: fixture request object
:returns: postgresql client
Expand Down Expand Up @@ -85,3 +96,85 @@ def postgresql_factory(request: FixtureRequest) -> Iterator[Connection]:
db_connection.close()

return postgresql_factory


def postgresql_async(
process_fixture_name: str,
dbname: str | None = None,
isolation_level: "psycopg.IsolationLevel | None" = None,
scope: FixtureScopeT = "function",
) -> Callable[[FixtureRequest], AsyncIterator[AsyncConnection]]:
"""Return async connection fixture factory for PostgreSQL.

Requires ``pytest-asyncio`` >= 0.24 (install via ``pip install pytest-postgresql[async]``).
Scoped fixtures pass ``loop_scope=scope`` to ``pytest_asyncio.fixture``, which is only
supported in pytest-asyncio 0.24 and later.

:param process_fixture_name: name of the process fixture
:param dbname: database name
:param isolation_level: optional postgresql isolation level
defaults to server's default
:param scope: fixture scope; by default "function" which is recommended.
:returns: function which makes an async connection to postgresql
"""
if pytest_asyncio is None:

@pytest.fixture(scope=scope)
def postgresql_async_stub(request: FixtureRequest) -> None:
"""Sync stub that raises ImportError when pytest-asyncio is absent."""
raise ImportError(
"pytest-asyncio >= 0.24 is required for async fixtures. "
"Install it with: pip install pytest-postgresql[async]"
)

return cast(
Callable[[FixtureRequest], AsyncIterator[AsyncConnection]],
postgresql_async_stub,
)

assert pytest_asyncio is not None

@pytest_asyncio.fixture(scope=scope, loop_scope=scope) # type: ignore[untyped-decorator]
async def postgresql_async_factory(request: FixtureRequest) -> AsyncIterator[AsyncConnection]:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"""Async connection fixture factory for PostgreSQL.

:param request: fixture request object
:returns: postgresql async client
"""
proc_fixture: PostgreSQLExecutor | NoopExecutor = request.getfixturevalue(process_fixture_name)
config = get_config(request)

pg_host = proc_fixture.host
pg_port = proc_fixture.port
pg_user = proc_fixture.user
pg_password = proc_fixture.password
pg_options = proc_fixture.options
pg_db = dbname or proc_fixture.dbname
janitor = AsyncDatabaseJanitor(
user=pg_user,
host=pg_host,
port=pg_port,
dbname=pg_db,
template_dbname=proc_fixture.template_dbname,
version=proc_fixture.version,
password=pg_password,
isolation_level=isolation_level,
)
if config.drop_test_database:
await janitor.drop()
async with janitor:
db_connection: AsyncConnection = await AsyncConnection.connect(
dbname=pg_db,
user=pg_user,
password=pg_password,
host=pg_host,
port=pg_port,
options=pg_options,
)
yield db_connection
await db_connection.close()

return cast(
Callable[[FixtureRequest], AsyncIterator[AsyncConnection]],
postgresql_async_factory,
)
5 changes: 4 additions & 1 deletion pytest_postgresql/factories/noprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from pytest_postgresql.config import get_config
from pytest_postgresql.executor_noop import NoopExecutor
from pytest_postgresql.janitor import DatabaseJanitor
from pytest_postgresql.types import FixtureScopeT


def xdistify_dbname(dbname: str) -> str:
Expand All @@ -46,6 +47,7 @@ def postgresql_noproc(
options: str = "",
load: list[Callable | str | Path] | None = None,
depends_on: str | None = None,
scope: FixtureScopeT = "session",
) -> Callable[[FixtureRequest], Iterator[NoopExecutor]]:
"""Postgresql noprocess factory.

Expand All @@ -57,10 +59,11 @@ def postgresql_noproc(
:param options: Postgresql connection options
:param load: List of functions used to initialize database's template.
:param depends_on: Optional name of the fixture to depend on.
:param scope: fixture scope; by default "session" which is recommended.
:returns: function which makes a postgresql process
"""

@pytest.fixture(scope="session")
@pytest.fixture(scope=scope)
def postgresql_noproc_fixture(request: FixtureRequest) -> Iterator[NoopExecutor]:
"""Noop Process fixture for PostgreSQL.

Expand Down
Loading
Loading