浏览代码

Fixed #17671 - Cursors are now context managers.

Michael Manfre 11 年之前
父节点
当前提交
99c87f1410

+ 4 - 1
django/db/backends/__init__.py

@@ -1,7 +1,7 @@
 import datetime
 import datetime
 import time
 import time
 
 
-from django.db.utils import DatabaseError
+from django.db.utils import DatabaseError, ProgrammingError
 
 
 try:
 try:
     from django.utils.six.moves import _thread as thread
     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?
     # Does the backend require a connection reset after each material schema change?
     connection_persists_old_columns = False
     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):
     def __init__(self, connection):
         self.connection = 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.version import get_version
 from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection
 from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection
 from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor
 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.encoding import force_str
 from django.utils.functional import cached_property
 from django.utils.functional import cached_property
 from django.utils.safestring import SafeText, SafeBytes
 from django.utils.safestring import SafeText, SafeBytes
@@ -60,6 +61,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
     can_rollback_ddl = True
     can_rollback_ddl = True
     supports_combined_alters = True
     supports_combined_alters = True
     nulls_order_largest = True
     nulls_order_largest = True
+    closed_cursor_error_class = InterfaceError
 
 
 
 
 class DatabaseWrapper(BaseDatabaseWrapper):
 class DatabaseWrapper(BaseDatabaseWrapper):

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

@@ -36,6 +36,14 @@ class CursorWrapper(object):
     def __iter__(self):
     def __iter__(self):
         return iter(self.cursor)
         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):
 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
 server time zone are different, to clarify how the value inserted in the field
 will be interpreted.
 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
 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 ``"?"``
 Also note that Django expects the ``"%s"`` placeholder, *not* the ``"?"``
 placeholder, which is used by the SQLite Python bindings. This is for the sake
 placeholder, which is used by the SQLite Python bindings. This is for the sake
 of consistency and sanity.
 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):
         with self.assertRaises(DatabaseError):
             cursor.execute(query)
             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
 # We don't make these tests conditional because that means we would need to
 # check and differentiate between:
 # check and differentiate between: