Skip to content

feat: add async PostgreSQL fixture support#1295

Open
tboy1337 wants to merge 46 commits into
dbfixtures:mainfrom
tboy1337:async
Open

feat: add async PostgreSQL fixture support#1295
tboy1337 wants to merge 46 commits into
dbfixtures:mainfrom
tboy1337:async

Conversation

@tboy1337

@tboy1337 tboy1337 commented Mar 13, 2026

Copy link
Copy Markdown
Contributor
  • Adds postgresql_async fixture factory and AsyncDatabaseJanitor class for testing async code against PostgreSQL using psycopg.AsyncConnection.
  • Adds an optional async extra (pip install pytest-postgresql[async]) that pulls in pytest-asyncio (>= 1.4) and aiofiles as dependencies.
  • Refactors sync DatabaseJanitor DDL to use psycopg.sql composable identifiers (same approach used by the async janitor).
  • Ensures per-port sentinel/lock files are cleaned up in postgresql_proc even when setup fails.
  • Configures Windows asyncio event-loop handling for psycopg async compatibility, using pytest-asyncio's loop-factory hook on >= 1.4 and avoiding the deprecated policy API on Python 3.14+.

Changes

  • pytest_postgresql/factories/client.py: new postgresql_async factory with a synchronous stub that raises ImportError when pytest-asyncio is absent
  • pytest_postgresql/factories/process.py: try/finally port-lock cleanup on teardown
  • pytest_postgresql/janitor.py: new AsyncDatabaseJanitor with init, drop, load, cursor, and async context manager support; sync janitor refactored to psycopg.sql identifiers
  • pytest_postgresql/loader.py: new build_loader_async and sql_async for async SQL file loading via aiofiles; switch to importlib.import_module and allow deeply nested import paths
  • pytest_postgresql/retry.py: new retry_async coroutine mirroring the existing retry helper
  • pytest_postgresql/plugin.py: Windows SelectorEventLoop support via optional pytest-asyncio loop-factory hook
  • pyproject.toml: new [async] optional dependency group
  • README.rst: async installation, usage, and Windows event-loop notes
  • tests/: unit and integration tests covering async paths, plugin behavior, and process fixture cleanup

tboy1337 and others added 6 commits March 13, 2026 20:17
- Introduced AsyncDatabaseJanitor for managing database state asynchronously.
- Added async loading capabilities with build_loader_async and sql_async functions.
- Updated factories to include async versions of PostgreSQL fixtures.
- Enhanced tests to cover async functionality for janitor and loader.
- Updated pyproject.toml to include optional dependencies for async support.
- Added tests for custom database name handling in AsyncDatabaseJanitor.
- Implemented tests for database initialization and dropping with various configurations.
- Included tests for callable resolution in build_loader and build_loader_async with dot-separated paths.
- Improved error handling tests for sql_async when aiofiles is not installed.
…c functions

- Updated the loader and client to check for aiofiles and pytest_asyncio imports at runtime, raising ImportError with clear messages if they are not installed.
- Refactored tests to mock the absence of these dependencies more effectively, ensuring proper error handling is validated.
… default postgresql_async fixture"

This reverts commit 5c92310.
@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces async PostgreSQL fixture support via postgresql_async factory backed by AsyncDatabaseJanitor, retry_async, and build_loader_async/sql_async utilities. Declares optional async extra (pytest-asyncio >= 1.4, aiofiles >= 23.0). Fixes process fixture to reliably remove per-port marker files on teardown failures. Adds Windows asyncio event loop policy configuration for pytest-asyncio compatibility.

Changes

Async PostgreSQL support

Layer / File(s) Summary
Async retry mechanism
pytest_postgresql/retry.py, tests/test_retry.py
Adds retry_async awaiting coroutines with asyncio.sleep(1) between retries on matching exceptions until timeout; comprehensive tests cover immediate success, transient retries with attempt counts, timeout with TimeoutError, and non-matching exception propagation.
Async SQL loader utilities
pytest_postgresql/loader.py, tests/test_loader.py
Adds build_loader_async resolving callables and import strings (colon and dot-separated formats) to async callables, and sql_async reading SQL files via aiofiles and executing against psycopg.AsyncConnection; raises ImportError when aiofiles is absent; tests verify callable resolution, import-path handling, partial construction, missing-dependency error, and successful execute/commit flow.
AsyncDatabaseJanitor and sync janitor SQL refactoring
pytest_postgresql/janitor.py
Introduces AsyncDatabaseJanitor with async init (creating database with optional template and IS_TEMPLATE flag), drop (terminating backends and removing database), load (building and awaiting async loaders), cursor (async context manager with retry_async and isolation-level handling), and __aenter__/__aexit__ lifecycle wiring; refactors sync DatabaseJanitor to use psycopg.sql composition instead of f-string concatenation for all database management queries.
AsyncDatabaseJanitor comprehensive tests
tests/test_janitor.py
Full async test suite covering version type casting, cursor connection defaults and kwargs forwarding (password and custom dbname), sync and async loader execution, live init()/drop() lifecycle validation via system catalogue queries, as_template flag verification, template_dbname cloning with base and clone cleanup, is_template() boolean checks, async context manager wiring, _terminate_connection SQL content and parameter binding, and psycopg3-style async connection/cursor mock helper.
postgresql_async fixture factory and public exports
pytest_postgresql/factories/client.py, pytest_postgresql/factories/__init__.py
Adds postgresql_async factory registering an @pytest_asyncio.fixture that constructs AsyncDatabaseJanitor, optionally drops the test database, connects via AsyncConnection.connect(), yields the connection, and closes on teardown; returns a synchronous stub raising ImportError when pytest-asyncio is unavailable; exports the symbol via public API.
Plugin asyncio integration and Windows event loop support
pytest_postgresql/plugin.py
Implements Windows selector event loop policy configuration for pytest-asyncio compatibility; detects loop-factory support and Python version to gate deprecated event-loop policy on Windows; exports pytest_asyncio_loop_factories hook on Windows to provide a "selector" loop factory backed by asyncio.SelectorEventLoop; exports postgresql_async fixture factory wired to postgresql_proc.
postgresql_async ImportError fallback and asyncio integration tests
tests/test_factory_errors.py, tests/test_plugin_asyncio.py
Validates factory creation succeeds when pytest_asyncio is patched to None, that the unwrapped stub is synchronous (no __await__), and that invoking it raises ImportError matching "pytest-asyncio >= 1.4"; tests Windows selector event loop construction and closure, confirm pytest_configure skips deprecated policy calls on Python 3.14+, and assert pytest_asyncio_loop_factories returns None on non-Windows versus a {"selector"} factory mapping on Windows.
Process fixture port-lock cleanup
pytest_postgresql/factories/process.py, tests/test_executor.py
Wraps the postgresql_proc fixture setup, yield, and teardown in try/finally to unlink postgresql-{port}.port with missing_ok=True even on startup failures; updates existing executor integration tests to use process._pg_port(None, config, []) instead of -1 for port selection; adds new tests verifying the sentinel file is created after first next(gen) and removed after normal generator exhaustion, and that it is not left behind when setup raises after a port is claimed.
Conftest async fixtures
tests/conftest.py
Adds postgresql2_async (with dbname="test-db") and postgresql_load_1_async module-level async fixtures both wired to postgresql_proc2 for use in integration tests.
Integration test variants and example tests
tests/test_template_database.py, tests/test_postgres_options_plugin.py, tests/docker/test_noproc_docker.py, tests/examples/test_drop_test_database_async.py
Adds async integration tests for template-database lifecycle (test_template_database_async validating truncation behaviour via async cursor) and drop-test-database workflow validation (test_postgres_drop_test_database_async via subprocess execution). Extends Docker noproc tests with async variants using AsyncConnection. Adds example test module demonstrating drop-test-database override with async fixture.
Dependencies, documentation, and changelog
pyproject.toml, Pipfile, README.rst, newsfragments/1235.feature.rst, newsfragments/1295.bugfix.rst
Declares async optional extra in pyproject.toml with pytest-asyncio >= 1.4 and aiofiles >= 23.0; adds dev dependencies to Pipfile; extends README.rst with async installation instructions, Quick Start example using postgresql_async with pytest.mark.asyncio and cursor usage, How does it work overview of async fixtures, and Customising Fixtures example for custom async client fixtures; adds feature changelog entry and bugfix entry documenting async support and Windows Python 3.14 DeprecationWarning fix.

Sequence Diagram(s)

sequenceDiagram
  participant Test as Test (pytest-asyncio)
  participant Factory as postgresql_async factory
  participant Janitor as AsyncDatabaseJanitor
  participant retry_async
  participant AsyncConnection as psycopg.AsyncConnection
  participant PostgreSQL

  Factory->>Janitor: construct(host, port, dbname, ...)
  Janitor->>retry_async: connect to postgres db
  retry_async->>AsyncConnection: AsyncConnection.connect(host, port, ...)
  AsyncConnection->>PostgreSQL: open session
  PostgreSQL-->>AsyncConnection: connected
  Janitor->>PostgreSQL: CREATE DATABASE dbname
  PostgreSQL-->>Janitor: created
  Janitor-->>Test: yield AsyncConnection
  Test->>AsyncConnection: await cursor.execute(...)
  AsyncConnection->>PostgreSQL: execute query
  PostgreSQL-->>AsyncConnection: result rows
  Test->>AsyncConnection: await commit()
  Test-->>Factory: test complete
  Janitor->>PostgreSQL: pg_terminate_backend
  Janitor->>PostgreSQL: DROP DATABASE dbname
  PostgreSQL-->>Janitor: dropped
  AsyncConnection->>PostgreSQL: close()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • dbfixtures/pytest-postgresql#1259: Modifies pytest_postgresql/janitor.py sync DatabaseJanitor template and database creation/drop logic; this PR refactors the same logic to use psycopg.sql composition and introduces async equivalents.

