diff --git a/CHANGELOG.md b/CHANGELOG.md index c87dc97a4bed599ebce4bfe12eaf100989ca17f8..bc4504eb51ab3d3efbb332aeef293c3e4c69b8f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,4 @@ -# v2.19.0 +# v2.19.2 + +* added `db_call` -* unit tests migrated to CircleCI -* added __len__ to FutureCollection -* fixed a bug in DictionaryEQAble -* fixed a bug in ListDeleter -* minor breaking change: changed semantics of ListDeleter -* added `CPManager` -* added `SetZip` \ No newline at end of file diff --git a/docs/db.rst b/docs/db.rst new file mode 100644 index 0000000000000000000000000000000000000000..2df2fcb7d14341cc277f7551a1ae6ce15f5de95a --- /dev/null +++ b/docs/db.rst @@ -0,0 +1,9 @@ +Python DB API 2 +=============== + +However imperfect may it be, it's here to stay. + +So enjoy! + +.. autoclass:: satella.db.transaction + :members: diff --git a/docs/index.rst b/docs/index.rst index 601fa559a1d69a3c9a875b155c5519cbd32b7d1a..d8d58cad0768e7b0c0bf1fcfc8ea849e0e788c85 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,6 +28,7 @@ Visit the project's page at GitHub_! instrumentation/metrics exception_handling dao + db json posix import diff --git a/satella/__init__.py b/satella/__init__.py index 6d2db50c097c278d885331e65be7b51a2178f18b..a910817da22d06aa0244c6d488b40d30da2bfb7e 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.19.0' +__version__ = '2.20.0' diff --git a/satella/db.py b/satella/db.py new file mode 100644 index 0000000000000000000000000000000000000000..1d3ad8ef00636dd3c6c13806ce14aec41620f9f5 --- /dev/null +++ b/satella/db.py @@ -0,0 +1,48 @@ +import logging + +logger = logging.getLogger(__name__) + + +class transaction: + """ + A context manager for wrapping a transaction and getting a cursor from the Python DB API 2. + + Use it as a context manager. commit and rollback will be automatically called for you. + + Use like: + + >>> with transaction(conn) as cur: + >>> cur.execute('DROP DATABASE') + + Leaving the context manager will automatically close the cursor for you. + + :param connection: the connection object to use + :param close_the_connection_after: whether the connection should be closed after use, False by default + :param log_exception: whether to log an exception if it happens + """ + def __init__(self, connection, close_the_connection_after: bool = False, + log_exception: bool = True): + self.connection = connection + self.close_the_connection_after = close_the_connection_after + self.cursor = None + self.log_exception = log_exception + + def __enter__(self): + self.cursor = self.connection.cursor() + return self.cursor() + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_val is None: + self.connection.commit() + else: + self.connection.rollback() + + if self.log_exception: + logger.error('Exception occurred of type %s', exc_type, exc_info=exc_val) + + self.cursor.close() + + if self.close_the_connection_after: + self.connection.close() + + return False diff --git a/tests/test_db.py b/tests/test_db.py new file mode 100644 index 0000000000000000000000000000000000000000..c5d86b029f9c6aa32922bbe0dce40fd66305dda5 --- /dev/null +++ b/tests/test_db.py @@ -0,0 +1,40 @@ +import unittest +from unittest.mock import Mock + +from satella.db import transaction + + +class TestDB(unittest.TestCase): + def test_something(self): + class RealConnection: + def __init__(self): + self.cursor_called = 0 + self.commit_called = 0 + self.rollback_called = 0 + self.close_called = 0 + + def cursor(self): + self.cursor_called += 1 + return Mock() + + def commit(self): + self.commit_called += 1 + + def rollback(self): + self.rollback_called += 1 + + def close(self): + self.close_called += 1 + + conn = RealConnection() + a = transaction(conn) + with a as cur: + self.assertEqual(conn.cursor_called, 1) + cur.execute('TEST') + self.assertEqual(conn.commit_called, 1) + try: + with a as cur: + raise ValueError() + except ValueError: + self.assertEqual(conn.commit_called, 1) + self.assertEqual(conn.rollback_called, 1)