Fixed #22598 -- Allowed make_aware() to work with ambiguous datetime
This commit is contained in:
parent
269a5dbdd3
commit
143255c8bb
|
@ -347,7 +347,7 @@ def is_naive(value):
|
||||||
return value.tzinfo is None or value.tzinfo.utcoffset(value) is None
|
return value.tzinfo is None or value.tzinfo.utcoffset(value) is None
|
||||||
|
|
||||||
|
|
||||||
def make_aware(value, timezone=None):
|
def make_aware(value, timezone=None, is_dst=None):
|
||||||
"""
|
"""
|
||||||
Makes a naive datetime.datetime in a given time zone aware.
|
Makes a naive datetime.datetime in a given time zone aware.
|
||||||
"""
|
"""
|
||||||
|
@ -355,7 +355,7 @@ def make_aware(value, timezone=None):
|
||||||
timezone = get_current_timezone()
|
timezone = get_current_timezone()
|
||||||
if hasattr(timezone, 'localize'):
|
if hasattr(timezone, 'localize'):
|
||||||
# This method is available for pytz time zones.
|
# This method is available for pytz time zones.
|
||||||
return timezone.localize(value, is_dst=None)
|
return timezone.localize(value, is_dst=is_dst)
|
||||||
else:
|
else:
|
||||||
# Check that we won't overwrite the timezone of an aware datetime.
|
# Check that we won't overwrite the timezone of an aware datetime.
|
||||||
if is_aware(value):
|
if is_aware(value):
|
||||||
|
|
|
@ -947,20 +947,37 @@ appropriate entities.
|
||||||
Returns ``True`` if ``value`` is naive, ``False`` if it is aware. This
|
Returns ``True`` if ``value`` is naive, ``False`` if it is aware. This
|
||||||
function assumes that ``value`` is a :class:`~datetime.datetime`.
|
function assumes that ``value`` is a :class:`~datetime.datetime`.
|
||||||
|
|
||||||
.. function:: make_aware(value, timezone=None)
|
.. function:: make_aware(value, timezone=None, is_dst=None)
|
||||||
|
|
||||||
Returns an aware :class:`~datetime.datetime` that represents the same
|
Returns an aware :class:`~datetime.datetime` that represents the same
|
||||||
point in time as ``value`` in ``timezone``, ``value`` being a naive
|
point in time as ``value`` in ``timezone``, ``value`` being a naive
|
||||||
:class:`~datetime.datetime`. If ``timezone`` is set to ``None``, it
|
:class:`~datetime.datetime`. If ``timezone`` is set to ``None``, it
|
||||||
defaults to the :ref:`current time zone <default-current-time-zone>`.
|
defaults to the :ref:`current time zone <default-current-time-zone>`.
|
||||||
|
|
||||||
This function can raise an exception if ``value`` doesn't exist or is
|
When pytz_ is installed, the exception ``pytz.AmbiguousTimeError``
|
||||||
ambiguous because of DST transitions.
|
will be raised if you try to make ``value`` aware during a DST transition
|
||||||
|
where the same time occurs twice (when reverting from DST). Setting
|
||||||
|
``is_dst`` to ``True`` or ``False`` will avoid the exception by choosing if
|
||||||
|
the time is pre-transition or post-transition respectively.
|
||||||
|
|
||||||
|
When pytz_ is installed, the exception ``pytz.NonExistentTimeError``
|
||||||
|
will be raised if you try to make ``value`` aware during a DST transition
|
||||||
|
such that the time never occurred (when entering into DST). Setting
|
||||||
|
``is_dst`` to ``True`` or ``False`` will avoid the exception by moving the
|
||||||
|
hour backwards or forwards by 1 respectively. For example, ``is_dst=True``
|
||||||
|
would change a non-existent time of 2:30 to 1:30 and ``is_dst=False``
|
||||||
|
would change the time to 3:30.
|
||||||
|
|
||||||
|
``is_dst`` has no effect when ``pytz`` is not installed.
|
||||||
|
|
||||||
.. versionchanged:: 1.8
|
.. versionchanged:: 1.8
|
||||||
|
|
||||||
In older versions of Django, ``timezone`` was a required argument.
|
In older versions of Django, ``timezone`` was a required argument.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.9
|
||||||
|
|
||||||
|
The ``is_dst`` argument was added.
|
||||||
|
|
||||||
.. function:: make_naive(value, timezone=None)
|
.. function:: make_naive(value, timezone=None)
|
||||||
|
|
||||||
Returns an naive :class:`~datetime.datetime` that represents in
|
Returns an naive :class:`~datetime.datetime` that represents in
|
||||||
|
|
|
@ -159,6 +159,9 @@ Internationalization
|
||||||
* The :func:`django.views.i18n.javascript_catalog` view now works correctly
|
* The :func:`django.views.i18n.javascript_catalog` view now works correctly
|
||||||
if used multiple times with different configurations on the same page.
|
if used multiple times with different configurations on the same page.
|
||||||
|
|
||||||
|
* The :func:`django.utils.timezone.make_aware` function gained an ``is_dst``
|
||||||
|
argument to help resolve ambiguous times during DST transitions.
|
||||||
|
|
||||||
Management Commands
|
Management Commands
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -155,3 +155,37 @@ class TimezoneTests(unittest.TestCase):
|
||||||
datetime.datetime(2011, 9, 1, 12, 20, 30))
|
datetime.datetime(2011, 9, 1, 12, 20, 30))
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30), CET)
|
timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30), CET)
|
||||||
|
|
||||||
|
@unittest.skipIf(pytz is None, "this test requires pytz")
|
||||||
|
def test_make_aware_pytz_ambiguous(self):
|
||||||
|
# 2:30 happens twice, once before DST ends and once after
|
||||||
|
ambiguous = datetime.datetime(2015, 10, 25, 2, 30)
|
||||||
|
|
||||||
|
with self.assertRaises(pytz.AmbiguousTimeError):
|
||||||
|
timezone.make_aware(ambiguous, timezone=CET)
|
||||||
|
|
||||||
|
std = timezone.make_aware(ambiguous, timezone=CET, is_dst=False)
|
||||||
|
dst = timezone.make_aware(ambiguous, timezone=CET, is_dst=True)
|
||||||
|
self.assertEqual(std - dst, datetime.timedelta(hours=1))
|
||||||
|
self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
|
||||||
|
self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
|
||||||
|
|
||||||
|
@unittest.skipIf(pytz is None, "this test requires pytz")
|
||||||
|
def test_make_aware_pytz_non_existent(self):
|
||||||
|
# 2:30 never happened due to DST
|
||||||
|
non_existent = datetime.datetime(2015, 3, 29, 2, 30)
|
||||||
|
|
||||||
|
with self.assertRaises(pytz.NonExistentTimeError):
|
||||||
|
timezone.make_aware(non_existent, timezone=CET)
|
||||||
|
|
||||||
|
std = timezone.make_aware(non_existent, timezone=CET, is_dst=False)
|
||||||
|
dst = timezone.make_aware(non_existent, timezone=CET, is_dst=True)
|
||||||
|
self.assertEqual(std - dst, datetime.timedelta(hours=1))
|
||||||
|
self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
|
||||||
|
self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
|
||||||
|
|
||||||
|
# round trip to UTC then back to CET
|
||||||
|
std = timezone.localtime(timezone.localtime(std, timezone.UTC()), CET)
|
||||||
|
dst = timezone.localtime(timezone.localtime(dst, timezone.UTC()), CET)
|
||||||
|
self.assertEqual((std.hour, std.minute), (3, 30))
|
||||||
|
self.assertEqual((dst.hour, dst.minute), (1, 30))
|
||||||
|
|
Loading…
Reference in New Issue