Fixed #17671 - Cursors are now context managers.
This commit is contained in:
parent
04a2a6b0f9
commit
99c87f1410
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue