Merge branch 'contextmanager' into devel

This commit is contained in:
Daniele Varrazzo 2012-12-04 00:38:01 +00:00
commit e6fbf47c46
8 changed files with 367 additions and 17 deletions

2
NEWS
View File

@ -3,6 +3,8 @@ What's new in psycopg 2.5
- Added JSON adaptation. - Added JSON adaptation.
- Added support for PostgreSQL 9.2 range types. - Added support for PostgreSQL 9.2 range types.
- 'connection' and 'cursor' objects can be used in 'with' statements
as context managers as specified by recent DBAPI extension.
- Added support for backward scrollable cursors. Thanks to Jon Nelson - Added support for backward scrollable cursors. Thanks to Jon Nelson
for the initial patch (ticket #108). for the initial patch (ticket #108).
- Added a simple way to customize casting of composite types into Python - Added a simple way to customize casting of composite types into Python

View File

@ -74,6 +74,10 @@ The ``connection`` class
automatically open, commands have immediate effect. See automatically open, commands have immediate effect. See
:ref:`transactions-control` for details. :ref:`transactions-control` for details.
.. versionchanged:: 2.5 if the connection is used in a ``with``
statement, the method is automatically called if no exception is
raised in the ``with`` block.
.. index:: .. index::
pair: Transaction; Rollback pair: Transaction; Rollback
@ -84,6 +88,10 @@ The ``connection`` class
connection without committing the changes first will cause an implicit connection without committing the changes first will cause an implicit
rollback to be performed. rollback to be performed.
.. versionchanged:: 2.5 if the connection is used in a ``with``
statement, the method is automatically called if an exception is
raised in the ``with`` block.
.. method:: close() .. method:: close()

View File

@ -83,6 +83,11 @@ The ``cursor`` class
The cursor will be unusable from this point forward; an The cursor will be unusable from this point forward; an
`~psycopg2.InterfaceError` will be raised if any operation is `~psycopg2.InterfaceError` will be raised if any operation is
attempted with the cursor. attempted with the cursor.
.. versionchanged:: 2.5 if the cursor is used in a ``with`` statement,
the method is automatically called at the end of the ``with``
block.
.. attribute:: closed .. attribute:: closed

View File

@ -548,6 +548,30 @@ change the isolation level. See the `~connection.set_session()` method for all
the details. the details.
.. index::
single: with statement
``with`` statement
^^^^^^^^^^^^^^^^^^
Starting from version 2.5, psycopg2's connections and cursors are *context
managers* and can be used with the ``with`` statement::
with psycopg2.connect(DSN) as conn:
with conn.cursor() as curs:
curs.execute(SQL)
When a connection exits the ``with`` block, if no exception has been raised by
the block, the transaction is committed. In case of exception the transaction
is rolled back. In no case the connection is closed: a connection can be used
in more than a ``with`` statement and each ``with`` block is effectively
wrapped in a transaction.
When a cursor exits the ``with`` block it is closed, releasing any resource
eventually associated with it. The state of the transaction is not affected.
.. index:: .. index::
pair: Server side; Cursor pair: Server side; Cursor
pair: Named; Cursor pair: Named; Cursor

View File

@ -124,7 +124,7 @@ exit:
#define psyco_conn_close_doc "close() -- Close the connection." #define psyco_conn_close_doc "close() -- Close the connection."
static PyObject * static PyObject *
psyco_conn_close(connectionObject *self, PyObject *args) psyco_conn_close(connectionObject *self)
{ {
Dprintf("psyco_conn_close: closing connection at %p", self); Dprintf("psyco_conn_close: closing connection at %p", self);
conn_close(self); conn_close(self);
@ -140,7 +140,7 @@ psyco_conn_close(connectionObject *self, PyObject *args)
#define psyco_conn_commit_doc "commit() -- Commit all changes to database." #define psyco_conn_commit_doc "commit() -- Commit all changes to database."
static PyObject * static PyObject *
psyco_conn_commit(connectionObject *self, PyObject *args) psyco_conn_commit(connectionObject *self)
{ {
EXC_IF_CONN_CLOSED(self); EXC_IF_CONN_CLOSED(self);
EXC_IF_CONN_ASYNC(self, commit); EXC_IF_CONN_ASYNC(self, commit);
@ -160,7 +160,7 @@ psyco_conn_commit(connectionObject *self, PyObject *args)
"rollback() -- Roll back all changes done to database." "rollback() -- Roll back all changes done to database."
static PyObject * static PyObject *
psyco_conn_rollback(connectionObject *self, PyObject *args) psyco_conn_rollback(connectionObject *self)
{ {
EXC_IF_CONN_CLOSED(self); EXC_IF_CONN_CLOSED(self);
EXC_IF_CONN_ASYNC(self, rollback); EXC_IF_CONN_ASYNC(self, rollback);
@ -234,7 +234,7 @@ exit:
"tpc_prepare() -- perform the first phase of a two-phase transaction." "tpc_prepare() -- perform the first phase of a two-phase transaction."
static PyObject * static PyObject *
psyco_conn_tpc_prepare(connectionObject *self, PyObject *args) psyco_conn_tpc_prepare(connectionObject *self)
{ {
EXC_IF_CONN_CLOSED(self); EXC_IF_CONN_CLOSED(self);
EXC_IF_CONN_ASYNC(self, tpc_prepare); EXC_IF_CONN_ASYNC(self, tpc_prepare);
@ -378,7 +378,7 @@ psyco_conn_tpc_rollback(connectionObject *self, PyObject *args)
"tpc_recover() -- returns a list of pending transaction IDs." "tpc_recover() -- returns a list of pending transaction IDs."
static PyObject * static PyObject *
psyco_conn_tpc_recover(connectionObject *self, PyObject *args) psyco_conn_tpc_recover(connectionObject *self)
{ {
EXC_IF_CONN_CLOSED(self); EXC_IF_CONN_CLOSED(self);
EXC_IF_CONN_ASYNC(self, tpc_recover); EXC_IF_CONN_ASYNC(self, tpc_recover);
@ -389,6 +389,54 @@ psyco_conn_tpc_recover(connectionObject *self, PyObject *args)
} }
#define psyco_conn_enter_doc \
"__enter__ -> self"
static PyObject *
psyco_conn_enter(connectionObject *self)
{
EXC_IF_CONN_CLOSED(self);
Py_INCREF(self);
return (PyObject *)self;
}
#define psyco_conn_exit_doc \
"__exit__ -- commit if no exception, else roll back"
static PyObject *
psyco_conn_exit(connectionObject *self, PyObject *args)
{
PyObject *type, *name, *tb;
PyObject *tmp = NULL;
PyObject *rv = NULL;
if (!PyArg_ParseTuple(args, "OOO", &type, &name, &tb)) {
goto exit;
}
if (type == Py_None) {
if (!(tmp = PyObject_CallMethod((PyObject *)self, "commit", ""))) {
goto exit;
}
} else {
if (!(tmp = PyObject_CallMethod((PyObject *)self, "rollback", ""))) {
goto exit;
}
}
/* success (of the commit or rollback, there may have been an exception in
* the block). Return None to avoid swallowing the exception */
rv = Py_None;
Py_INCREF(rv);
exit:
Py_XDECREF(tmp);
return rv;
}
#ifdef PSYCOPG_EXTENSIONS #ifdef PSYCOPG_EXTENSIONS
@ -652,7 +700,7 @@ psyco_conn_set_client_encoding(connectionObject *self, PyObject *args)
"get_transaction_status() -- Get backend transaction status." "get_transaction_status() -- Get backend transaction status."
static PyObject * static PyObject *
psyco_conn_get_transaction_status(connectionObject *self, PyObject *args) psyco_conn_get_transaction_status(connectionObject *self)
{ {
EXC_IF_CONN_CLOSED(self); EXC_IF_CONN_CLOSED(self);
@ -756,7 +804,7 @@ psyco_conn_lobject(connectionObject *self, PyObject *args, PyObject *keywds)
"get_backend_pid() -- Get backend process id." "get_backend_pid() -- Get backend process id."
static PyObject * static PyObject *
psyco_conn_get_backend_pid(connectionObject *self, PyObject *args) psyco_conn_get_backend_pid(connectionObject *self)
{ {
EXC_IF_CONN_CLOSED(self); EXC_IF_CONN_CLOSED(self);
@ -769,7 +817,7 @@ psyco_conn_get_backend_pid(connectionObject *self, PyObject *args)
"reset() -- Reset current connection to defaults." "reset() -- Reset current connection to defaults."
static PyObject * static PyObject *
psyco_conn_reset(connectionObject *self, PyObject *args) psyco_conn_reset(connectionObject *self)
{ {
int res; int res;
@ -797,7 +845,7 @@ psyco_conn_get_exception(PyObject *self, void *closure)
} }
static PyObject * static PyObject *
psyco_conn_poll(connectionObject *self, PyObject *args) psyco_conn_poll(connectionObject *self)
{ {
int res; int res;
@ -819,7 +867,7 @@ psyco_conn_poll(connectionObject *self, PyObject *args)
"fileno() -> int -- Return file descriptor associated to database connection." "fileno() -> int -- Return file descriptor associated to database connection."
static PyObject * static PyObject *
psyco_conn_fileno(connectionObject *self, PyObject *args) psyco_conn_fileno(connectionObject *self)
{ {
long int socket; long int socket;
@ -838,7 +886,7 @@ psyco_conn_fileno(connectionObject *self, PyObject *args)
"executing an asynchronous operation." "executing an asynchronous operation."
static PyObject * static PyObject *
psyco_conn_isexecuting(connectionObject *self, PyObject *args) psyco_conn_isexecuting(connectionObject *self)
{ {
/* synchronous connections will always return False */ /* synchronous connections will always return False */
if (self->async == 0) { if (self->async == 0) {
@ -870,7 +918,7 @@ psyco_conn_isexecuting(connectionObject *self, PyObject *args)
"cancel() -- cancel the current operation" "cancel() -- cancel the current operation"
static PyObject * static PyObject *
psyco_conn_cancel(connectionObject *self, PyObject *args) psyco_conn_cancel(connectionObject *self)
{ {
char errbuf[256]; char errbuf[256];
@ -924,6 +972,10 @@ static struct PyMethodDef connectionObject_methods[] = {
METH_VARARGS, psyco_conn_tpc_rollback_doc}, METH_VARARGS, psyco_conn_tpc_rollback_doc},
{"tpc_recover", (PyCFunction)psyco_conn_tpc_recover, {"tpc_recover", (PyCFunction)psyco_conn_tpc_recover,
METH_NOARGS, psyco_conn_tpc_recover_doc}, METH_NOARGS, psyco_conn_tpc_recover_doc},
{"__enter__", (PyCFunction)psyco_conn_enter,
METH_NOARGS, psyco_conn_enter_doc},
{"__exit__", (PyCFunction)psyco_conn_exit,
METH_VARARGS, psyco_conn_exit_doc},
#ifdef PSYCOPG_EXTENSIONS #ifdef PSYCOPG_EXTENSIONS
{"set_session", (PyCFunction)psyco_conn_set_session, {"set_session", (PyCFunction)psyco_conn_set_session,
METH_VARARGS|METH_KEYWORDS, psyco_conn_set_session_doc}, METH_VARARGS|METH_KEYWORDS, psyco_conn_set_session_doc},

View File

@ -50,7 +50,7 @@ extern PyObject *pyPsycopgTzFixedOffsetTimezone;
"close() -- Close the cursor." "close() -- Close the cursor."
static PyObject * static PyObject *
psyco_curs_close(cursorObject *self, PyObject *args) psyco_curs_close(cursorObject *self)
{ {
EXC_IF_ASYNC_IN_PROGRESS(self, close); EXC_IF_ASYNC_IN_PROGRESS(self, close);
@ -761,7 +761,7 @@ exit:
} }
static PyObject * static PyObject *
psyco_curs_fetchone(cursorObject *self, PyObject *args) psyco_curs_fetchone(cursorObject *self)
{ {
PyObject *res; PyObject *res;
@ -953,7 +953,7 @@ exit:
"Return `!None` when no more data is available.\n" "Return `!None` when no more data is available.\n"
static PyObject * static PyObject *
psyco_curs_fetchall(cursorObject *self, PyObject *args) psyco_curs_fetchall(cursorObject *self)
{ {
int i, size; int i, size;
PyObject *list = NULL; PyObject *list = NULL;
@ -1085,7 +1085,7 @@ exit:
"sets) and will raise a NotSupportedError exception." "sets) and will raise a NotSupportedError exception."
static PyObject * static PyObject *
psyco_curs_nextset(cursorObject *self, PyObject *args) psyco_curs_nextset(cursorObject *self)
{ {
EXC_IF_CURS_CLOSED(self); EXC_IF_CURS_CLOSED(self);
@ -1201,6 +1201,42 @@ psyco_curs_scroll(cursorObject *self, PyObject *args, PyObject *kwargs)
} }
#define psyco_curs_enter_doc \
"__enter__ -> self"
static PyObject *
psyco_curs_enter(cursorObject *self)
{
Py_INCREF(self);
return (PyObject *)self;
}
#define psyco_curs_exit_doc \
"__exit__ -- close the cursor"
static PyObject *
psyco_curs_exit(cursorObject *self, PyObject *args)
{
PyObject *tmp = NULL;
PyObject *rv = NULL;
/* don't care about the arguments here: don't need to parse them */
if (!(tmp = PyObject_CallMethod((PyObject *)self, "close", ""))) {
goto exit;
}
/* success (of curs.close()).
* Return None to avoid swallowing the exception */
rv = Py_None;
Py_INCREF(rv);
exit:
Py_XDECREF(tmp);
return rv;
}
#ifdef PSYCOPG_EXTENSIONS #ifdef PSYCOPG_EXTENSIONS
/* Return a newly allocated buffer containing the list of columns to be /* Return a newly allocated buffer containing the list of columns to be
@ -1674,7 +1710,7 @@ cursor_next(PyObject *self)
if (NULL == ((cursorObject*)self)->name) { if (NULL == ((cursorObject*)self)->name) {
/* we don't parse arguments: psyco_curs_fetchone will do that for us */ /* we don't parse arguments: psyco_curs_fetchone will do that for us */
res = psyco_curs_fetchone((cursorObject*)self, NULL); res = psyco_curs_fetchone((cursorObject*)self);
/* convert a None to NULL to signal the end of iteration */ /* convert a None to NULL to signal the end of iteration */
if (res && res == Py_None) { if (res && res == Py_None) {
@ -1716,6 +1752,10 @@ static struct PyMethodDef cursorObject_methods[] = {
/* DBAPI-2.0 extensions */ /* DBAPI-2.0 extensions */
{"scroll", (PyCFunction)psyco_curs_scroll, {"scroll", (PyCFunction)psyco_curs_scroll,
METH_VARARGS|METH_KEYWORDS, psyco_curs_scroll_doc}, METH_VARARGS|METH_KEYWORDS, psyco_curs_scroll_doc},
{"__enter__", (PyCFunction)psyco_curs_enter,
METH_NOARGS, psyco_curs_enter_doc},
{"__exit__", (PyCFunction)psyco_curs_exit,
METH_VARARGS, psyco_curs_exit_doc},
/* psycopg extensions */ /* psycopg extensions */
#ifdef PSYCOPG_EXTENSIONS #ifdef PSYCOPG_EXTENSIONS
{"cast", (PyCFunction)psyco_curs_cast, {"cast", (PyCFunction)psyco_curs_cast,

View File

@ -45,6 +45,11 @@ import test_transaction
import test_types_basic import test_types_basic
import test_types_extras import test_types_extras
if sys.version_info[:2] >= (2, 5):
import test_with
else:
test_with = None
def test_suite(): def test_suite():
# If connection to test db fails, bail out early. # If connection to test db fails, bail out early.
import psycopg2 import psycopg2
@ -76,6 +81,8 @@ def test_suite():
suite.addTest(test_transaction.test_suite()) suite.addTest(test_transaction.test_suite())
suite.addTest(test_types_basic.test_suite()) suite.addTest(test_types_basic.test_suite())
suite.addTest(test_types_extras.test_suite()) suite.addTest(test_types_extras.test_suite())
if test_with:
suite.addTest(test_with.test_suite())
return suite return suite
if __name__ == '__main__': if __name__ == '__main__':

212
tests/test_with.py Executable file
View File

@ -0,0 +1,212 @@
#!/usr/bin/env python
# test_ctxman.py - unit test for connection and cursor used as context manager
#
# Copyright (C) 2012 Daniele Varrazzo <daniele.varrazzo@gmail.com>
#
# psycopg2 is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# In addition, as a special exception, the copyright holders give
# permission to link this program with the OpenSSL library (or with
# modified versions of OpenSSL that use the same license as OpenSSL),
# and distribute linked combinations including the two.
#
# You must obey the GNU Lesser General Public License in all respects for
# all of the code used other than OpenSSL.
#
# psycopg2 is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
# License for more details.
from __future__ import with_statement
import psycopg2
import psycopg2.extensions as ext
from testconfig import dsn
from testutils import unittest
class TestMixin(object):
def setUp(self):
self.conn = conn = psycopg2.connect(dsn)
curs = conn.cursor()
try:
curs.execute("delete from test_with")
conn.commit()
except psycopg2.ProgrammingError:
# assume table doesn't exist
conn.rollback()
curs.execute("create table test_with (id integer primary key)")
conn.commit()
def tearDown(self):
self.conn.close()
class WithConnectionTestCase(TestMixin, unittest.TestCase):
def test_with_ok(self):
with self.conn as conn:
self.assert_(self.conn is conn)
self.assertEqual(conn.status, ext.STATUS_READY)
curs = conn.cursor()
curs.execute("insert into test_with values (1)")
self.assertEqual(conn.status, ext.STATUS_BEGIN)
self.assertEqual(self.conn.status, ext.STATUS_READY)
self.assert_(not self.conn.closed)
curs = self.conn.cursor()
curs.execute("select * from test_with")
self.assertEqual(curs.fetchall(), [(1,)])
def test_with_connect_idiom(self):
with psycopg2.connect(dsn) as conn:
self.assertEqual(conn.status, ext.STATUS_READY)
curs = conn.cursor()
curs.execute("insert into test_with values (2)")
self.assertEqual(conn.status, ext.STATUS_BEGIN)
self.assertEqual(self.conn.status, ext.STATUS_READY)
self.assert_(not self.conn.closed)
curs = self.conn.cursor()
curs.execute("select * from test_with")
self.assertEqual(curs.fetchall(), [(2,)])
def test_with_error_db(self):
def f():
with self.conn as conn:
curs = conn.cursor()
curs.execute("insert into test_with values ('a')")
self.assertRaises(psycopg2.DataError, f)
self.assertEqual(self.conn.status, ext.STATUS_READY)
self.assert_(not self.conn.closed)
curs = self.conn.cursor()
curs.execute("select * from test_with")
self.assertEqual(curs.fetchall(), [])
def test_with_error_python(self):
def f():
with self.conn as conn:
curs = conn.cursor()
curs.execute("insert into test_with values (3)")
1/0
self.assertRaises(ZeroDivisionError, f)
self.assertEqual(self.conn.status, ext.STATUS_READY)
self.assert_(not self.conn.closed)
curs = self.conn.cursor()
curs.execute("select * from test_with")
self.assertEqual(curs.fetchall(), [])
def test_with_closed(self):
def f():
with self.conn:
pass
self.conn.close()
self.assertRaises(psycopg2.InterfaceError, f)
def test_subclass_commit(self):
commits = []
class MyConn(ext.connection):
def commit(self):
commits.append(None)
super(MyConn, self).commit()
with psycopg2.connect(dsn, connection_factory=MyConn) as conn:
curs = conn.cursor()
curs.execute("insert into test_with values (10)")
self.assertEqual(conn.status, ext.STATUS_READY)
self.assert_(commits)
curs = self.conn.cursor()
curs.execute("select * from test_with")
self.assertEqual(curs.fetchall(), [(10,)])
def test_subclass_rollback(self):
rollbacks = []
class MyConn(ext.connection):
def rollback(self):
rollbacks.append(None)
super(MyConn, self).rollback()
try:
with psycopg2.connect(dsn, connection_factory=MyConn) as conn:
curs = conn.cursor()
curs.execute("insert into test_with values (11)")
1/0
except ZeroDivisionError:
pass
else:
self.assert_("exception not raised")
self.assertEqual(conn.status, ext.STATUS_READY)
self.assert_(rollbacks)
curs = conn.cursor()
curs.execute("select * from test_with")
self.assertEqual(curs.fetchall(), [])
class WithCursorTestCase(TestMixin, unittest.TestCase):
def test_with_ok(self):
with self.conn as conn:
with conn.cursor() as curs:
curs.execute("insert into test_with values (4)")
self.assert_(not curs.closed)
self.assertEqual(self.conn.status, ext.STATUS_BEGIN)
self.assert_(curs.closed)
self.assertEqual(self.conn.status, ext.STATUS_READY)
self.assert_(not self.conn.closed)
curs = self.conn.cursor()
curs.execute("select * from test_with")
self.assertEqual(curs.fetchall(), [(4,)])
def test_with_error(self):
try:
with self.conn as conn:
with conn.cursor() as curs:
curs.execute("insert into test_with values (5)")
1/0
except ZeroDivisionError:
pass
self.assertEqual(self.conn.status, ext.STATUS_READY)
self.assert_(not self.conn.closed)
self.assert_(curs.closed)
curs = self.conn.cursor()
curs.execute("select * from test_with")
self.assertEqual(curs.fetchall(), [])
def test_subclass(self):
closes = []
class MyCurs(ext.cursor):
def close(self):
closes.append(None)
super(MyCurs, self).close()
with self.conn.cursor(cursor_factory=MyCurs) as curs:
self.assert_(isinstance(curs, MyCurs))
self.assert_(curs.closed)
self.assert_(closes)
def test_suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == "__main__":
unittest.main()