Fixed #17671 - Cursors are now context managers.

This commit is contained in:
Michael Manfre 2013-09-23 20:17:59 -04:00 committed by Anssi Kääriäinen
parent 04a2a6b0f9
commit 99c87f1410
6 changed files with 85 additions and 1 deletions

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

View File

@ -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()

View File

@ -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: