From 1dcd0dba4719c7c0e4b8c774375f444199b6a621 Mon Sep 17 00:00:00 2001 From: nilbacardit26 Date: Fri, 30 Jan 2026 12:39:28 +0100 Subject: [PATCH 1/5] add async context to acquire advisory locks --- CHANGELOG.rst | 2 + guillotina/db/locks.py | 54 +++++++++++++++++++++++ guillotina/exceptions.py | 7 +++ guillotina/tests/test_postgres.py | 73 ++++++++++++++++++++++++++----- 4 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 guillotina/db/locks.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c6d9d1ced..82c0ae79a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,8 @@ CHANGELOG 7.0.7 (unreleased) ------------------ +- Add async context manager to acquire advisory write locks for objects + [nilbacardit26] - Docs: Update documentation and configuration settings - Chore: Update sphinx-guillotina-theme version to 1.0.9 [rboixaderg] diff --git a/guillotina/db/locks.py b/guillotina/db/locks.py new file mode 100644 index 000000000..495a42db6 --- /dev/null +++ b/guillotina/db/locks.py @@ -0,0 +1,54 @@ +from contextlib import asynccontextmanager +from guillotina import task_vars +from guillotina.exceptions import ObjectLockedError +from guillotina.exceptions import ReadOnlyError +from guillotina.exceptions import TransactionNotFound + +import asyncio +import asyncpg +import hashlib + + +def _oid_lock_key(oid: str) -> int: + digest = hashlib.blake2b(oid.encode("utf-8"), digest_size=8).digest() + return int.from_bytes(digest, "big", signed=True) + + +@asynccontextmanager +async def lock_object_for_write(oid: str, *, retries: int = 3, delay: float = 0.05): + """ + Acquire a transaction-scoped advisory lock for a Guillotina object. + Must be used inside an active transaction. + """ + txn = task_vars.txn.get() + if txn is None: + raise TransactionNotFound() + if txn.read_only: + raise ReadOnlyError() + + storage = txn.storage + if txn._db_txn is None: + await storage.start_transaction(txn) + + if retries < 1: + retries = 1 + + key = _oid_lock_key(oid) + async with storage.acquire(txn, "object_lock") as conn: + for attempt in range(1, retries + 1): + try: + locked = await conn.fetchval("SELECT pg_try_advisory_xact_lock($1);", key) + except asyncpg.exceptions.UndefinedFunctionError as ex: + raise NotImplementedError("Object locks require PostgreSQL advisory locks") from ex + if locked: + break + if attempt < retries and delay > 0: + await asyncio.sleep(delay) + else: + raise ObjectLockedError(oid, retries) + + try: + yield + finally: + # xact lock is released on commit/rollback + pass diff --git a/guillotina/exceptions.py b/guillotina/exceptions.py index 26c5a4628..1afc14ee2 100644 --- a/guillotina/exceptions.py +++ b/guillotina/exceptions.py @@ -145,6 +145,13 @@ class TIDConflictError(ConflictError): pass +class ObjectLockedError(Exception): + def __init__(self, oid, retries): + super().__init__(f"Object {oid} is locked for modification after {retries} retries") + self.oid = oid + self.retries = retries + + class RestartCommit(Exception): """ Commits requires restart diff --git a/guillotina/tests/test_postgres.py b/guillotina/tests/test_postgres.py index 0bb9f0fe8..49882aa05 100644 --- a/guillotina/tests/test_postgres.py +++ b/guillotina/tests/test_postgres.py @@ -2,11 +2,13 @@ from guillotina.component import get_adapter from guillotina.content import Folder from guillotina.db.interfaces import IVacuumProvider +from guillotina.db.locks import lock_object_for_write from guillotina.db.storages.cockroach import CockroachStorage from guillotina.db.storages.pg import PostgresqlStorage from guillotina.db.transaction_manager import TransactionManager from guillotina.exceptions import ConflictError from guillotina.exceptions import ConflictIdOnContainer +from guillotina.exceptions import ObjectLockedError from guillotina.tests import mocks from guillotina.tests.utils import create_content from unittest.mock import Mock @@ -44,11 +46,15 @@ async def cleanup(aps): async def get_aps(postgres, pool_size=16, autovacuum=True): - dsn = "postgres://postgres:postgres@{}:{}/guillotina".format(postgres[0], postgres[1]) + dsn = "postgres://postgres:postgres@{}:{}/guillotina".format( + postgres[0], postgres[1] + ) klass = PostgresqlStorage if DATABASE == "cockroachdb": klass = CockroachStorage - dsn = "postgres://root:@{}:{}/guillotina?sslmode=disable".format(postgres[0], postgres[1]) + dsn = "postgres://root:@{}:{}/guillotina?sslmode=disable".format( + postgres[0], postgres[1] + ) aps = klass( dsn=dsn, name="db", @@ -135,6 +141,37 @@ async def test_restart_connection_pg(db, dummy_guillotina): await cleanup(aps) +@pytest.mark.skipif(DATABASE == "DUMMY", reason="Not for dummy db") +async def test_object_lock_for_write(db, dummy_guillotina): + aps = await get_aps(db) + tm = TransactionManager(aps) + + async with tm: + txn1 = await tm.begin() + ob = create_content() + txn1.register(ob) + await tm.commit(txn=txn1) + + txn1 = await tm.begin() + async with lock_object_for_write(ob.__uuid__, retries=1, delay=0): + txn2 = await tm.begin() + try: + with pytest.raises(ObjectLockedError): + async with lock_object_for_write(ob.__uuid__, retries=2, delay=0): + pass + finally: + await tm.abort(txn=txn2) + await tm.abort(txn=txn1) + + txn3 = await tm.begin() + async with lock_object_for_write(ob.__uuid__, retries=1, delay=0): + pass + await tm.abort(txn=txn3) + + await aps.remove() + await cleanup(aps) + + @pytest.mark.skipif( DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not have cascade support", @@ -234,8 +271,12 @@ async def test_delete_resource_deletes_blob(db, dummy_guillotina): await cleanup(aps) -@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve...") -async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_strat(db, dummy_guillotina): +@pytest.mark.skipif( + DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve..." +) +async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_strat( + db, dummy_guillotina +): aps = await get_aps(db) with TransactionManager(aps) as tm, await tm.begin() as txn: ob = create_content() @@ -266,7 +307,9 @@ async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_s await cleanup(aps) -@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve...") +@pytest.mark.skipif( + DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve..." +) async def test_should_resolve_conflict_error(db, dummy_guillotina): aps = await get_aps(db) with TransactionManager(aps) as tm, await tm.begin() as txn: @@ -297,7 +340,9 @@ async def test_should_resolve_conflict_error(db, dummy_guillotina): @pytest.mark.skipif(DATABASE in ("DUMMY",), reason="DUMMY not support simple...") -async def test_should_raise_conflict_error_on_concurrent_insert(db, container_requester): +async def test_should_raise_conflict_error_on_concurrent_insert( + db, container_requester +): async with container_requester as requester: with requester.db.get_transaction_manager() as tm: txn = await tm.begin() @@ -324,7 +369,9 @@ async def test_should_raise_conflict_error_on_concurrent_insert(db, container_re @pytest.mark.skipif(DATABASE in ("DUMMY",), reason="DUMMY not support simple...") -async def test_should_raise_conflict_error_on_concurrent_update(db, container_requester): +async def test_should_raise_conflict_error_on_concurrent_update( + db, container_requester +): async with container_requester as requester: with requester.db.get_transaction_manager() as tm: txn = await tm.begin() @@ -528,7 +575,9 @@ async def test_iterate_keys(db, dummy_guillotina): await tm.abort(txn=txn) -@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test...") +@pytest.mark.skipif( + DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test..." +) async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): aps = await get_aps(db) tm = TransactionManager(aps) @@ -536,7 +585,9 @@ async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): for conn in tm._storage.pool._queue._queue: if conn._con is None: await conn.connect() - conn._con._top_xact = asyncpg.transaction.Transaction(conn._con, "read_committed", False, False) + conn._con._top_xact = asyncpg.transaction.Transaction( + conn._con, "read_committed", False, False + ) with await tm.begin() as txn, tm: # then, try doing stuff... @@ -558,7 +609,9 @@ async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): await cleanup(aps) -@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test...") +@pytest.mark.skipif( + DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test..." +) async def test_handles_asyncpg_trying_txn_with_manual_txn(db, dummy_guillotina): aps = await get_aps(db) tm = TransactionManager(aps) From e17d343b5454e3afc51433df72614129cdf95178 Mon Sep 17 00:00:00 2001 From: nilbacardit26 Date: Fri, 30 Jan 2026 13:01:35 +0100 Subject: [PATCH 2/5] black --- guillotina/tests/test_postgres.py | 40 ++++++++----------------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/guillotina/tests/test_postgres.py b/guillotina/tests/test_postgres.py index 49882aa05..e07548b6f 100644 --- a/guillotina/tests/test_postgres.py +++ b/guillotina/tests/test_postgres.py @@ -46,15 +46,11 @@ async def cleanup(aps): async def get_aps(postgres, pool_size=16, autovacuum=True): - dsn = "postgres://postgres:postgres@{}:{}/guillotina".format( - postgres[0], postgres[1] - ) + dsn = "postgres://postgres:postgres@{}:{}/guillotina".format(postgres[0], postgres[1]) klass = PostgresqlStorage if DATABASE == "cockroachdb": klass = CockroachStorage - dsn = "postgres://root:@{}:{}/guillotina?sslmode=disable".format( - postgres[0], postgres[1] - ) + dsn = "postgres://root:@{}:{}/guillotina?sslmode=disable".format(postgres[0], postgres[1]) aps = klass( dsn=dsn, name="db", @@ -271,12 +267,8 @@ async def test_delete_resource_deletes_blob(db, dummy_guillotina): await cleanup(aps) -@pytest.mark.skipif( - DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve..." -) -async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_strat( - db, dummy_guillotina -): +@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve...") +async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_strat(db, dummy_guillotina): aps = await get_aps(db) with TransactionManager(aps) as tm, await tm.begin() as txn: ob = create_content() @@ -307,9 +299,7 @@ async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_s await cleanup(aps) -@pytest.mark.skipif( - DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve..." -) +@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve...") async def test_should_resolve_conflict_error(db, dummy_guillotina): aps = await get_aps(db) with TransactionManager(aps) as tm, await tm.begin() as txn: @@ -340,9 +330,7 @@ async def test_should_resolve_conflict_error(db, dummy_guillotina): @pytest.mark.skipif(DATABASE in ("DUMMY",), reason="DUMMY not support simple...") -async def test_should_raise_conflict_error_on_concurrent_insert( - db, container_requester -): +async def test_should_raise_conflict_error_on_concurrent_insert(db, container_requester): async with container_requester as requester: with requester.db.get_transaction_manager() as tm: txn = await tm.begin() @@ -369,9 +357,7 @@ async def test_should_raise_conflict_error_on_concurrent_insert( @pytest.mark.skipif(DATABASE in ("DUMMY",), reason="DUMMY not support simple...") -async def test_should_raise_conflict_error_on_concurrent_update( - db, container_requester -): +async def test_should_raise_conflict_error_on_concurrent_update(db, container_requester): async with container_requester as requester: with requester.db.get_transaction_manager() as tm: txn = await tm.begin() @@ -575,9 +561,7 @@ async def test_iterate_keys(db, dummy_guillotina): await tm.abort(txn=txn) -@pytest.mark.skipif( - DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test..." -) +@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test...") async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): aps = await get_aps(db) tm = TransactionManager(aps) @@ -585,9 +569,7 @@ async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): for conn in tm._storage.pool._queue._queue: if conn._con is None: await conn.connect() - conn._con._top_xact = asyncpg.transaction.Transaction( - conn._con, "read_committed", False, False - ) + conn._con._top_xact = asyncpg.transaction.Transaction(conn._con, "read_committed", False, False) with await tm.begin() as txn, tm: # then, try doing stuff... @@ -609,9 +591,7 @@ async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): await cleanup(aps) -@pytest.mark.skipif( - DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test..." -) +@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test...") async def test_handles_asyncpg_trying_txn_with_manual_txn(db, dummy_guillotina): aps = await get_aps(db) tm = TransactionManager(aps) From 2b05eb566c3aeeee05f62553270da638534bdc31 Mon Sep 17 00:00:00 2001 From: nilbacardit26 Date: Fri, 30 Jan 2026 13:10:49 +0100 Subject: [PATCH 3/5] mypy --- guillotina/db/locks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guillotina/db/locks.py b/guillotina/db/locks.py index 495a42db6..ee6f67883 100644 --- a/guillotina/db/locks.py +++ b/guillotina/db/locks.py @@ -23,11 +23,11 @@ async def lock_object_for_write(oid: str, *, retries: int = 3, delay: float = 0. txn = task_vars.txn.get() if txn is None: raise TransactionNotFound() - if txn.read_only: + if getattr(txn, "read_only", False): raise ReadOnlyError() storage = txn.storage - if txn._db_txn is None: + if getattr(txn, "_db_txn", None) is None: await storage.start_transaction(txn) if retries < 1: From 5c3e6d799a052726086b23af483fabbc2ea8f717 Mon Sep 17 00:00:00 2001 From: nilbacardit26 Date: Fri, 30 Jan 2026 13:51:42 +0100 Subject: [PATCH 4/5] tests postgres --- guillotina/tests/test_postgres.py | 42 +++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/guillotina/tests/test_postgres.py b/guillotina/tests/test_postgres.py index e07548b6f..a8ab44e6a 100644 --- a/guillotina/tests/test_postgres.py +++ b/guillotina/tests/test_postgres.py @@ -46,11 +46,15 @@ async def cleanup(aps): async def get_aps(postgres, pool_size=16, autovacuum=True): - dsn = "postgres://postgres:postgres@{}:{}/guillotina".format(postgres[0], postgres[1]) + dsn = "postgres://postgres:postgres@{}:{}/guillotina".format( + postgres[0], postgres[1] + ) klass = PostgresqlStorage if DATABASE == "cockroachdb": klass = CockroachStorage - dsn = "postgres://root:@{}:{}/guillotina?sslmode=disable".format(postgres[0], postgres[1]) + dsn = "postgres://root:@{}:{}/guillotina?sslmode=disable".format( + postgres[0], postgres[1] + ) aps = klass( dsn=dsn, name="db", @@ -137,7 +141,7 @@ async def test_restart_connection_pg(db, dummy_guillotina): await cleanup(aps) -@pytest.mark.skipif(DATABASE == "DUMMY", reason="Not for dummy db") +@pytest.mark.skipif(DATABASE != "postgres", reason="Requires postgres advisory locks") async def test_object_lock_for_write(db, dummy_guillotina): aps = await get_aps(db) tm = TransactionManager(aps) @@ -267,8 +271,12 @@ async def test_delete_resource_deletes_blob(db, dummy_guillotina): await cleanup(aps) -@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve...") -async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_strat(db, dummy_guillotina): +@pytest.mark.skipif( + DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve..." +) +async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_strat( + db, dummy_guillotina +): aps = await get_aps(db) with TransactionManager(aps) as tm, await tm.begin() as txn: ob = create_content() @@ -299,7 +307,9 @@ async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_s await cleanup(aps) -@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve...") +@pytest.mark.skipif( + DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve..." +) async def test_should_resolve_conflict_error(db, dummy_guillotina): aps = await get_aps(db) with TransactionManager(aps) as tm, await tm.begin() as txn: @@ -330,7 +340,9 @@ async def test_should_resolve_conflict_error(db, dummy_guillotina): @pytest.mark.skipif(DATABASE in ("DUMMY",), reason="DUMMY not support simple...") -async def test_should_raise_conflict_error_on_concurrent_insert(db, container_requester): +async def test_should_raise_conflict_error_on_concurrent_insert( + db, container_requester +): async with container_requester as requester: with requester.db.get_transaction_manager() as tm: txn = await tm.begin() @@ -357,7 +369,9 @@ async def test_should_raise_conflict_error_on_concurrent_insert(db, container_re @pytest.mark.skipif(DATABASE in ("DUMMY",), reason="DUMMY not support simple...") -async def test_should_raise_conflict_error_on_concurrent_update(db, container_requester): +async def test_should_raise_conflict_error_on_concurrent_update( + db, container_requester +): async with container_requester as requester: with requester.db.get_transaction_manager() as tm: txn = await tm.begin() @@ -561,7 +575,9 @@ async def test_iterate_keys(db, dummy_guillotina): await tm.abort(txn=txn) -@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test...") +@pytest.mark.skipif( + DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test..." +) async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): aps = await get_aps(db) tm = TransactionManager(aps) @@ -569,7 +585,9 @@ async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): for conn in tm._storage.pool._queue._queue: if conn._con is None: await conn.connect() - conn._con._top_xact = asyncpg.transaction.Transaction(conn._con, "read_committed", False, False) + conn._con._top_xact = asyncpg.transaction.Transaction( + conn._con, "read_committed", False, False + ) with await tm.begin() as txn, tm: # then, try doing stuff... @@ -591,7 +609,9 @@ async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): await cleanup(aps) -@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test...") +@pytest.mark.skipif( + DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test..." +) async def test_handles_asyncpg_trying_txn_with_manual_txn(db, dummy_guillotina): aps = await get_aps(db) tm = TransactionManager(aps) From dc751bfef685347f6420ad96d7f4306600e42049 Mon Sep 17 00:00:00 2001 From: nilbacardit26 Date: Fri, 30 Jan 2026 13:52:20 +0100 Subject: [PATCH 5/5] black --- guillotina/tests/test_postgres.py | 40 ++++++++----------------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/guillotina/tests/test_postgres.py b/guillotina/tests/test_postgres.py index a8ab44e6a..eff427b81 100644 --- a/guillotina/tests/test_postgres.py +++ b/guillotina/tests/test_postgres.py @@ -46,15 +46,11 @@ async def cleanup(aps): async def get_aps(postgres, pool_size=16, autovacuum=True): - dsn = "postgres://postgres:postgres@{}:{}/guillotina".format( - postgres[0], postgres[1] - ) + dsn = "postgres://postgres:postgres@{}:{}/guillotina".format(postgres[0], postgres[1]) klass = PostgresqlStorage if DATABASE == "cockroachdb": klass = CockroachStorage - dsn = "postgres://root:@{}:{}/guillotina?sslmode=disable".format( - postgres[0], postgres[1] - ) + dsn = "postgres://root:@{}:{}/guillotina?sslmode=disable".format(postgres[0], postgres[1]) aps = klass( dsn=dsn, name="db", @@ -271,12 +267,8 @@ async def test_delete_resource_deletes_blob(db, dummy_guillotina): await cleanup(aps) -@pytest.mark.skipif( - DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve..." -) -async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_strat( - db, dummy_guillotina -): +@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve...") +async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_strat(db, dummy_guillotina): aps = await get_aps(db) with TransactionManager(aps) as tm, await tm.begin() as txn: ob = create_content() @@ -307,9 +299,7 @@ async def test_should_raise_conflict_error_when_editing_diff_data_with_resolve_s await cleanup(aps) -@pytest.mark.skipif( - DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve..." -) +@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach not support resolve...") async def test_should_resolve_conflict_error(db, dummy_guillotina): aps = await get_aps(db) with TransactionManager(aps) as tm, await tm.begin() as txn: @@ -340,9 +330,7 @@ async def test_should_resolve_conflict_error(db, dummy_guillotina): @pytest.mark.skipif(DATABASE in ("DUMMY",), reason="DUMMY not support simple...") -async def test_should_raise_conflict_error_on_concurrent_insert( - db, container_requester -): +async def test_should_raise_conflict_error_on_concurrent_insert(db, container_requester): async with container_requester as requester: with requester.db.get_transaction_manager() as tm: txn = await tm.begin() @@ -369,9 +357,7 @@ async def test_should_raise_conflict_error_on_concurrent_insert( @pytest.mark.skipif(DATABASE in ("DUMMY",), reason="DUMMY not support simple...") -async def test_should_raise_conflict_error_on_concurrent_update( - db, container_requester -): +async def test_should_raise_conflict_error_on_concurrent_update(db, container_requester): async with container_requester as requester: with requester.db.get_transaction_manager() as tm: txn = await tm.begin() @@ -575,9 +561,7 @@ async def test_iterate_keys(db, dummy_guillotina): await tm.abort(txn=txn) -@pytest.mark.skipif( - DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test..." -) +@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test...") async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): aps = await get_aps(db) tm = TransactionManager(aps) @@ -585,9 +569,7 @@ async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): for conn in tm._storage.pool._queue._queue: if conn._con is None: await conn.connect() - conn._con._top_xact = asyncpg.transaction.Transaction( - conn._con, "read_committed", False, False - ) + conn._con._top_xact = asyncpg.transaction.Transaction(conn._con, "read_committed", False, False) with await tm.begin() as txn, tm: # then, try doing stuff... @@ -609,9 +591,7 @@ async def test_handles_asyncpg_trying_savepoints(db, dummy_guillotina): await cleanup(aps) -@pytest.mark.skipif( - DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test..." -) +@pytest.mark.skipif(DATABASE in ("cockroachdb", "DUMMY"), reason="Cockroach does not like this test...") async def test_handles_asyncpg_trying_txn_with_manual_txn(db, dummy_guillotina): aps = await get_aps(db) tm = TransactionManager(aps)