Refs #32365 -- Allowed use of non-pytz timezone implementations.

This commit is contained in:
Paul Ganssle 2021-01-19 11:16:01 +01:00 committed by Carlton Gibson
parent 73ffc73b68
commit 10d1261984
11 changed files with 477 additions and 306 deletions

View File

@ -161,6 +161,11 @@ def from_current_timezone(value):
if settings.USE_TZ and value is not None and timezone.is_naive(value): if settings.USE_TZ and value is not None and timezone.is_naive(value):
current_timezone = timezone.get_current_timezone() current_timezone = timezone.get_current_timezone()
try: try:
if (
not timezone._is_pytz_zone(current_timezone) and
timezone._datetime_ambiguous_or_imaginary(value, current_timezone)
):
raise ValueError('Ambiguous or non-existent time.')
return timezone.make_aware(value, current_timezone) return timezone.make_aware(value, current_timezone)
except Exception as exc: except Exception as exc:
raise ValidationError( raise ValidationError(

View File

@ -20,7 +20,8 @@ from django.utils.dates import (
) )
from django.utils.regex_helper import _lazy_re_compile from django.utils.regex_helper import _lazy_re_compile
from django.utils.timezone import ( from django.utils.timezone import (
get_default_timezone, is_aware, is_naive, make_aware, _datetime_ambiguous_or_imaginary, get_default_timezone, is_aware, is_naive,
make_aware,
) )
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -160,15 +161,9 @@ class TimeFormat(Formatter):
if not self.timezone: if not self.timezone:
return "" return ""
name = None if not _datetime_ambiguous_or_imaginary(self.data, self.timezone):
try:
name = self.timezone.tzname(self.data) name = self.timezone.tzname(self.data)
except Exception: else:
# pytz raises AmbiguousTimeError during the autumn DST change.
# This happens mainly when __init__ receives a naive datetime
# and sets self.timezone = get_default_timezone().
pass
if name is None:
name = self.format('O') name = self.format('O')
return str(name) return str(name)
@ -184,16 +179,13 @@ class TimeFormat(Formatter):
If timezone information is not available, return an empty string. If timezone information is not available, return an empty string.
""" """
if not self.timezone: if (
not self.timezone or
_datetime_ambiguous_or_imaginary(self.data, self.timezone)
):
return "" return ""
try: offset = self.timezone.utcoffset(self.data)
offset = self.timezone.utcoffset(self.data)
except Exception:
# pytz raises AmbiguousTimeError during the autumn DST change.
# This happens mainly when __init__ receives a naive datetime
# and sets self.timezone = get_default_timezone().
return ""
# `offset` is a datetime.timedelta. For negative values (to the west of # `offset` is a datetime.timedelta. For negative values (to the west of
# UTC) only days can be negative (days=-1) and seconds are always # UTC) only days can be negative (days=-1) and seconds are always
@ -232,16 +224,12 @@ class DateFormat(TimeFormat):
def I(self): # NOQA: E743, E741 def I(self): # NOQA: E743, E741
"'1' if Daylight Savings Time, '0' otherwise." "'1' if Daylight Savings Time, '0' otherwise."
try: if (
if self.timezone and self.timezone.dst(self.data): not self.timezone or
return '1' _datetime_ambiguous_or_imaginary(self.data, self.timezone)
else: ):
return '0'
except Exception:
# pytz raises AmbiguousTimeError during the autumn DST change.
# This happens mainly when __init__ receives a naive datetime
# and sets self.timezone = get_default_timezone().
return '' return ''
return '1' if self.timezone.dst(self.data) else '0'
def j(self): def j(self):
"Day of the month without leading zeros; i.e. '1' to '31'" "Day of the month without leading zeros; i.e. '1' to '31'"

View File

@ -24,6 +24,11 @@ __all__ = [
# UTC time zone as a tzinfo instance. # UTC time zone as a tzinfo instance.
utc = pytz.utc utc = pytz.utc
_PYTZ_BASE_CLASSES = (pytz.tzinfo.BaseTzInfo, pytz._FixedOffset)
# In releases prior to 2018.4, pytz.UTC was not a subclass of BaseTzInfo
if not isinstance(pytz.UTC, pytz._FixedOffset):
_PYTZ_BASE_CLASSES = _PYTZ_BASE_CLASSES + (type(pytz.UTC),)
def get_fixed_timezone(offset): def get_fixed_timezone(offset):
"""Return a tzinfo instance with a fixed offset from UTC.""" """Return a tzinfo instance with a fixed offset from UTC."""
@ -68,7 +73,7 @@ def get_current_timezone_name():
def _get_timezone_name(timezone): def _get_timezone_name(timezone):
"""Return the name of ``timezone``.""" """Return the name of ``timezone``."""
return timezone.tzname(None) return str(timezone)
# Timezone selection functions. # Timezone selection functions.
@ -229,7 +234,7 @@ def make_aware(value, timezone=None, is_dst=None):
"""Make a naive datetime.datetime in a given time zone aware.""" """Make a naive datetime.datetime in a given time zone aware."""
if timezone is None: if timezone is None:
timezone = get_current_timezone() timezone = get_current_timezone()
if hasattr(timezone, 'localize'): if _is_pytz_zone(timezone):
# This method is available for pytz time zones. # This method is available for pytz time zones.
return timezone.localize(value, is_dst=is_dst) return timezone.localize(value, is_dst=is_dst)
else: else:
@ -249,3 +254,20 @@ def make_naive(value, timezone=None):
if is_naive(value): if is_naive(value):
raise ValueError("make_naive() cannot be applied to a naive datetime") raise ValueError("make_naive() cannot be applied to a naive datetime")
return value.astimezone(timezone).replace(tzinfo=None) return value.astimezone(timezone).replace(tzinfo=None)
def _is_pytz_zone(tz):
"""Checks if a zone is a pytz zone."""
return isinstance(tz, _PYTZ_BASE_CLASSES)
def _datetime_ambiguous_or_imaginary(dt, tz):
if _is_pytz_zone(tz):
try:
tz.utcoffset(dt)
except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError):
return True
else:
return False
return tz.utcoffset(dt.replace(fold=not dt.fold)) != tz.utcoffset(dt)

View File

@ -943,21 +943,24 @@ appropriate entities.
: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>`.
The ``pytz.AmbiguousTimeError`` exception is raised if you try to make When using ``pytz``, the ``pytz.AmbiguousTimeError`` exception is raised if
``value`` aware during a DST transition where the same time occurs twice you try to make ``value`` aware during a DST transition where the same time
(when reverting from DST). Setting ``is_dst`` to ``True`` or ``False`` will occurs twice (when reverting from DST). Setting ``is_dst`` to ``True`` or
avoid the exception by choosing if the time is pre-transition or ``False`` will avoid the exception by choosing if the time is
post-transition respectively. pre-transition or post-transition respectively.
The ``pytz.NonExistentTimeError`` exception is raised if you try to make When using ``pytz``, the ``pytz.NonExistentTimeError`` exception is raised
``value`` aware during a DST transition such that the time never occurred. if you try to make ``value`` aware during a DST transition such that the
For example, if the 2:00 hour is skipped during a DST transition, trying to time never occurred. For example, if the 2:00 hour is skipped during a DST
make 2:30 aware in that time zone will raise an exception. To avoid that transition, trying to make 2:30 aware in that time zone will raise an
you can use ``is_dst`` to specify how ``make_aware()`` should interpret exception. To avoid that you can use ``is_dst`` to specify how
such a nonexistent time. If ``is_dst=True`` then the above time would be ``make_aware()`` should interpret such a nonexistent time. If
interpreted as 2:30 DST time (equivalent to 1:30 local time). Conversely, ``is_dst=True`` then the above time would be interpreted as 2:30 DST time
if ``is_dst=False`` the time would be interpreted as 2:30 standard time (equivalent to 1:30 local time). Conversely, if ``is_dst=False`` the time
(equivalent to 3:30 local time). would be interpreted as 2:30 standard time (equivalent to 3:30 local time).
The ``is_dst`` parameter has no effect when using non-``pytz`` timezone
implementations.
.. function:: make_naive(value, timezone=None) .. function:: make_naive(value, timezone=None)

View File

@ -657,6 +657,9 @@ MySQL 5.7 and higher.
Miscellaneous Miscellaneous
------------- -------------
* Django now supports non-``pytz`` time zones, such as Python 3.9+'s
:mod:`zoneinfo` module and its backport.
* The undocumented ``SpatiaLiteOperations.proj4_version()`` method is renamed * The undocumented ``SpatiaLiteOperations.proj4_version()`` method is renamed
to ``proj_version()``. to ``proj_version()``.

View File

@ -26,8 +26,15 @@ to this problem is to use UTC in the code and use local time only when
interacting with end users. interacting with end users.
Time zone support is disabled by default. To enable it, set :setting:`USE_TZ = Time zone support is disabled by default. To enable it, set :setting:`USE_TZ =
True <USE_TZ>` in your settings file. Time zone support uses pytz_, which is True <USE_TZ>` in your settings file. By default, time zone support uses pytz_,
installed when you install Django. which is installed when you install Django; Django also supports the use of
other time zone implementations like :mod:`zoneinfo` by passing
:class:`~datetime.tzinfo` objects directly to functions in
:mod:`django.utils.timezone`.
.. versionchanged:: 3.2
Support for non-``pytz`` timezone implementations was added.
.. note:: .. note::
@ -680,7 +687,8 @@ Usage
pytz_ provides helpers_, including a list of current time zones and a list pytz_ provides helpers_, including a list of current time zones and a list
of all available time zones -- some of which are only of historical of all available time zones -- some of which are only of historical
interest. interest. :mod:`zoneinfo` also provides similar functionality via
:func:`zoneinfo.available_timezones`.
.. _pytz: http://pytz.sourceforge.net/ .. _pytz: http://pytz.sourceforge.net/
.. _more examples: http://pytz.sourceforge.net/#example-usage .. _more examples: http://pytz.sourceforge.net/#example-usage

View File

@ -7,6 +7,14 @@ from urllib.parse import parse_qsl, urljoin, urlparse
import pytz import pytz
try:
import zoneinfo
except ImportError:
try:
from backports import zoneinfo
except ImportError:
zoneinfo = None
from django.contrib import admin from django.contrib import admin
from django.contrib.admin import AdminSite, ModelAdmin from django.contrib.admin import AdminSite, ModelAdmin
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
@ -63,6 +71,14 @@ for a staff account. Note that both fields may be case-sensitive."
MULTIPART_ENCTYPE = 'enctype="multipart/form-data"' MULTIPART_ENCTYPE = 'enctype="multipart/form-data"'
def make_aware_datetimes(dt, iana_key):
"""Makes one aware datetime for each supported time zone provider."""
yield pytz.timezone(iana_key).localize(dt, is_dst=None)
if zoneinfo is not None:
yield dt.replace(tzinfo=zoneinfo.ZoneInfo(iana_key))
class AdminFieldExtractionMixin: class AdminFieldExtractionMixin:
""" """
Helper methods for extracting data from AdminForm. Helper methods for extracting data from AdminForm.
@ -995,24 +1011,26 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
@override_settings(TIME_ZONE='America/Sao_Paulo', USE_TZ=True) @override_settings(TIME_ZONE='America/Sao_Paulo', USE_TZ=True)
def test_date_hierarchy_timezone_dst(self): def test_date_hierarchy_timezone_dst(self):
# This datetime doesn't exist in this timezone due to DST. # This datetime doesn't exist in this timezone due to DST.
date = pytz.timezone('America/Sao_Paulo').localize(datetime.datetime(2016, 10, 16, 15), is_dst=None) for date in make_aware_datetimes(datetime.datetime(2016, 10, 16, 15), 'America/Sao_Paulo'):
q = Question.objects.create(question='Why?', expires=date) with self.subTest(repr(date.tzinfo)):
Answer2.objects.create(question=q, answer='Because.') q = Question.objects.create(question='Why?', expires=date)
response = self.client.get(reverse('admin:admin_views_answer2_changelist')) Answer2.objects.create(question=q, answer='Because.')
self.assertContains(response, 'question__expires__day=16') response = self.client.get(reverse('admin:admin_views_answer2_changelist'))
self.assertContains(response, 'question__expires__month=10') self.assertContains(response, 'question__expires__day=16')
self.assertContains(response, 'question__expires__year=2016') self.assertContains(response, 'question__expires__month=10')
self.assertContains(response, 'question__expires__year=2016')
@override_settings(TIME_ZONE='America/Los_Angeles', USE_TZ=True) @override_settings(TIME_ZONE='America/Los_Angeles', USE_TZ=True)
def test_date_hierarchy_local_date_differ_from_utc(self): def test_date_hierarchy_local_date_differ_from_utc(self):
# This datetime is 2017-01-01 in UTC. # This datetime is 2017-01-01 in UTC.
date = pytz.timezone('America/Los_Angeles').localize(datetime.datetime(2016, 12, 31, 16)) for date in make_aware_datetimes(datetime.datetime(2016, 12, 31, 16), 'America/Los_Angeles'):
q = Question.objects.create(question='Why?', expires=date) with self.subTest(repr(date.tzinfo)):
Answer2.objects.create(question=q, answer='Because.') q = Question.objects.create(question='Why?', expires=date)
response = self.client.get(reverse('admin:admin_views_answer2_changelist')) Answer2.objects.create(question=q, answer='Because.')
self.assertContains(response, 'question__expires__day=31') response = self.client.get(reverse('admin:admin_views_answer2_changelist'))
self.assertContains(response, 'question__expires__month=12') self.assertContains(response, 'question__expires__day=31')
self.assertContains(response, 'question__expires__year=2016') self.assertContains(response, 'question__expires__month=12')
self.assertContains(response, 'question__expires__year=2016')
def test_sortable_by_columns_subset(self): def test_sortable_by_columns_subset(self):
expected_sortable_fields = ('date', 'callable_year') expected_sortable_fields = ('date', 'callable_year')

View File

@ -2,6 +2,14 @@ from datetime import datetime, timedelta, timezone as datetime_timezone
import pytz import pytz
try:
import zoneinfo
except ImportError:
try:
from backports import zoneinfo
except ImportError:
zoneinfo = None
from django.conf import settings from django.conf import settings
from django.db.models import ( from django.db.models import (
DateField, DateTimeField, F, IntegerField, Max, OuterRef, Subquery, DateField, DateTimeField, F, IntegerField, Max, OuterRef, Subquery,
@ -21,6 +29,10 @@ from django.utils import timezone
from ..models import Author, DTModel, Fan from ..models import Author, DTModel, Fan
ZONE_CONSTRUCTORS = (pytz.timezone,)
if zoneinfo is not None:
ZONE_CONSTRUCTORS += (zoneinfo.ZoneInfo,)
def truncate_to(value, kind, tzinfo=None): def truncate_to(value, kind, tzinfo=None):
# Convert to target timezone before truncation # Convert to target timezone before truncation
@ -1039,7 +1051,7 @@ class DateFunctionTests(TestCase):
outer = Author.objects.annotate( outer = Author.objects.annotate(
newest_fan_year=TruncYear(Subquery(inner, output_field=DateTimeField())) newest_fan_year=TruncYear(Subquery(inner, output_field=DateTimeField()))
) )
tz = pytz.UTC if settings.USE_TZ else None tz = timezone.utc if settings.USE_TZ else None
self.assertSequenceEqual( self.assertSequenceEqual(
outer.order_by('name').values('name', 'newest_fan_year'), outer.order_by('name').values('name', 'newest_fan_year'),
[ [
@ -1052,63 +1064,68 @@ class DateFunctionTests(TestCase):
@override_settings(USE_TZ=True, TIME_ZONE='UTC') @override_settings(USE_TZ=True, TIME_ZONE='UTC')
class DateFunctionWithTimeZoneTests(DateFunctionTests): class DateFunctionWithTimeZoneTests(DateFunctionTests):
def get_timezones(self, key):
for constructor in ZONE_CONSTRUCTORS:
yield constructor(key)
def test_extract_func_with_timezone(self): def test_extract_func_with_timezone(self):
start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321) start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123) end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123)
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime, is_dst=False)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) end_datetime = timezone.make_aware(end_datetime, is_dst=False)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
melb = pytz.timezone('Australia/Melbourne')
delta_tzinfo_pos = datetime_timezone(timedelta(hours=5)) delta_tzinfo_pos = datetime_timezone(timedelta(hours=5))
delta_tzinfo_neg = datetime_timezone(timedelta(hours=-5, minutes=17)) delta_tzinfo_neg = datetime_timezone(timedelta(hours=-5, minutes=17))
qs = DTModel.objects.annotate( for melb in self.get_timezones('Australia/Melbourne'):
day=Extract('start_datetime', 'day'), with self.subTest(repr(melb)):
day_melb=Extract('start_datetime', 'day', tzinfo=melb), qs = DTModel.objects.annotate(
week=Extract('start_datetime', 'week', tzinfo=melb), day=Extract('start_datetime', 'day'),
isoyear=ExtractIsoYear('start_datetime', tzinfo=melb), day_melb=Extract('start_datetime', 'day', tzinfo=melb),
weekday=ExtractWeekDay('start_datetime'), week=Extract('start_datetime', 'week', tzinfo=melb),
weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb), isoyear=ExtractIsoYear('start_datetime', tzinfo=melb),
isoweekday=ExtractIsoWeekDay('start_datetime'), weekday=ExtractWeekDay('start_datetime'),
isoweekday_melb=ExtractIsoWeekDay('start_datetime', tzinfo=melb), weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
quarter=ExtractQuarter('start_datetime', tzinfo=melb), isoweekday=ExtractIsoWeekDay('start_datetime'),
hour=ExtractHour('start_datetime'), isoweekday_melb=ExtractIsoWeekDay('start_datetime', tzinfo=melb),
hour_melb=ExtractHour('start_datetime', tzinfo=melb), quarter=ExtractQuarter('start_datetime', tzinfo=melb),
hour_with_delta_pos=ExtractHour('start_datetime', tzinfo=delta_tzinfo_pos), hour=ExtractHour('start_datetime'),
hour_with_delta_neg=ExtractHour('start_datetime', tzinfo=delta_tzinfo_neg), hour_melb=ExtractHour('start_datetime', tzinfo=melb),
minute_with_delta_neg=ExtractMinute('start_datetime', tzinfo=delta_tzinfo_neg), hour_with_delta_pos=ExtractHour('start_datetime', tzinfo=delta_tzinfo_pos),
).order_by('start_datetime') hour_with_delta_neg=ExtractHour('start_datetime', tzinfo=delta_tzinfo_neg),
minute_with_delta_neg=ExtractMinute('start_datetime', tzinfo=delta_tzinfo_neg),
).order_by('start_datetime')
utc_model = qs.get() utc_model = qs.get()
self.assertEqual(utc_model.day, 15) self.assertEqual(utc_model.day, 15)
self.assertEqual(utc_model.day_melb, 16) self.assertEqual(utc_model.day_melb, 16)
self.assertEqual(utc_model.week, 25) self.assertEqual(utc_model.week, 25)
self.assertEqual(utc_model.isoyear, 2015) self.assertEqual(utc_model.isoyear, 2015)
self.assertEqual(utc_model.weekday, 2) self.assertEqual(utc_model.weekday, 2)
self.assertEqual(utc_model.weekday_melb, 3) self.assertEqual(utc_model.weekday_melb, 3)
self.assertEqual(utc_model.isoweekday, 1) self.assertEqual(utc_model.isoweekday, 1)
self.assertEqual(utc_model.isoweekday_melb, 2) self.assertEqual(utc_model.isoweekday_melb, 2)
self.assertEqual(utc_model.quarter, 2) self.assertEqual(utc_model.quarter, 2)
self.assertEqual(utc_model.hour, 23) self.assertEqual(utc_model.hour, 23)
self.assertEqual(utc_model.hour_melb, 9) self.assertEqual(utc_model.hour_melb, 9)
self.assertEqual(utc_model.hour_with_delta_pos, 4) self.assertEqual(utc_model.hour_with_delta_pos, 4)
self.assertEqual(utc_model.hour_with_delta_neg, 18) self.assertEqual(utc_model.hour_with_delta_neg, 18)
self.assertEqual(utc_model.minute_with_delta_neg, 47) self.assertEqual(utc_model.minute_with_delta_neg, 47)
with timezone.override(melb): with timezone.override(melb):
melb_model = qs.get() melb_model = qs.get()
self.assertEqual(melb_model.day, 16) self.assertEqual(melb_model.day, 16)
self.assertEqual(melb_model.day_melb, 16) self.assertEqual(melb_model.day_melb, 16)
self.assertEqual(melb_model.week, 25) self.assertEqual(melb_model.week, 25)
self.assertEqual(melb_model.isoyear, 2015) self.assertEqual(melb_model.isoyear, 2015)
self.assertEqual(melb_model.weekday, 3) self.assertEqual(melb_model.weekday, 3)
self.assertEqual(melb_model.isoweekday, 2) self.assertEqual(melb_model.isoweekday, 2)
self.assertEqual(melb_model.quarter, 2) self.assertEqual(melb_model.quarter, 2)
self.assertEqual(melb_model.weekday_melb, 3) self.assertEqual(melb_model.weekday_melb, 3)
self.assertEqual(melb_model.isoweekday_melb, 2) self.assertEqual(melb_model.isoweekday_melb, 2)
self.assertEqual(melb_model.hour, 9) self.assertEqual(melb_model.hour, 9)
self.assertEqual(melb_model.hour_melb, 9) self.assertEqual(melb_model.hour_melb, 9)
def test_extract_func_explicit_timezone_priority(self): def test_extract_func_explicit_timezone_priority(self):
start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321) start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
@ -1116,27 +1133,29 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime, is_dst=False)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) end_datetime = timezone.make_aware(end_datetime, is_dst=False)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
melb = pytz.timezone('Australia/Melbourne')
with timezone.override(melb): for melb in self.get_timezones('Australia/Melbourne'):
model = DTModel.objects.annotate( with self.subTest(repr(melb)):
day_melb=Extract('start_datetime', 'day'), with timezone.override(melb):
day_utc=Extract('start_datetime', 'day', tzinfo=timezone.utc), model = DTModel.objects.annotate(
).order_by('start_datetime').get() day_melb=Extract('start_datetime', 'day'),
self.assertEqual(model.day_melb, 16) day_utc=Extract('start_datetime', 'day', tzinfo=timezone.utc),
self.assertEqual(model.day_utc, 15) ).order_by('start_datetime').get()
self.assertEqual(model.day_melb, 16)
self.assertEqual(model.day_utc, 15)
def test_extract_invalid_field_with_timezone(self): def test_extract_invalid_field_with_timezone(self):
melb = pytz.timezone('Australia/Melbourne') for melb in self.get_timezones('Australia/Melbourne'):
msg = 'tzinfo can only be used with DateTimeField.' with self.subTest(repr(melb)):
with self.assertRaisesMessage(ValueError, msg): msg = 'tzinfo can only be used with DateTimeField.'
DTModel.objects.annotate( with self.assertRaisesMessage(ValueError, msg):
day_melb=Extract('start_date', 'day', tzinfo=melb), DTModel.objects.annotate(
).get() day_melb=Extract('start_date', 'day', tzinfo=melb),
with self.assertRaisesMessage(ValueError, msg): ).get()
DTModel.objects.annotate( with self.assertRaisesMessage(ValueError, msg):
hour_melb=Extract('start_time', 'hour', tzinfo=melb), DTModel.objects.annotate(
).get() hour_melb=Extract('start_time', 'hour', tzinfo=melb),
).get()
def test_trunc_timezone_applied_before_truncation(self): def test_trunc_timezone_applied_before_truncation(self):
start_datetime = datetime(2016, 1, 1, 1, 30, 50, 321) start_datetime = datetime(2016, 1, 1, 1, 30, 50, 321)
@ -1145,36 +1164,37 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
end_datetime = timezone.make_aware(end_datetime, is_dst=False) end_datetime = timezone.make_aware(end_datetime, is_dst=False)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
melb = pytz.timezone('Australia/Melbourne') for melb, pacific in zip(
pacific = pytz.timezone('US/Pacific') self.get_timezones('Australia/Melbourne'), self.get_timezones('America/Los_Angeles')
):
with self.subTest((repr(melb), repr(pacific))):
model = DTModel.objects.annotate(
melb_year=TruncYear('start_datetime', tzinfo=melb),
pacific_year=TruncYear('start_datetime', tzinfo=pacific),
melb_date=TruncDate('start_datetime', tzinfo=melb),
pacific_date=TruncDate('start_datetime', tzinfo=pacific),
melb_time=TruncTime('start_datetime', tzinfo=melb),
pacific_time=TruncTime('start_datetime', tzinfo=pacific),
).order_by('start_datetime').get()
model = DTModel.objects.annotate( melb_start_datetime = start_datetime.astimezone(melb)
melb_year=TruncYear('start_datetime', tzinfo=melb), pacific_start_datetime = start_datetime.astimezone(pacific)
pacific_year=TruncYear('start_datetime', tzinfo=pacific), self.assertEqual(model.start_datetime, start_datetime)
melb_date=TruncDate('start_datetime', tzinfo=melb), self.assertEqual(model.melb_year, truncate_to(start_datetime, 'year', melb))
pacific_date=TruncDate('start_datetime', tzinfo=pacific), self.assertEqual(model.pacific_year, truncate_to(start_datetime, 'year', pacific))
melb_time=TruncTime('start_datetime', tzinfo=melb), self.assertEqual(model.start_datetime.year, 2016)
pacific_time=TruncTime('start_datetime', tzinfo=pacific), self.assertEqual(model.melb_year.year, 2016)
).order_by('start_datetime').get() self.assertEqual(model.pacific_year.year, 2015)
self.assertEqual(model.melb_date, melb_start_datetime.date())
melb_start_datetime = start_datetime.astimezone(melb) self.assertEqual(model.pacific_date, pacific_start_datetime.date())
pacific_start_datetime = start_datetime.astimezone(pacific) self.assertEqual(model.melb_time, melb_start_datetime.time())
self.assertEqual(model.start_datetime, start_datetime) self.assertEqual(model.pacific_time, pacific_start_datetime.time())
self.assertEqual(model.melb_year, truncate_to(start_datetime, 'year', melb))
self.assertEqual(model.pacific_year, truncate_to(start_datetime, 'year', pacific))
self.assertEqual(model.start_datetime.year, 2016)
self.assertEqual(model.melb_year.year, 2016)
self.assertEqual(model.pacific_year.year, 2015)
self.assertEqual(model.melb_date, melb_start_datetime.date())
self.assertEqual(model.pacific_date, pacific_start_datetime.date())
self.assertEqual(model.melb_time, melb_start_datetime.time())
self.assertEqual(model.pacific_time, pacific_start_datetime.time())
def test_trunc_ambiguous_and_invalid_times(self): def test_trunc_ambiguous_and_invalid_times(self):
sao = pytz.timezone('America/Sao_Paulo') sao = pytz.timezone('America/Sao_Paulo')
utc = pytz.timezone('UTC') utc = timezone.utc
start_datetime = utc.localize(datetime(2016, 10, 16, 13)) start_datetime = datetime(2016, 10, 16, 13, tzinfo=utc)
end_datetime = utc.localize(datetime(2016, 2, 21, 1)) end_datetime = datetime(2016, 2, 21, 1, tzinfo=utc)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
with timezone.override(sao): with timezone.override(sao):
with self.assertRaisesMessage(pytz.NonExistentTimeError, '2016-10-16 00:00:00'): with self.assertRaisesMessage(pytz.NonExistentTimeError, '2016-10-16 00:00:00'):
@ -1206,94 +1226,99 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
self.create_model(end_datetime, start_datetime) self.create_model(end_datetime, start_datetime)
melb = pytz.timezone('Australia/Melbourne') for melb in self.get_timezones('Australia/Melbourne'):
with self.subTest(repr(melb)):
def test_datetime_kind(kind): def test_datetime_kind(kind):
self.assertQuerysetEqual( self.assertQuerysetEqual(
DTModel.objects.annotate( DTModel.objects.annotate(
truncated=Trunc('start_datetime', kind, output_field=DateTimeField(), tzinfo=melb) truncated=Trunc(
).order_by('start_datetime'), 'start_datetime', kind, output_field=DateTimeField(), tzinfo=melb
[ )
(start_datetime, truncate_to(start_datetime.astimezone(melb), kind, melb)), ).order_by('start_datetime'),
(end_datetime, truncate_to(end_datetime.astimezone(melb), kind, melb)) [
], (start_datetime, truncate_to(start_datetime.astimezone(melb), kind, melb)),
lambda m: (m.start_datetime, m.truncated) (end_datetime, truncate_to(end_datetime.astimezone(melb), kind, melb))
) ],
lambda m: (m.start_datetime, m.truncated)
def test_datetime_to_date_kind(kind):
self.assertQuerysetEqual(
DTModel.objects.annotate(
truncated=Trunc(
'start_datetime',
kind,
output_field=DateField(),
tzinfo=melb,
),
).order_by('start_datetime'),
[
(
start_datetime,
truncate_to(start_datetime.astimezone(melb).date(), kind),
),
(
end_datetime,
truncate_to(end_datetime.astimezone(melb).date(), kind),
),
],
lambda m: (m.start_datetime, m.truncated),
)
def test_datetime_to_time_kind(kind):
self.assertQuerysetEqual(
DTModel.objects.annotate(
truncated=Trunc(
'start_datetime',
kind,
output_field=TimeField(),
tzinfo=melb,
) )
).order_by('start_datetime'),
[
(
start_datetime,
truncate_to(start_datetime.astimezone(melb).time(), kind),
),
(
end_datetime,
truncate_to(end_datetime.astimezone(melb).time(), kind),
),
],
lambda m: (m.start_datetime, m.truncated),
)
test_datetime_to_date_kind('year') def test_datetime_to_date_kind(kind):
test_datetime_to_date_kind('quarter') self.assertQuerysetEqual(
test_datetime_to_date_kind('month') DTModel.objects.annotate(
test_datetime_to_date_kind('week') truncated=Trunc(
test_datetime_to_date_kind('day') 'start_datetime',
test_datetime_to_time_kind('hour') kind,
test_datetime_to_time_kind('minute') output_field=DateField(),
test_datetime_to_time_kind('second') tzinfo=melb,
test_datetime_kind('year') ),
test_datetime_kind('quarter') ).order_by('start_datetime'),
test_datetime_kind('month') [
test_datetime_kind('week') (
test_datetime_kind('day') start_datetime,
test_datetime_kind('hour') truncate_to(start_datetime.astimezone(melb).date(), kind),
test_datetime_kind('minute') ),
test_datetime_kind('second') (
end_datetime,
truncate_to(end_datetime.astimezone(melb).date(), kind),
),
],
lambda m: (m.start_datetime, m.truncated),
)
qs = DTModel.objects.filter(start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField())) def test_datetime_to_time_kind(kind):
self.assertEqual(qs.count(), 2) self.assertQuerysetEqual(
DTModel.objects.annotate(
truncated=Trunc(
'start_datetime',
kind,
output_field=TimeField(),
tzinfo=melb,
)
).order_by('start_datetime'),
[
(
start_datetime,
truncate_to(start_datetime.astimezone(melb).time(), kind),
),
(
end_datetime,
truncate_to(end_datetime.astimezone(melb).time(), kind),
),
],
lambda m: (m.start_datetime, m.truncated),
)
test_datetime_to_date_kind('year')
test_datetime_to_date_kind('quarter')
test_datetime_to_date_kind('month')
test_datetime_to_date_kind('week')
test_datetime_to_date_kind('day')
test_datetime_to_time_kind('hour')
test_datetime_to_time_kind('minute')
test_datetime_to_time_kind('second')
test_datetime_kind('year')
test_datetime_kind('quarter')
test_datetime_kind('month')
test_datetime_kind('week')
test_datetime_kind('day')
test_datetime_kind('hour')
test_datetime_kind('minute')
test_datetime_kind('second')
qs = DTModel.objects.filter(
start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField())
)
self.assertEqual(qs.count(), 2)
def test_trunc_invalid_field_with_timezone(self): def test_trunc_invalid_field_with_timezone(self):
melb = pytz.timezone('Australia/Melbourne') for melb in self.get_timezones('Australia/Melbourne'):
msg = 'tzinfo can only be used with DateTimeField.' with self.subTest(repr(melb)):
with self.assertRaisesMessage(ValueError, msg): msg = 'tzinfo can only be used with DateTimeField.'
DTModel.objects.annotate( with self.assertRaisesMessage(ValueError, msg):
day_melb=Trunc('start_date', 'day', tzinfo=melb), DTModel.objects.annotate(
).get() day_melb=Trunc('start_date', 'day', tzinfo=melb),
with self.assertRaisesMessage(ValueError, msg): ).get()
DTModel.objects.annotate( with self.assertRaisesMessage(ValueError, msg):
hour_melb=Trunc('start_time', 'hour', tzinfo=melb), DTModel.objects.annotate(
).get() hour_melb=Trunc('start_time', 'hour', tzinfo=melb),
).get()

View File

@ -1,5 +1,6 @@
asgiref >= 3.2.10 asgiref >= 3.2.10
argon2-cffi >= 16.1.0 argon2-cffi >= 16.1.0
backports.zoneinfo; python_version < '3.9'
bcrypt bcrypt
docutils docutils
geoip2 geoip2
@ -17,4 +18,5 @@ PyYAML
selenium selenium
sqlparse >= 0.2.2 sqlparse >= 0.2.2
tblib >= 1.5.0 tblib >= 1.5.0
tzdata
colorama; sys.platform == 'win32' colorama; sys.platform == 'win32'

View File

@ -7,6 +7,14 @@ from xml.dom.minidom import parseString
import pytz import pytz
try:
import zoneinfo
except ImportError:
try:
from backports import zoneinfo
except ImportError:
zoneinfo = None
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import serializers from django.core import serializers
from django.db import connection from django.db import connection
@ -51,6 +59,14 @@ UTC = timezone.utc
EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi
ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok
ZONE_CONSTRUCTORS = (pytz.timezone,)
if zoneinfo is not None:
ZONE_CONSTRUCTORS += (zoneinfo.ZoneInfo,)
def get_timezones(key):
return [constructor(key) for constructor in ZONE_CONSTRUCTORS]
@contextmanager @contextmanager
def override_database_connection_timezone(timezone): def override_database_connection_timezone(timezone):
@ -326,16 +342,17 @@ class NewDatabaseTests(TestCase):
self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0) self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0)
def test_query_filter_with_pytz_timezones(self): def test_query_filter_with_pytz_timezones(self):
tz = pytz.timezone('Europe/Paris') for tz in get_timezones('Europe/Paris'):
dt = datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=tz) with self.subTest(repr(tz)):
Event.objects.create(dt=dt) dt = datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=tz)
next = dt + datetime.timedelta(seconds=3) Event.objects.create(dt=dt)
prev = dt - datetime.timedelta(seconds=3) next = dt + datetime.timedelta(seconds=3)
self.assertEqual(Event.objects.filter(dt__exact=dt).count(), 1) prev = dt - datetime.timedelta(seconds=3)
self.assertEqual(Event.objects.filter(dt__exact=next).count(), 0) self.assertEqual(Event.objects.filter(dt__exact=dt).count(), 1)
self.assertEqual(Event.objects.filter(dt__in=(prev, next)).count(), 0) self.assertEqual(Event.objects.filter(dt__exact=next).count(), 0)
self.assertEqual(Event.objects.filter(dt__in=(prev, dt, next)).count(), 1) self.assertEqual(Event.objects.filter(dt__in=(prev, next)).count(), 0)
self.assertEqual(Event.objects.filter(dt__range=(prev, next)).count(), 1) self.assertEqual(Event.objects.filter(dt__in=(prev, dt, next)).count(), 1)
self.assertEqual(Event.objects.filter(dt__range=(prev, next)).count(), 1)
def test_query_convert_timezones(self): def test_query_convert_timezones(self):
# Connection timezone is equal to the current timezone, datetime # Connection timezone is equal to the current timezone, datetime
@ -543,7 +560,7 @@ class NewDatabaseTests(TestCase):
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute('SELECT CURRENT_TIMESTAMP') cursor.execute('SELECT CURRENT_TIMESTAMP')
now = cursor.fetchone()[0] now = cursor.fetchone()[0]
self.assertEqual(now.tzinfo.zone, 'Europe/Paris') self.assertEqual(str(now.tzinfo), 'Europe/Paris')
@requires_tz_support @requires_tz_support
def test_filter_date_field_with_aware_datetime(self): def test_filter_date_field_with_aware_datetime(self):
@ -871,32 +888,26 @@ class TemplateTests(SimpleTestCase):
expected = results[k1][k2] expected = results[k1][k2]
self.assertEqual(actual, expected, '%s / %s: %r != %r' % (k1, k2, actual, expected)) self.assertEqual(actual, expected, '%s / %s: %r != %r' % (k1, k2, actual, expected))
def test_localtime_filters_with_pytz(self): def test_localtime_filters_with_iana(self):
""" """
Test the |localtime, |utc, and |timezone filters with pytz. Test the |localtime, |utc, and |timezone filters with iana zones.
""" """
# Use a pytz timezone as local time # Use an IANA timezone as local time
tpl = Template("{% load tz %}{{ dt|localtime }}|{{ dt|utc }}") tpl = Template("{% load tz %}{{ dt|localtime }}|{{ dt|utc }}")
ctx = Context({'dt': datetime.datetime(2011, 9, 1, 12, 20, 30)}) ctx = Context({'dt': datetime.datetime(2011, 9, 1, 12, 20, 30)})
with self.settings(TIME_ZONE='Europe/Paris'): with self.settings(TIME_ZONE='Europe/Paris'):
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00|2011-09-01T10:20:30+00:00") self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00|2011-09-01T10:20:30+00:00")
# Use a pytz timezone as argument # Use an IANA timezone as argument
tpl = Template("{% load tz %}{{ dt|timezone:tz }}") for tz in get_timezones('Europe/Paris'):
ctx = Context({ with self.subTest(repr(tz)):
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30), tpl = Template("{% load tz %}{{ dt|timezone:tz }}")
'tz': pytz.timezone('Europe/Paris'), ctx = Context({
}) 'dt': datetime.datetime(2011, 9, 1, 13, 20, 30),
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00") 'tz': pytz.timezone('Europe/Paris'),
})
# Use a pytz timezone name as argument self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
tpl = Template("{% load tz %}{{ dt|timezone:'Europe/Paris' }}")
ctx = Context({
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30),
'tz': pytz.timezone('Europe/Paris'),
})
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
def test_localtime_templatetag_invalid_argument(self): def test_localtime_templatetag_invalid_argument(self):
with self.assertRaises(TemplateSyntaxError): with self.assertRaises(TemplateSyntaxError):
@ -945,20 +956,22 @@ class TemplateTests(SimpleTestCase):
"2011-09-01T13:20:30+03:00|2011-09-01T17:20:30+07:00|2011-09-01T13:20:30+03:00" "2011-09-01T13:20:30+03:00|2011-09-01T17:20:30+07:00|2011-09-01T13:20:30+03:00"
) )
def test_timezone_templatetag_with_pytz(self): def test_timezone_templatetag_with_iana(self):
""" """
Test the {% timezone %} templatetag with pytz. Test the {% timezone %} templatetag with IANA time zone providers.
""" """
tpl = Template("{% load tz %}{% timezone tz %}{{ dt }}{% endtimezone %}") tpl = Template("{% load tz %}{% timezone tz %}{{ dt }}{% endtimezone %}")
# Use a pytz timezone as argument # Use a IANA timezone as argument
ctx = Context({ for tz in get_timezones('Europe/Paris'):
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT), with self.subTest(repr(tz)):
'tz': pytz.timezone('Europe/Paris'), ctx = Context({
}) 'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00") 'tz': tz,
})
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
# Use a pytz timezone name as argument # Use a IANA timezone name as argument
ctx = Context({ ctx = Context({
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT), 'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
'tz': 'Europe/Paris', 'tz': 'Europe/Paris',
@ -991,13 +1004,15 @@ class TemplateTests(SimpleTestCase):
with timezone.override(UTC): with timezone.override(UTC):
self.assertEqual(tpl.render(Context({'tz': ICT})), "+0700") self.assertEqual(tpl.render(Context({'tz': ICT})), "+0700")
def test_get_current_timezone_templatetag_with_pytz(self): def test_get_current_timezone_templatetag_with_iana(self):
""" """
Test the {% get_current_timezone %} templatetag with pytz. Test the {% get_current_timezone %} templatetag with pytz.
""" """
tpl = Template("{% load tz %}{% get_current_timezone as time_zone %}{{ time_zone }}") tpl = Template("{% load tz %}{% get_current_timezone as time_zone %}{{ time_zone }}")
with timezone.override(pytz.timezone('Europe/Paris')): for tz in get_timezones('Europe/Paris'):
self.assertEqual(tpl.render(Context()), "Europe/Paris") with self.subTest(repr(tz)):
with timezone.override(tz):
self.assertEqual(tpl.render(Context()), "Europe/Paris")
tpl = Template( tpl = Template(
"{% load tz %}{% timezone 'Europe/Paris' %}" "{% load tz %}{% timezone 'Europe/Paris' %}"
@ -1059,17 +1074,21 @@ class LegacyFormsTests(TestCase):
def test_form_with_non_existent_time(self): def test_form_with_non_existent_time(self):
form = EventForm({'dt': '2011-03-27 02:30:00'}) form = EventForm({'dt': '2011-03-27 02:30:00'})
with timezone.override(pytz.timezone('Europe/Paris')): for tz in get_timezones('Europe/Paris'):
# This is a bug. with self.subTest(repr(tz)):
self.assertTrue(form.is_valid()) with timezone.override(tz):
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 3, 27, 2, 30, 0)) # This is a bug.
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 3, 27, 2, 30, 0))
def test_form_with_ambiguous_time(self): def test_form_with_ambiguous_time(self):
form = EventForm({'dt': '2011-10-30 02:30:00'}) form = EventForm({'dt': '2011-10-30 02:30:00'})
with timezone.override(pytz.timezone('Europe/Paris')): for tz in get_timezones('Europe/Paris'):
# This is a bug. with self.subTest(repr(tz)):
self.assertTrue(form.is_valid()) with timezone.override(tz):
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 10, 30, 2, 30, 0)) # This is a bug.
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 10, 30, 2, 30, 0))
def test_split_form(self): def test_split_form(self):
form = EventSplitForm({'dt_0': '2011-09-01', 'dt_1': '13:20:30'}) form = EventSplitForm({'dt_0': '2011-09-01', 'dt_1': '13:20:30'})
@ -1098,26 +1117,30 @@ class NewFormsTests(TestCase):
self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC)) self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 9, 1, 10, 20, 30, tzinfo=UTC))
def test_form_with_non_existent_time(self): def test_form_with_non_existent_time(self):
with timezone.override(pytz.timezone('Europe/Paris')): for tz in get_timezones('Europe/Paris'):
form = EventForm({'dt': '2011-03-27 02:30:00'}) with self.subTest(repr(tz)):
self.assertFalse(form.is_valid()) with timezone.override(tz):
self.assertEqual( form = EventForm({'dt': '2011-03-27 02:30:00'})
form.errors['dt'], [ self.assertFalse(form.is_valid())
'2011-03-27 02:30:00 couldnt be interpreted in time zone ' self.assertEqual(
'Europe/Paris; it may be ambiguous or it may not exist.' form.errors['dt'], [
] '2011-03-27 02:30:00 couldnt be interpreted in time zone '
) 'Europe/Paris; it may be ambiguous or it may not exist.'
]
)
def test_form_with_ambiguous_time(self): def test_form_with_ambiguous_time(self):
with timezone.override(pytz.timezone('Europe/Paris')): for tz in get_timezones('Europe/Paris'):
form = EventForm({'dt': '2011-10-30 02:30:00'}) with self.subTest(repr(tz)):
self.assertFalse(form.is_valid()) with timezone.override(tz):
self.assertEqual( form = EventForm({'dt': '2011-10-30 02:30:00'})
form.errors['dt'], [ self.assertFalse(form.is_valid())
'2011-10-30 02:30:00 couldnt be interpreted in time zone ' self.assertEqual(
'Europe/Paris; it may be ambiguous or it may not exist.' form.errors['dt'], [
] '2011-10-30 02:30:00 couldnt be interpreted in time zone '
) 'Europe/Paris; it may be ambiguous or it may not exist.'
]
)
@requires_tz_support @requires_tz_support
def test_split_form(self): def test_split_form(self):

View File

@ -1,14 +1,38 @@
import datetime import datetime
import unittest
from unittest import mock from unittest import mock
import pytz import pytz
try:
import zoneinfo
except ImportError:
try:
from backports import zoneinfo
except ImportError:
zoneinfo = None
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, override_settings
from django.utils import timezone from django.utils import timezone
CET = pytz.timezone("Europe/Paris") CET = pytz.timezone("Europe/Paris")
EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi
ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok
UTC = datetime.timezone.utc
HAS_ZONEINFO = zoneinfo is not None
if not HAS_ZONEINFO:
PARIS_ZI = None
PARIS_IMPLS = (CET,)
needs_zoneinfo = unittest.skip("Test requires zoneinfo")
else:
PARIS_ZI = zoneinfo.ZoneInfo('Europe/Paris')
PARIS_IMPLS = (CET, PARIS_ZI)
def needs_zoneinfo(f):
return f
class TimezoneTests(SimpleTestCase): class TimezoneTests(SimpleTestCase):
@ -142,13 +166,21 @@ class TimezoneTests(SimpleTestCase):
) )
def test_make_aware2(self): def test_make_aware2(self):
self.assertEqual( CEST = datetime.timezone(datetime.timedelta(hours=2), 'CEST')
timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), CET), for tz in PARIS_IMPLS:
CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30))) with self.subTest(repr(tz)):
self.assertEqual(
timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), tz),
datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=CEST))
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
timezone.make_aware(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET) timezone.make_aware(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET)
def test_make_aware_pytz(self): if HAS_ZONEINFO:
with self.assertRaises(ValueError):
timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=PARIS_ZI), PARIS_ZI)
def test_make_naive_pytz(self):
self.assertEqual( self.assertEqual(
timezone.make_naive(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET), timezone.make_naive(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET),
datetime.datetime(2011, 9, 1, 12, 20, 30)) datetime.datetime(2011, 9, 1, 12, 20, 30))
@ -160,6 +192,18 @@ class TimezoneTests(SimpleTestCase):
with self.assertRaisesMessage(ValueError, 'make_naive() cannot be applied to a naive datetime'): with self.assertRaisesMessage(ValueError, 'make_naive() cannot be applied to a naive datetime'):
timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30), CET) timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30), CET)
@needs_zoneinfo
def test_make_naive_zoneinfo(self):
self.assertEqual(
timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=PARIS_ZI), PARIS_ZI),
datetime.datetime(2011, 9, 1, 12, 20, 30)
)
self.assertEqual(
timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30, fold=1, tzinfo=PARIS_ZI), PARIS_ZI),
datetime.datetime(2011, 9, 1, 12, 20, 30, fold=1)
)
def test_make_aware_pytz_ambiguous(self): def test_make_aware_pytz_ambiguous(self):
# 2:30 happens twice, once before DST ends and once after # 2:30 happens twice, once before DST ends and once after
ambiguous = datetime.datetime(2015, 10, 25, 2, 30) ambiguous = datetime.datetime(2015, 10, 25, 2, 30)
@ -173,6 +217,21 @@ class TimezoneTests(SimpleTestCase):
self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1)) self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2)) self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
@needs_zoneinfo
def test_make_aware_zoneinfo_ambiguous(self):
# 2:30 happens twice, once before DST ends and once after
ambiguous = datetime.datetime(2015, 10, 25, 2, 30)
std = timezone.make_aware(ambiguous.replace(fold=1), timezone=PARIS_ZI)
dst = timezone.make_aware(ambiguous, timezone=PARIS_ZI)
self.assertEqual(
std.astimezone(UTC) - dst.astimezone(UTC),
datetime.timedelta(hours=1)
)
self.assertEqual(std.utcoffset(), datetime.timedelta(hours=1))
self.assertEqual(dst.utcoffset(), datetime.timedelta(hours=2))
def test_make_aware_pytz_non_existent(self): def test_make_aware_pytz_non_existent(self):
# 2:30 never happened due to DST # 2:30 never happened due to DST
non_existent = datetime.datetime(2015, 3, 29, 2, 30) non_existent = datetime.datetime(2015, 3, 29, 2, 30)
@ -186,6 +245,21 @@ class TimezoneTests(SimpleTestCase):
self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1)) self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2)) self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
@needs_zoneinfo
def test_make_aware_zoneinfo_non_existent(self):
# 2:30 never happened due to DST
non_existent = datetime.datetime(2015, 3, 29, 2, 30)
std = timezone.make_aware(non_existent, PARIS_ZI)
dst = timezone.make_aware(non_existent.replace(fold=1), PARIS_ZI)
self.assertEqual(
std.astimezone(UTC) - dst.astimezone(UTC),
datetime.timedelta(hours=1)
)
self.assertEqual(std.utcoffset(), datetime.timedelta(hours=1))
self.assertEqual(dst.utcoffset(), datetime.timedelta(hours=2))
def test_get_default_timezone(self): def test_get_default_timezone(self):
self.assertEqual(timezone.get_default_timezone_name(), 'America/Chicago') self.assertEqual(timezone.get_default_timezone_name(), 'America/Chicago')