Browse Source

Fixed #17671 - Cursors are now context managers.

Michael Manfre 11 years ago
parent
commit
99c87f1410

+ 4 - 1
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
 

+ 2 - 0
django/db/backends/postgresql_psycopg2/base.py

@@ -15,6 +15,7 @@ from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation
 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):

+ 8 - 0
django/db/backends/utils.py

@@ -36,6 +36,14 @@ class CursorWrapper(object):
     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):
 

+ 19 - 0
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
 ~~~~~~~~~~~~~~
 

+ 27 - 0
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()

+ 25 - 0
tests/backends/tests.py

@@ -613,6 +613,31 @@ class BackendTestCase(TestCase):
         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: