Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fixed #17671 - Cursors are now context managers.

  • Loading branch information...
commit 99c87f1410106ce543a1a0332428afc472beef7f 1 parent 04a2a6b
@manfre manfre authored akaariai committed
View
5 django/db/backends/__init__.py
@@ -1,7 +1,7 @@
import datetime
import time
-from django.db.utils import DatabaseError
+from django.db.utils import DatabaseError, ProgrammingError
try:
from django.utils.six.moves import _thread as thread
@@ -664,6 +664,9 @@ class BaseDatabaseFeatures(object):
# Does the backend require a connection reset after each material schema change?
connection_persists_old_columns = False
+ # What kind of error does the backend throw when accessing closed cursor?
+ closed_cursor_error_class = ProgrammingError
+
def __init__(self, connection):
self.connection = connection
View
2  django/db/backends/postgresql_psycopg2/base.py
@@ -15,6 +15,7 @@
from django.db.backends.postgresql_psycopg2.version import get_version
from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection
from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor
+from django.db.utils import InterfaceError
from django.utils.encoding import force_str
from django.utils.functional import cached_property
from django.utils.safestring import SafeText, SafeBytes
@@ -60,6 +61,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
can_rollback_ddl = True
supports_combined_alters = True
nulls_order_largest = True
+ closed_cursor_error_class = InterfaceError
class DatabaseWrapper(BaseDatabaseWrapper):
View
8 django/db/backends/utils.py
@@ -36,6 +36,14 @@ def __getattr__(self, attr):
def __iter__(self):
return iter(self.cursor)
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ # Ticket #17671 - Close instead of passing thru to avoid backend
+ # specific behavior.
+ self.close()
+
class CursorDebugWrapper(CursorWrapper):
View
19 docs/releases/1.7.txt
@@ -111,6 +111,25 @@ In addition, the widgets now display a help message when the browser and
server time zone are different, to clarify how the value inserted in the field
will be interpreted.
+Using database cursors as context managers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Prior to Python 2.7, database cursors could be used as a context manager. The
+specific backend's cursor defined the behavior of the context manager. The
+behavior of magic method lookups was changed with Python 2.7 and cursors were
+no longer usable as context managers.
+
+Django 1.7 allows a cursor to be used as a context manager that is a shortcut
+for the following, instead of backend specific behavior.
+
+.. code-block:: python
+
+ c = connection.cursor()
+ try:
+ c.execute(...)
+ finally:
+ c.close()
+
Minor features
~~~~~~~~~~~~~~
View
27 docs/topics/db/sql.txt
@@ -297,3 +297,30 @@ database library will automatically escape your parameters as necessary.
Also note that Django expects the ``"%s"`` placeholder, *not* the ``"?"``
placeholder, which is used by the SQLite Python bindings. This is for the sake
of consistency and sanity.
+
+.. versionchanged:: 1.7
+
+:pep:`249` does not state whether a cursor should be usable as a context
+manager. Prior to Python 2.7, a cursor was usable as a context manager due
+an unexpected behavior in magic method lookups (`Python ticket #9220`_).
+Django 1.7 explicitly added support to allow using a cursor as context
+manager.
+
+.. _`Python ticket #9220`: http://bugs.python.org/issue9220
+
+Using a cursor as a context manager:
+
+.. code-block:: python
+
+ with connection.cursor() as c:
+ c.execute(...)
+
+is equivalent to:
+
+.. code-block:: python
+
+ c = connection.cursor()
+ try:
+ c.execute(...)
+ finally:
+ c.close()
View
25 tests/backends/tests.py
@@ -613,6 +613,31 @@ def test_duplicate_table_error(self):
with self.assertRaises(DatabaseError):
cursor.execute(query)
+ def test_cursor_contextmanager(self):
+ """
+ Test that cursors can be used as a context manager
+ """
+ with connection.cursor() as cursor:
+ from django.db.backends.util import CursorWrapper
+ self.assertTrue(isinstance(cursor, CursorWrapper))
+ # Both InterfaceError and ProgrammingError seem to be used when
+ # accessing closed cursor (psycopg2 has InterfaceError, rest seem
+ # to use ProgrammingError).
+ with self.assertRaises(connection.features.closed_cursor_error_class):
+ # cursor should be closed, so no queries should be possible.
+ cursor.execute("select 1")
+
+ @unittest.skipUnless(connection.vendor == 'postgresql',
+ "Psycopg2 specific cursor.closed attribute needed")
+ def test_cursor_contextmanager_closing(self):
+ # There isn't a generic way to test that cursors are closed, but
+ # psycopg2 offers us a way to check that by closed attribute.
+ # So, run only on psycopg2 for that reason.
+ with connection.cursor() as cursor:
+ from django.db.backends.util import CursorWrapper
+ self.assertTrue(isinstance(cursor, CursorWrapper))
+ self.assertTrue(cursor.closed)
+
# We don't make these tests conditional because that means we would need to
# check and differentiate between:
Please sign in to comment.
Something went wrong with that request. Please try again.