Removed global timezone-aware datetime converters.

Refs #23820.
This commit is contained in:
Aymeric Augustin 2015-04-12 14:46:24 +02:00
parent eda12ceef1
commit ec186572e6
10 changed files with 85 additions and 45 deletions

View File

@ -74,6 +74,8 @@ class DatabaseCache(BaseDatabaseCache):
# All core backends work without typecasting, so be careful about
# changes here - test suite will NOT pick regressions here.
expires = typecast_timestamp(str(expires))
if settings.USE_TZ:
expires = expires.replace(tzinfo=timezone.utc)
if expires < now:
db = router.db_for_write(self.cache_model_class)
with connections[db].cursor() as cursor:
@ -132,6 +134,8 @@ class DatabaseCache(BaseDatabaseCache):
if (connections[db].features.needs_datetime_string_cast and not
isinstance(current_expires, datetime)):
current_expires = typecast_timestamp(str(current_expires))
if settings.USE_TZ:
current_expires = current_expires.replace(tzinfo=timezone.utc)
exp = connections[db].ops.value_to_db_datetime(exp)
if result and (mode == 'set' or (mode == 'add' and current_expires < now)):
cursor.execute("UPDATE %s SET value = %%s, expires = %%s "

View File

@ -51,17 +51,6 @@ if (version < (1, 2, 1) or (version[:3] == (1, 2, 1) and
DatabaseError = Database.DatabaseError
IntegrityError = Database.IntegrityError
# It's impossible to import datetime_or_None directly from MySQLdb.times
parse_datetime = conversions[FIELD_TYPE.DATETIME]
def parse_datetime_with_timezone_support(value):
dt = parse_datetime(value)
# Confirm that dt is naive before overwriting its tzinfo.
if dt is not None and settings.USE_TZ and timezone.is_naive(dt):
dt = dt.replace(tzinfo=timezone.utc)
return dt
def adapt_datetime_with_timezone_support(value, conv):
# Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL.
@ -80,14 +69,11 @@ def adapt_datetime_with_timezone_support(value, conv):
# and Django expects time, so we still need to override that. We also need to
# add special handling for SafeText and SafeBytes as MySQLdb's type
# checking is too tight to catch those (see Django ticket #6052).
# Finally, MySQLdb always returns naive datetime objects. However, when
# timezone support is active, Django expects timezone-aware datetime objects.
django_conversions = conversions.copy()
django_conversions.update({
FIELD_TYPE.TIME: backend_utils.typecast_time,
FIELD_TYPE.DECIMAL: backend_utils.typecast_decimal,
FIELD_TYPE.NEWDECIMAL: backend_utils.typecast_decimal,
FIELD_TYPE.DATETIME: parse_datetime_with_timezone_support,
datetime.datetime: adapt_datetime_with_timezone_support,
})

View File

@ -184,6 +184,8 @@ class DatabaseOperations(BaseDatabaseOperations):
internal_type = expression.output_field.get_internal_type()
if internal_type in ['BooleanField', 'NullBooleanField']:
converters.append(self.convert_booleanfield_value)
if internal_type == 'DateTimeField':
converters.append(self.convert_datetimefield_value)
if internal_type == 'UUIDField':
converters.append(self.convert_uuidfield_value)
if internal_type == 'TextField':
@ -195,6 +197,12 @@ class DatabaseOperations(BaseDatabaseOperations):
value = bool(value)
return value
def convert_datetimefield_value(self, value, expression, connection, context):
if value is not None:
if settings.USE_TZ:
value = value.replace(tzinfo=timezone.utc)
return value
def convert_uuidfield_value(self, value, expression, connection, context):
if value is not None:
value = uuid.UUID(value)

View File

@ -588,12 +588,6 @@ def _rowfactory(row, cursor):
value = decimal.Decimal(value)
else:
value = int(value)
# datetimes are returned as TIMESTAMP, except the results
# of "dates" queries, which are returned as DATETIME.
elif desc[1] in (Database.TIMESTAMP, Database.DATETIME):
# Confirm that dt is naive before overwriting its tzinfo.
if settings.USE_TZ and value is not None and timezone.is_naive(value):
value = value.replace(tzinfo=timezone.utc)
elif desc[1] in (Database.STRING, Database.FIXED_CHAR,
Database.LONG_STRING):
value = to_unicode(value)

View File

@ -163,6 +163,8 @@ WHEN (new.%(col_name)s IS NULL)
converters.append(self.convert_binaryfield_value)
elif internal_type in ['BooleanField', 'NullBooleanField']:
converters.append(self.convert_booleanfield_value)
elif internal_type == 'DateTimeField':
converters.append(self.convert_datetimefield_value)
elif internal_type == 'DateField':
converters.append(self.convert_datefield_value)
elif internal_type == 'TimeField':
@ -202,6 +204,13 @@ WHEN (new.%(col_name)s IS NULL)
# cx_Oracle always returns datetime.datetime objects for
# DATE and TIMESTAMP columns, but Django wants to see a
# python datetime.date, .time, or .datetime.
def convert_datetimefield_value(self, value, expression, connection, context):
if value is not None:
if settings.USE_TZ:
value = value.replace(tzinfo=timezone.utc)
return value
def convert_datefield_value(self, value, expression, connection, context):
if isinstance(value, Database.Timestamp):
return value.date()

View File

@ -17,7 +17,9 @@ from django.db.backends import utils as backend_utils
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.backends.base.validation import BaseDatabaseValidation
from django.utils import six, timezone
from django.utils.dateparse import parse_date, parse_duration, parse_time
from django.utils.dateparse import (
parse_date, parse_datetime, parse_duration, parse_time,
)
from django.utils.encoding import force_text
from django.utils.safestring import SafeBytes
@ -42,7 +44,6 @@ from .features import DatabaseFeatures # isort:skip
from .introspection import DatabaseIntrospection # isort:skip
from .operations import DatabaseOperations # isort:skip
from .schema import DatabaseSchemaEditor # isort:skip
from .utils import parse_datetime_with_timezone_support # isort:skip
DatabaseError = Database.DatabaseError
IntegrityError = Database.IntegrityError
@ -71,9 +72,9 @@ def decoder(conv_func):
Database.register_converter(str("bool"), decoder(lambda s: s == '1'))
Database.register_converter(str("time"), decoder(parse_time))
Database.register_converter(str("date"), decoder(parse_date))
Database.register_converter(str("datetime"), decoder(parse_datetime_with_timezone_support))
Database.register_converter(str("timestamp"), decoder(parse_datetime_with_timezone_support))
Database.register_converter(str("TIMESTAMP"), decoder(parse_datetime_with_timezone_support))
Database.register_converter(str("datetime"), decoder(parse_datetime))
Database.register_converter(str("timestamp"), decoder(parse_datetime))
Database.register_converter(str("TIMESTAMP"), decoder(parse_datetime))
Database.register_converter(str("decimal"), decoder(backend_utils.typecast_decimal))
Database.register_adapter(datetime.datetime, adapt_datetime_with_timezone_support)

View File

@ -10,11 +10,9 @@ from django.db.backends import utils as backend_utils
from django.db.backends.base.operations import BaseDatabaseOperations
from django.db.models import aggregates, fields
from django.utils import six, timezone
from django.utils.dateparse import parse_date, parse_time
from django.utils.dateparse import parse_date, parse_datetime, parse_time
from django.utils.duration import duration_string
from .utils import parse_datetime_with_timezone_support
try:
import pytz
except ImportError:
@ -157,18 +155,23 @@ class DatabaseOperations(BaseDatabaseOperations):
return backend_utils.typecast_decimal(expression.output_field.format_number(value))
def convert_datefield_value(self, value, expression, connection, context):
if value is not None and not isinstance(value, datetime.date):
value = parse_date(value)
if value is not None:
if not isinstance(value, datetime.date):
value = parse_date(value)
return value
def convert_datetimefield_value(self, value, expression, connection, context):
if value is not None and not isinstance(value, datetime.datetime):
value = parse_datetime_with_timezone_support(value)
if value is not None:
if not isinstance(value, datetime.datetime):
value = parse_datetime(value)
if settings.USE_TZ:
value = value.replace(tzinfo=timezone.utc)
return value
def convert_timefield_value(self, value, expression, connection, context):
if value is not None and not isinstance(value, datetime.time):
value = parse_time(value)
if value is not None:
if not isinstance(value, datetime.time):
value = parse_time(value)
return value
def convert_uuidfield_value(self, value, expression, connection, context):

View File

@ -1,11 +0,0 @@
from django.conf import settings
from django.utils import timezone
from django.utils.dateparse import parse_datetime
def parse_datetime_with_timezone_support(value):
dt = parse_datetime(value)
# Confirm that dt is naive before overwriting its tzinfo.
if dt is not None and settings.USE_TZ and timezone.is_naive(dt):
dt = dt.replace(tzinfo=timezone.utc)
return dt

View File

@ -313,6 +313,12 @@ Database backend API
doesn't implement this. You may want to review the implementation on the
backends that Django includes for reference (:ticket:`24245`).
* The recommended way to add time zone information to datetimes fetched from
databases that don't support time zones is to register a converter for
``DateTimeField``. Do this in ``DatabaseOperations.get_db_converters()``.
Registering a global converter at the level of the DB-API module is
discouraged because it can conflict with other libraries.
Default settings that were tuples are now lists
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -412,6 +418,21 @@ console, for example.
If you are overriding Django's default logging, you should check to see how
your configuration merges with the new defaults.
Removal of time zone aware global converters for datetimes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Django no longer registers global converters for returning time zone aware
datetimes in database query results when :setting:`USE_TZ` is ``True``.
Instead the ORM adds suitable time zone information.
As a consequence, SQL queries executed outside of the ORM, for instance with
``cursor.execute(query, params)``, now return naive datetimes instead of aware
datetimes on databases that do not support time zones: SQLite, MySQL, and
Oracle. Since these datetimes are in UTC, you can make them aware as follows::
from django.utils import timezone
value = value.replace(tzinfo=timezone.utc)
Miscellaneous
~~~~~~~~~~~~~

View File

@ -10,6 +10,7 @@ from xml.dom.minidom import parseString
from django.contrib.auth.models import User
from django.core import serializers
from django.core.urlresolvers import reverse
from django.db import connection
from django.db.models import Max, Min
from django.http import HttpRequest
from django.template import (
@ -265,6 +266,13 @@ class LegacyDatabaseTests(TestCase):
[event],
transform=lambda d: d)
def test_cursor_execute_returns_naive_datetime(self):
dt = datetime.datetime(2011, 9, 1, 13, 20, 30)
Event.objects.create(dt=dt)
with connection.cursor() as cursor:
cursor.execute('SELECT dt FROM timezones_event WHERE dt = %s', [dt])
self.assertEqual(cursor.fetchall()[0][0], dt)
def test_filter_date_field_with_aware_datetime(self):
# Regression test for #17742
day = datetime.date(2011, 9, 1)
@ -556,6 +564,23 @@ class NewDatabaseTests(TestCase):
[event],
transform=lambda d: d)
@skipUnlessDBFeature('supports_timezones')
def test_cursor_execute_returns_aware_datetime(self):
dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
Event.objects.create(dt=dt)
with connection.cursor() as cursor:
cursor.execute('SELECT dt FROM timezones_event WHERE dt = %s', [dt])
self.assertEqual(cursor.fetchall()[0][0], dt)
@skipIfDBFeature('supports_timezones')
def test_cursor_execute_returns_naive_datetime(self):
dt = datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT)
utc_naive_dt = timezone.make_naive(dt, timezone.utc)
Event.objects.create(dt=dt)
with connection.cursor() as cursor:
cursor.execute('SELECT dt FROM timezones_event WHERE dt = %s', [dt])
self.assertEqual(cursor.fetchall()[0][0], utc_naive_dt)
@requires_tz_support
def test_filter_date_field_with_aware_datetime(self):
# Regression test for #17742