Suggested labels

Feature

Suggested reviewers

  • fizyk

Poem

🐇 Async hops through the code at last,
AsyncDatabaseJanitor holds the mast!
retry_async sleeps then tries once more,
aiofiles unlocks the SQL store.
Port locks vanish in finally grace —
This rabbit's tests run at async pace! 🚀

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 83.76% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Newsfragment Check ✅ Passed Two properly formatted newsfragments have been added: 1235.feature.rst (async PostgreSQL fixture support) and 1295.bugfix.rst (Windows DeprecationWarning fix), both using valid towncrier types.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding async PostgreSQL fixture support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tboy1337

Copy link
Copy Markdown
Contributor Author

pre-commit broke coderabbit @fizyk

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pytest_postgresql/factories/process.py (1)

85-110: ⚠️ Potential issue | 🟠 Major

Configurable process scopes currently leak the port lock file.

Line 110 now allows repeated setup/teardown cycles, but the postgresql-<port>.port sentinel created on Line 140 is never removed if the fixture later tears down or fails. With scope="function" and a fixed port or narrow range, the second invocation can fail with PortForException even though the previous server has already stopped.

Possible fix
-        postgresql_executor = PostgreSQLExecutor(
-            executable=postgresql_ctl,
-            host=host or config.host,
-            port=pg_port,
-            user=user or config.user,
-            password=password or config.password,
-            dbname=pg_dbname,
-            options=options or config.options,
-            datadir=str(datadir),
-            unixsocketdir=unixsocketdir or config.unixsocketdir,
-            logfile=str(logfile_path),
-            startparams=startparams or config.startparams,
-            postgres_options=postgres_options or config.postgres_options,
-        )
-        # start server
-        with postgresql_executor:
-            postgresql_executor.wait_for_postgres()
-            janitor = DatabaseJanitor(
-                user=postgresql_executor.user,
-                host=postgresql_executor.host,
-                port=postgresql_executor.port,
-                dbname=postgresql_executor.template_dbname,
-                as_template=True,
-                version=postgresql_executor.version,
-                password=postgresql_executor.password,
-            )
-            if config.drop_test_database:
-                janitor.drop()
-            with janitor:
-                for load_element in pg_load:
-                    janitor.load(load_element)
-                yield postgresql_executor
+        try:
+            postgresql_executor = PostgreSQLExecutor(
+                executable=postgresql_ctl,
+                host=host or config.host,
+                port=pg_port,
+                user=user or config.user,
+                password=password or config.password,
+                dbname=pg_dbname,
+                options=options or config.options,
+                datadir=str(datadir),
+                unixsocketdir=unixsocketdir or config.unixsocketdir,
+                logfile=str(logfile_path),
+                startparams=startparams or config.startparams,
+                postgres_options=postgres_options or config.postgres_options,
+            )
+            # start server
+            with postgresql_executor:
+                postgresql_executor.wait_for_postgres()
+                janitor = DatabaseJanitor(
+                    user=postgresql_executor.user,
+                    host=postgresql_executor.host,
+                    port=postgresql_executor.port,
+                    dbname=postgresql_executor.template_dbname,
+                    as_template=True,
+                    version=postgresql_executor.version,
+                    password=postgresql_executor.password,
+                )
+                if config.drop_test_database:
+                    janitor.drop()
+                with janitor:
+                    for load_element in pg_load:
+                        janitor.load(load_element)
+                    yield postgresql_executor
+        finally:
+            port_filename_path.unlink(missing_ok=True)

Also applies to: 133-141, 155-186

🧹 Nitpick comments (1)
tests/test_retry.py (1)

18-32: Avoid the real backoff in the failure-path tests.

These two tests add roughly three seconds of wall-clock delay and can become timing-sensitive on busy runners. Patching pytest_postgresql.retry.asyncio.sleep (and the clock helper for the timeout case) would keep the async retry coverage fast and deterministic.

Also applies to: 35-43

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_retry.py` around lines 18 - 32, The tests
test_retry_async_succeeds_after_failures (and the related timeout test) are
incurring real backoff delays; patch the sleep and clock used by retry_async to
make them deterministic and fast: in the tests monkeypatch
pytest_postgresql.retry.asyncio.sleep to an async no-op (or a function that
records calls) and also patch the retry module's clock/timer helper used for
timeout handling so the timeout case can advance without waiting; update the
tests to use these monkeypatched replacements while calling retry_async to
eliminate wall-clock backoff and keep behavior identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pytest_postgresql/factories/client.py`:
- Around line 102-119: The async fixture decorator call for
postgresql_async_factory must set loop_scope to match the fixture cache scope to
avoid event-loop lifetime misconfiguration; update the `@pytest_asyncio.fixture`
invocation (the decorator on postgresql_async_factory) to pass loop_scope=scope
so the event loop scope is at least as broad as the fixture scope (keeping the
existing scope=scope argument).

In `@tests/test_postgresql.py`:
- Around line 109-127: The async test test_postgres_terminate_connection_async
is missing the pytest-xdist isolation marker; add the same
`@pytest.mark.xdist_group`(name="terminate_connection") decorator to the async
test (alongside the existing `@pytest.mark.asyncio` and `@pytest.mark.parametrize`)
so it gets run in the same xdist group as the synchronous counterpart and avoids
cross-test interference.

---

Nitpick comments:
In `@tests/test_retry.py`:
- Around line 18-32: The tests test_retry_async_succeeds_after_failures (and the
related timeout test) are incurring real backoff delays; patch the sleep and
clock used by retry_async to make them deterministic and fast: in the tests
monkeypatch pytest_postgresql.retry.asyncio.sleep to an async no-op (or a
function that records calls) and also patch the retry module's clock/timer
helper used for timeout handling so the timeout case can advance without
waiting; update the tests to use these monkeypatched replacements while calling
retry_async to eliminate wall-clock backoff and keep behavior identical.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 821e8573-8a73-4dae-a6fc-dde60894e840

📥 Commits

Reviewing files that changed from the base of the PR and between 524e05a and 990496a.

📒 Files selected for processing (18)
  • newsfragments/1235.feature.rst
  • pyproject.toml
  • pytest_postgresql/factories/__init__.py
  • pytest_postgresql/factories/client.py
  • pytest_postgresql/factories/noprocess.py
  • pytest_postgresql/factories/process.py
  • pytest_postgresql/janitor.py
  • pytest_postgresql/loader.py
  • pytest_postgresql/retry.py
  • pytest_postgresql/types.py
  • tests/conftest.py
  • tests/docker/test_noproc_docker.py
  • tests/test_factory_errors.py
  • tests/test_janitor.py
  • tests/test_loader.py
  • tests/test_postgresql.py
  • tests/test_retry.py
  • tests/test_template_database.py

Comment thread pytest_postgresql/factories/client.py Outdated
Comment thread tests/test_postgresql.py
- Updated the postgresql_async fixture to include loop_scope for better async handling.
- Added xdist_group marker to the test_postgres_terminate_connection_async for improved test organization.
- Refactored retry tests to mock sleep and current time, ensuring accurate timeout handling and retry logic validation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_retry.py (1)

49-64: Optionally harden timeout assertions.

You may want to assert no sleep occurred and only one failing call happened in this scenario, to lock in current timeout behaviour more explicitly.

Optional tightening
-    async def always_fail() -> None:
-        raise ValueError("boom")
+    always_fail = AsyncMock(side_effect=ValueError("boom"))
@@
         with pytest.raises(TimeoutError, match="Failed after"):
             await retry_async(always_fail, timeout=1, possible_exception=ValueError)
+    sleep_mock.assert_not_awaited()
+    assert always_fail.await_count == 1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_retry.py` around lines 49 - 64, Add explicit assertions that no
sleep was awaited and that only the expected number of time-checks/calls
happened: after the with-block raising TimeoutError, call
sleep_mock.assert_not_awaited() to ensure no backoff was used and assert
call_count == 2 (verifying get_current_datetime was called exactly once to
capture the start and once to detect the timeout) to lock the current timeout
behavior for retry_async when always_fail raises immediately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/test_retry.py`:
- Around line 49-64: Add explicit assertions that no sleep was awaited and that
only the expected number of time-checks/calls happened: after the with-block
raising TimeoutError, call sleep_mock.assert_not_awaited() to ensure no backoff
was used and assert call_count == 2 (verifying get_current_datetime was called
exactly once to capture the start and once to detect the timeout) to lock the
current timeout behavior for retry_async when always_fail raises immediately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5bf34b69-b624-4e9d-8cb3-1fdaeffb1f94

📥 Commits

Reviewing files that changed from the base of the PR and between 990496a and 1384100.

📒 Files selected for processing (3)
  • pytest_postgresql/factories/client.py
  • tests/test_postgresql.py
  • tests/test_retry.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_postgresql.py

- Changed isolation level and autocommit settings to direct attribute assignment instead of using methods.
- Removed unnecessary mock methods in test for improved clarity and performance.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_janitor.py (1)

124-143: Consider adding test coverage for async loaders.

This test validates that AsyncDatabaseJanitor.load() works with synchronous loaders via the inspect.isawaitable check. Consider adding a companion test that exercises the async loader path (e.g., using a Path argument to trigger sql_async), ensuring the await result branch is also covered.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_janitor.py` around lines 124 - 143, Add a companion async test
that exercises the AsyncDatabaseJanitor.load() path where the loader is
asynchronous (so the inspect.isawaitable branch and the await result path are
executed): create an async loader (or point to an async loader via a Path to
trigger sql_async), patch tests.loader.psycopg.connect like in
test_janitor_populate_async, instantiate AsyncDatabaseJanitor(version=10,
**call_kwargs), await janitor.load(async_loader) and assert connect_mock.called
and connect_mock.call_args.kwargs == call_kwargs; ensure the test uses
`@pytest.mark.asyncio` and param or fixture to provide the async loader so
sql_async/inspect.isawaitable logic in AsyncDatabaseJanitor.load is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pytest_postgresql/janitor.py`:
- Around line 252-259: The SQL string concatenation in the async helper
_terminate_connection (async def _terminate_connection(cur: AsyncCursor, dbname:
str)) currently joins two literals without a space, producing "...pid)FROM..."
and causing a syntax error; fix it by ensuring there's whitespace between ")”
and "FROM" (e.g., include a trailing space on the first literal or a leading
space on the second) so the executed query becomes "...pid) FROM
pg_stat_activity ...", and apply the same whitespace fix to the synchronous
DatabaseJanitor._terminate_connection query to avoid the identical runtime
error.

---

Nitpick comments:
In `@tests/test_janitor.py`:
- Around line 124-143: Add a companion async test that exercises the
AsyncDatabaseJanitor.load() path where the loader is asynchronous (so the
inspect.isawaitable branch and the await result path are executed): create an
async loader (or point to an async loader via a Path to trigger sql_async),
patch tests.loader.psycopg.connect like in test_janitor_populate_async,
instantiate AsyncDatabaseJanitor(version=10, **call_kwargs), await
janitor.load(async_loader) and assert connect_mock.called and
connect_mock.call_args.kwargs == call_kwargs; ensure the test uses
`@pytest.mark.asyncio` and param or fixture to provide the async loader so
sql_async/inspect.isawaitable logic in AsyncDatabaseJanitor.load is covered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 951653a7-eb09-47eb-a5cc-adc4bbea59c1

📥 Commits

Reviewing files that changed from the base of the PR and between 1384100 and f16583a.

📒 Files selected for processing (2)
  • pytest_postgresql/janitor.py
  • tests/test_janitor.py

