Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added context manager support to Cursor object.

Cursor objects can now be used in "with" statements and will automatically commit the
transaction if successful.

This is part of the effort to allow programs to be written using only Cursors.  There are times
when having separate connections and cursors are necessary, but very rare.  By duplicating
connection functionality in the cursor, most programs can probably use a single Cursor object
everywhere.
  • Loading branch information...
commit 738cd01d053d49330a2dada01af6bbab17b52bb1 1 parent 66c5136
@mkleehammer authored
Showing with 104 additions and 2 deletions.
  1. +10 −2 src/connection.cpp
  2. +36 −0 src/cursor.cpp
  3. +58 −0 tests2/sqlservertests.py
View
12 src/connection.cpp
@@ -902,7 +902,7 @@ static PyObject* Connection_enter(PyObject* self, PyObject* args)
return self;
}
-static char exit_doc[] = "__exit__(*excinfo) -> None. Closes the connection.";
+static char exit_doc[] = "__exit__(*excinfo) -> None. Commits the connection if necessary.";
static PyObject* Connection_exit(PyObject* self, PyObject* args)
{
Connection* cnxn = (Connection*)self;
@@ -911,8 +911,16 @@ static PyObject* Connection_exit(PyObject* self, PyObject* args)
I(PyTuple_Check(args));
if (cnxn->nAutoCommit == SQL_AUTOCOMMIT_OFF && PyTuple_GetItem(args, 0) == Py_None)
- SQLEndTran(SQL_HANDLE_DBC, cnxn->hdbc, SQL_COMMIT);
+ {
+ SQLRETURN ret;
+ Py_BEGIN_ALLOW_THREADS
+ ret = SQLEndTran(SQL_HANDLE_DBC, cnxn->hdbc, SQL_COMMIT);
+ Py_END_ALLOW_THREADS
+ if (!SQL_SUCCEEDED(ret))
+ return RaiseErrorFromHandle("SQLEndTran(SQL_COMMIT)", cnxn->hdbc, SQL_NULL_HANDLE);
+ }
+
Py_RETURN_NONE;
}
View
36 src/cursor.cpp
@@ -2054,6 +2054,40 @@ static char fetchmany_doc[] =
"A ProgrammingError exception is raised if the previous call to execute() did\n" \
"not produce any result set or no call was issued yet.";
+
+static char enter_doc[] = "__enter__() -> self.";
+static PyObject* Cursor_enter(PyObject* self, PyObject* args)
+{
+ UNUSED(args);
+ Py_INCREF(self);
+ return self;
+}
+
+static char exit_doc[] = "__exit__(*excinfo) -> None. Commits the connection if necessary..";
+static PyObject* Cursor_exit(PyObject* self, PyObject* args)
+{
+ Cursor* cursor = Cursor_Validate(self, CURSOR_REQUIRE_OPEN | CURSOR_RAISE_ERROR);
+ if (!cursor)
+ return 0;
+
+ // If an error has occurred, `args` will be a tuple of 3 values. Otherwise it will be a tuple of 3 `None`s.
+ I(PyTuple_Check(args));
+
+ if (cursor->cnxn->nAutoCommit == SQL_AUTOCOMMIT_OFF && PyTuple_GetItem(args, 0) == Py_None)
+ {
+ SQLRETURN ret;
+ Py_BEGIN_ALLOW_THREADS
+ ret = SQLEndTran(SQL_HANDLE_DBC, cursor->cnxn->hdbc, SQL_COMMIT);
+ Py_END_ALLOW_THREADS
+
+ if (!SQL_SUCCEEDED(ret))
+ return RaiseErrorFromHandle("SQLEndTran(SQL_COMMIT)", cursor->cnxn->hdbc, cursor->hstmt);
+ }
+
+ Py_RETURN_NONE;
+}
+
+
static PyMethodDef Cursor_methods[] =
{
{ "close", (PyCFunction)Cursor_close, METH_NOARGS, close_doc },
@@ -2078,6 +2112,8 @@ static PyMethodDef Cursor_methods[] =
{ "skip", (PyCFunction)Cursor_skip, METH_VARARGS, skip_doc },
{ "commit", (PyCFunction)Cursor_commit, METH_NOARGS, commit_doc },
{ "rollback", (PyCFunction)Cursor_rollback, METH_NOARGS, rollback_doc },
+ { "__enter__", Cursor_enter, METH_NOARGS, enter_doc },
+ { "__exit__", Cursor_exit, METH_VARARGS, exit_doc },
{ 0, 0, 0, 0 }
};
View
58 tests2/sqlservertests.py
@@ -1272,7 +1272,28 @@ def test_row_gtlt(self):
rows.sort() # uses <
def test_context_manager_success(self):
+ """
+ Ensure a successful with statement causes a commit.
+ """
+ self.cursor.execute("create table t1(n int)")
+ self.cnxn.commit()
+
+ with pyodbc.connect(self.connection_string) as cnxn:
+ cursor = cnxn.cursor()
+ cursor.execute("insert into t1 values (1)")
+ cnxn = None
+ cursor = None
+
+ rows = self.cursor.execute("select n from t1").fetchall()
+ self.assertEquals(len(rows), 1)
+ self.assertEquals(rows[0][0], 1)
+
+
+ def test_context_manager_fail(self):
+ """
+ Ensure an exception in a with statement causes a rollback.
+ """
self.cursor.execute("create table t1(n int)")
self.cnxn.commit()
@@ -1280,17 +1301,54 @@ def test_context_manager_success(self):
with pyodbc.connect(self.connection_string) as cnxn:
cursor = cnxn.cursor()
cursor.execute("insert into t1 values (1)")
+ raise Exception("Testing failure")
except Exception:
pass
cnxn = None
cursor = None
+ count = self.cursor.execute("select count(*) from t1").fetchone()[0]
+ self.assertEquals(count, 0)
+
+
+ def test_cursor_context_manager_success(self):
+ """
+ Ensure a successful with statement using a cursor causes a commit.
+ """
+ self.cursor.execute("create table t1(n int)")
+ self.cnxn.commit()
+
+ with pyodbc.connect(self.connection_string).cursor() as cursor:
+ cursor.execute("insert into t1 values (1)")
+
+ cursor = None
+
rows = self.cursor.execute("select n from t1").fetchall()
self.assertEquals(len(rows), 1)
self.assertEquals(rows[0][0], 1)
+ def test_cursor_context_manager_fail(self):
+ """
+ Ensure an exception in a with statement using a cursor causes a rollback.
+ """
+ self.cursor.execute("create table t1(n int)")
+ self.cnxn.commit()
+
+ try:
+ with pyodbc.connect(self.connection_string).cursor() as cursor:
+ cursor.execute("insert into t1 values (1)")
+ raise Exception("Testing failure")
+ except Exception:
+ pass
+
+ cursor = None
+
+ count = self.cursor.execute("select count(*) from t1").fetchone()[0]
+ self.assertEquals(count, 0)
+
+
def test_untyped_none(self):
# From issue 129
value = self.cursor.execute("select ?", None).fetchone()[0]
Please sign in to comment.
Something went wrong with that request. Please try again.