Comment thread pytest_postgresql/janitor.py
- Added a space at the end of the SQL query string in both DatabaseJanitor and AsyncDatabaseJanitor to ensure proper formatting.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pytest_postgresql/janitor.py`:
- Around line 224-231: The CREATE DATABASE DDL currently builds SQL with
f-strings using self.dbname and self.template_dbname (in the method that calls
cur.execute with query), which can break on embedded quotes; replace this string
interpolation with psycopg.sql.Identifier composition: import psycopg.sql as
sql, build the base sql.SQL("CREATE DATABASE
{}").format(sql.Identifier(self.dbname)), conditionally append sql.SQL("
TEMPLATE {}").format(sql.Identifier(self.template_dbname)) when present and
sql.SQL(" IS_TEMPLATE = true") when self.as_template is true, then pass the
composed sql.SQL object to await cur.execute(...) instead of the f-string.
Ensure all occurrences that construct database identifiers (the blocks using
self.dbname, self.template_dbname, and as_template) use sql.Identifier.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3dceab04-c112-4e1c-8a96-71c78d839bfd

📥 Commits

Reviewing files that changed from the base of the PR and between f16583a and 38a5ae4.

📒 Files selected for processing (1)
  • pytest_postgresql/janitor.py

Comment thread pytest_postgresql/janitor.py Outdated
tboy1337 and others added 4 commits March 14, 2026 11:21
- Updated PostgreSQLExecutor to conditionally omit Unix socket parameters on Windows.
- Enhanced error handling in PostgreSQLExecutor for Windows compatibility.
- Refactored AsyncDatabaseJanitor to use async methods for setting isolation level and autocommit.
- Updated tests to mock async connection methods for better compatibility.
Remove Windows executor compatibility changes (log_destination quoting,
unix_socket_directories platform guard, os.killpg AttributeError catch)
and the Windows-only event_loop_policy fixture from conftest.py.
These belong in the separate Windows compatibility PR dbfixtures#1182.

Made-with: Cursor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
pytest_postgresql/janitor.py (1)

223-250: ⚠️ Potential issue | 🟠 Major

Compose database identifiers with psycopg.sql.Identifier.

self.dbname, self.template_dbname, and dbname are still interpolated straight into DDL here. Any identifier containing " will generate invalid SQL, so the new async path inherits the same quoting bug as the sync janitor. Please build these statements with sql.Identifier(...) instead of f-strings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pytest_postgresql/janitor.py` around lines 223 - 250, Replace all f-string
interpolation of database identifiers with psycopg's composable SQL identifiers:
build queries using psycopg.sql.SQL(...) and psycopg.sql.Identifier(...) instead
of f-strings for the code paths that use self.dbname and self.template_dbname
(the block that constructs query and calls await cur.execute(f"{query};")), the
drop method (ALTER DATABASE ... with is_template false; DROP DATABASE IF EXISTS
...), and the helper _dont_datallowconn (which runs ALTER DATABASE ... with
allow_connections false;). Use sql.Identifier(dbname) /
sql.Identifier(self.template_dbname) to compose the identifier parts and
sql.SQL(...) to join fragments so the executed statements are safe even when
identifiers contain quotes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pytest_postgresql/plugin.py`:
- Line 138: The module-level call to
factories.postgresql_async("postgresql_proc") (assigned to postgresql_async)
triggers ImportError when pytest_asyncio is not installed; change this to defer
creation so the ImportError only occurs if the async fixture is actually
requested: replace the module-level invocation of factories.postgresql_async
with a lazy registration pattern — either wrap the call in a try/except
ImportError and register a stub fixture named postgresql_async that raises a
clear ImportError when used, or implement a small wrapper function/fixture that
calls factories.postgresql_async("postgresql_proc") on first access; update
references to the postgresql_async symbol accordingly so the optional [async]
extra remains optional.

---

Duplicate comments:
In `@pytest_postgresql/janitor.py`:
- Around line 223-250: Replace all f-string interpolation of database
identifiers with psycopg's composable SQL identifiers: build queries using
psycopg.sql.SQL(...) and psycopg.sql.Identifier(...) instead of f-strings for
the code paths that use self.dbname and self.template_dbname (the block that
constructs query and calls await cur.execute(f"{query};")), the drop method
(ALTER DATABASE ... with is_template false; DROP DATABASE IF EXISTS ...), and
the helper _dont_datallowconn (which runs ALTER DATABASE ... with
allow_connections false;). Use sql.Identifier(dbname) /
sql.Identifier(self.template_dbname) to compose the identifier parts and
sql.SQL(...) to join fragments so the executed statements are safe even when
identifiers contain quotes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d9ce58b1-1939-4f2d-abc2-0e5fabc5dc59

📥 Commits

Reviewing files that changed from the base of the PR and between 38a5ae4 and 2267c00.

📒 Files selected for processing (5)
  • pytest_postgresql/executor.py
  • pytest_postgresql/janitor.py
  • pytest_postgresql/plugin.py
  • tests/conftest.py
  • tests/test_janitor.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/conftest.py

Comment thread pytest_postgresql/plugin.py
tboy1337 and others added 5 commits March 14, 2026 11:53
…baseJanitor

- Updated SQL query construction to use psycopg.sql for better safety and readability.
- Refactored database creation, alteration, and dropping methods to utilize composable SQL objects.
- Improved test assertions by rendering SQL commands for clarity in test outputs.
- Updated the postgresql_async fixture to conditionally use pytest_asyncio if available, enhancing compatibility with synchronous fixtures.
- Added tests to ensure that the fixture does not raise errors at creation time when pytest_asyncio is absent, while still raising an ImportError during usage without pytest_asyncio.
@tboy1337

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Reviews resumed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_janitor.py (1)

173-175: Make the patched cursor helper signature explicit.

When patched onto AsyncDatabaseJanitor.cursor, the first bound argument is the instance. Declaring self explicitly in _ctx avoids brittle behaviour if future tests pass dbname.

♻️ Suggested tweak
-    async def _ctx(dbname: str = "postgres") -> AsyncIterator[AsyncMock]:
+    async def _ctx(self: AsyncDatabaseJanitor, dbname: str = "postgres") -> AsyncIterator[AsyncMock]:
         yield cur
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_janitor.py` around lines 173 - 175, The helper asynccontextmanager
_ctx used to patch AsyncDatabaseJanitor.cursor must declare the bound instance
explicitly; change the signature to include self as the first parameter (e.g.
async def _ctx(self, dbname: str = "postgres") -> AsyncIterator[AsyncMock]) so
the patched method receives the instance correctly when called on
AsyncDatabaseJanitor, then yield cur as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pytest_postgresql/factories/client.py`:
- Around line 116-127: The async fixture postgresql_async_factory currently gets
decorated even when pytest_asyncio is None, causing an async def to be passed to
pytest and leading to "coroutine was never awaited" errors; change the
implementation so that when pytest_asyncio is None you register a synchronous
fixture function (not async) that immediately raises ImportError with the same
message, and only define the async def postgresql_async_factory when
pytest_asyncio is present; update usage of fixture_decorator/pytest.fixture to
wrap the sync stub in that branch so the guard executes at fixture call time and
surfaces the clear ImportError instead of an unawaited coroutine.

---

Nitpick comments:
In `@tests/test_janitor.py`:
- Around line 173-175: The helper asynccontextmanager _ctx used to patch
AsyncDatabaseJanitor.cursor must declare the bound instance explicitly; change
the signature to include self as the first parameter (e.g. async def _ctx(self,
dbname: str = "postgres") -> AsyncIterator[AsyncMock]) so the patched method
receives the instance correctly when called on AsyncDatabaseJanitor, then yield
cur as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f6aae3db-164c-4f28-99f3-850e516c8115

📥 Commits

Reviewing files that changed from the base of the PR and between 38a5ae4 and bfbff75.

📒 Files selected for processing (5)
  • pytest_postgresql/factories/client.py
  • pytest_postgresql/janitor.py
  • pytest_postgresql/plugin.py
  • tests/test_factory_errors.py
  • tests/test_janitor.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_factory_errors.py

Comment thread pytest_postgresql/factories/client.py Outdated
…n pytest-asyncio is absent

- Updated the postgresql_async fixture to return a synchronous stub that raises ImportError if pytest-asyncio is not available, preventing coroutine-related warnings.
- Modified tests to verify that the synchronous stub is correctly registered and raises the appropriate error when used without pytest-asyncio.
tboy1337 and others added 4 commits June 21, 2026 20:19
Use a psycopg 3.0-compatible SQL render fallback in janitor tests and set WindowsSelectorEventLoopPolicy in the plugin so psycopg async works on Windows.

Co-authored-by: Cursor <cursoragent@cursor.com>
… tests.

Exercise init, drop, template flags, and TEMPLATE cloning against live PostgreSQL and remove _render_sql mock infrastructure.

Co-authored-by: Cursor <cursoragent@cursor.com>
Inline AsyncConnection.connect kwargs instead of dict unpacking so strict mypy accepts the connection parameters.

Co-authored-by: Cursor <cursoragent@cursor.com>
@tboy1337

Copy link
Copy Markdown
Contributor Author

@fizyk

tboy1337 added 2 commits June 21, 2026 21:46
…ject.toml; enhance documentation for async extra to reflect the new version requirement.
…ying minimum pytest-asyncio version 0.24. Update error messages to reflect new version requirement and clarify async fixture behavior.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_factory_errors.py`:
- Line 32: In the pytest.raises call with the match parameter on line 32, escape
the dot metacharacter in the version string pattern. The current pattern uses a
literal dot in "pytest-asyncio >= 0.24" which in regex will match any character.
Replace the unescaped dot before the version numbers with a backslash-escaped
dot to ensure the assertion matches only the intended literal dot character and
follows regex best practices.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 87a86d75-749f-4ec6-9089-51833cef50ac

📥 Commits

Reviewing files that changed from the base of the PR and between 8c866fc and 18f04d7.

📒 Files selected for processing (3)
  • README.rst
  • pytest_postgresql/factories/client.py
  • tests/test_factory_errors.py
✅ Files skipped from review due to trivial changes (1)
  • README.rst
🚧 Files skipped from review as they are similar to previous changes (1)
  • pytest_postgresql/factories/client.py

Comment thread tests/test_factory_errors.py Outdated
@tboy1337

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

tboy1337 added 3 commits June 21, 2026 21:59
…itionally based on asyncio plugin presence; refactor password handling in janitor tests for consistency.
Comment thread pytest_postgresql/factories/client.py Outdated
…he scope parameter and setting defaults directly in the fixture decorators. Update README to clarify async fixture behavior and dependencies. Remove unused types module.
@tboy1337 tboy1337 requested a review from fizyk June 22, 2026 03:46
…s and modify test cases to handle port initialization correctly by passing None instead of -1. This change ensures better compatibility with various configurations.
@tboy1337

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

…files and documentation. Enhance README to reflect changes in async fixture requirements and clarify Windows event loop handling for compatibility with psycopg async.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
README.rst (1)

54-56: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Clarify Windows/Python 3.14+ event loop handling.

The statement "On Python 3.14+, this uses pytest-asyncio's loop-factory hook instead of the deprecated asyncio policy API" could be clearer. Based on the plugin implementation, the loop-factory hook is invoked whenever pytest-asyncio >= 1.4 is installed (via pytest_asyncio_loop_factories), regardless of Python version. On Python 3.14+, the policy API fallback is skipped because Python's default event loop is sufficient. Consider rewording to clarify the distinction.

Suggested revision
-   On Windows, the plugin configures a ``SelectorEventLoop`` automatically (required
-   by ``psycopg`` async). On Python 3.14+, this uses pytest-asyncio's loop-factory
-   hook instead of the deprecated asyncio policy API.
+   On Windows, the plugin configures a ``SelectorEventLoop`` automatically (required
+   by ``psycopg`` async) via pytest-asyncio's loop-factory hook when available
+   (pytest-asyncio >= 1.4). On Python 3.14+, the deprecated asyncio policy API is
+   avoided in favour of Python's default event loop behaviour.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.rst` around lines 54 - 56, The documentation in README.rst lines 54-56
currently conflates two separate concerns about event loop handling. Clarify
that the loop-factory hook is used whenever pytest-asyncio >= 1.4 is installed
(regardless of Python version), and separately explain that on Python 3.14+, the
deprecated asyncio policy API fallback is skipped because Python's default event
loop is sufficient. Reword the statement to make this distinction explicit
rather than implying the hook is only used on Python 3.14+.
tests/test_plugin_asyncio.py (1)

16-25: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Exercise the Windows path explicitly in this 3.14 test.

This currently passes on non-Windows via the early return at platform check, so it does not strictly validate the intended Windows+3.14 behaviour.

🧪 Proposed test hardening
 `@pytest.mark.skipif`(sys.version_info < (3, 14), reason="Deprecation applies from Python 3.14")
 def test_pytest_configure_skips_deprecated_policy_on_python_314() -> None:
     """pytest_configure must not call deprecated asyncio policy APIs on Python 3.14+."""
     config = MagicMock()
     config.pluginmanager.has_plugin.return_value = True
 
-    with patch.object(asyncio, "set_event_loop_policy") as set_policy:
+    with patch.object(sys, "platform", "win32"), patch.object(asyncio, "set_event_loop_policy") as set_policy:
         pytest_configure(config)
 
     set_policy.assert_not_called()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_plugin_asyncio.py` around lines 16 - 25, The
test_pytest_configure_skips_deprecated_policy_on_python_314 function currently
passes on non-Windows systems due to an early platform check return in the
pytest_configure function, which means it doesn't actually validate the
Windows-specific behavior on Python 3.14+. Modify the test to explicitly mock
the platform check (such as sys.platform or the equivalent platform detection
method used in pytest_configure) to force the Windows code path to execute,
ensuring that the test properly validates the intended Windows+Python 3.14
behavior of not calling deprecated asyncio policy APIs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pytest_postgresql/plugin.py`:
- Around line 73-80: The pytest_asyncio_loop_factories hook function is
registered unconditionally, which causes pytest plugin validation to fail if
pytest-asyncio is unavailable or outdated. Mark this hook as optional by adding
the `@pytest.hookimpl`(optionalhook=True) decorator above the function definition
to allow pytest to gracefully skip this hook when the pytest-asyncio hookspec is
not available.

---

Nitpick comments:
In `@README.rst`:
- Around line 54-56: The documentation in README.rst lines 54-56 currently
conflates two separate concerns about event loop handling. Clarify that the
loop-factory hook is used whenever pytest-asyncio >= 1.4 is installed
(regardless of Python version), and separately explain that on Python 3.14+, the
deprecated asyncio policy API fallback is skipped because Python's default event
loop is sufficient. Reword the statement to make this distinction explicit
rather than implying the hook is only used on Python 3.14+.

In `@tests/test_plugin_asyncio.py`:
- Around line 16-25: The
test_pytest_configure_skips_deprecated_policy_on_python_314 function currently
passes on non-Windows systems due to an early platform check return in the
pytest_configure function, which means it doesn't actually validate the
Windows-specific behavior on Python 3.14+. Modify the test to explicitly mock
the platform check (such as sys.platform or the equivalent platform detection
method used in pytest_configure) to force the Windows code path to execute,
ensuring that the test properly validates the intended Windows+Python 3.14
behavior of not calling deprecated asyncio policy APIs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2b094f99-5570-4b6e-af65-1612033117c0

📥 Commits

Reviewing files that changed from the base of the PR and between c98d05b and c5270ff.

📒 Files selected for processing (9)
  • Pipfile
  • README.rst
  • newsfragments/1235.feature.rst
  • newsfragments/1295.bugfix.rst
  • pyproject.toml
  • pytest_postgresql/factories/client.py
  • pytest_postgresql/plugin.py
  • tests/test_factory_errors.py
  • tests/test_plugin_asyncio.py
✅ Files skipped from review due to trivial changes (2)
  • newsfragments/1295.bugfix.rst
  • newsfragments/1235.feature.rst
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/test_factory_errors.py
  • pytest_postgresql/factories/client.py

Comment thread pytest_postgresql/plugin.py Outdated
tboy1337 added 4 commits June 22, 2026 14:13
…ith pytest-asyncio and update test cases to ensure compatibility with the new loop-factory hook. Add optional hook implementation for pytest-asyncio loop factories.
…mprove compatibility with asyncio. Introduce helper functions to check platform and Python version, and update tests to reflect these changes.
…ntext to ensure it is only registered when on a Windows platform. Update tests to verify the correct behavior of the loop factory registration based on the operating system.
This update introduces a conditional import for pytest-asyncio, allowing the plugin to function without it. The version check for pytest-asyncio has been refactored to improve clarity and maintain compatibility with versions below 1.4. This change enhances the plugin's flexibility in various environments.
@tboy1337

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

tboy1337 and others added 2 commits June 22, 2026 16:21
The postgresql_oldest / postgres (3.10) job failed due to a hosted runner losing communication after 46m; all other matrix jobs passed.

Co-authored-by: Cursor <cursoragent@cursor.com>
… parsing

This change simplifies the regex used in `build_loader` and `build_loader_async` functions, allowing for deeper nested import paths without limiting the split. Additionally, new tests have been added to verify the correct behavior of these functions with deeply nested import paths.
@tboy1337 tboy1337 changed the title feat: add async PostgreSQL fixture support and configurable fixture scope feat: add async PostgreSQL fixture support Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants