Browse Source

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

Paul Ganssle 4 years ago
parent
commit
10d1261984

+ 5 - 0
django/forms/utils.py

@@ -161,6 +161,11 @@ def from_current_timezone(value):
     if settings.USE_TZ and value is not None and timezone.is_naive(value):
         current_timezone = timezone.get_current_timezone()
         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)
         except Exception as exc:
             raise ValidationError(

+ 14 - 26
django/utils/dateformat.py

@@ -20,7 +20,8 @@ from django.utils.dates import (
 )
 from django.utils.regex_helper import _lazy_re_compile
 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 _
 
@@ -160,15 +161,9 @@ class TimeFormat(Formatter):
         if not self.timezone:
             return ""
 
-        name = None
-        try:
+        if not _datetime_ambiguous_or_imaginary(self.data, self.timezone):
             name = self.timezone.tzname(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().
-            pass
-        if name is None:
+        else:
             name = self.format('O')
         return str(name)
 
@@ -184,16 +179,13 @@ class TimeFormat(Formatter):
 
         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 ""
 
-        try:
-            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 = self.timezone.utcoffset(self.data)
 
         # `offset` is a datetime.timedelta. For negative values (to the west of
         # 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
         "'1' if Daylight Savings Time, '0' otherwise."
-        try:
-            if self.timezone and self.timezone.dst(self.data):
-                return '1'
-            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().
+        if (
+            not self.timezone or
+            _datetime_ambiguous_or_imaginary(self.data, self.timezone)
+        ):
             return ''
+        return '1' if self.timezone.dst(self.data) else '0'
 
     def j(self):
         "Day of the month without leading zeros; i.e. '1' to '31'"

+ 24 - 2
django/utils/timezone.py

@@ -24,6 +24,11 @@ __all__ = [
 # UTC time zone as a tzinfo instance.
 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):
     """Return a tzinfo instance with a fixed offset from UTC."""
@@ -68,7 +73,7 @@ def get_current_timezone_name():
 
 def _get_timezone_name(timezone):
     """Return the name of ``timezone``."""
-    return timezone.tzname(None)
+    return str(timezone)
 
 # 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."""
     if timezone is None:
         timezone = get_current_timezone()
-    if hasattr(timezone, 'localize'):
+    if _is_pytz_zone(timezone):
         # This method is available for pytz time zones.
         return timezone.localize(value, is_dst=is_dst)
     else:
@@ -249,3 +254,20 @@ def make_naive(value, timezone=None):
     if is_naive(value):
         raise ValueError("make_naive() cannot be applied to a naive datetime")
     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)

+ 18 - 15
docs/ref/utils.txt

@@ -943,21 +943,24 @@ appropriate entities.
     :class:`~datetime.datetime`. If ``timezone`` is set to ``None``, it
     defaults to the :ref:`current time zone <default-current-time-zone>`.
 
-    The ``pytz.AmbiguousTimeError`` exception is 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.
-
-    The ``pytz.NonExistentTimeError`` exception is raised if you try to make
-    ``value`` aware during a DST transition such that the time never occurred.
-    For example, if the 2:00 hour is skipped during a DST transition, trying to
-    make 2:30 aware in that time zone will raise an exception. To avoid that
-    you can use ``is_dst`` to specify how ``make_aware()`` should interpret
-    such a nonexistent time. If ``is_dst=True`` then the above time would be
-    interpreted as 2:30 DST time (equivalent to 1:30 local time). Conversely,
-    if ``is_dst=False`` the time would be interpreted as 2:30 standard time
-    (equivalent to 3:30 local time).
+    When using ``pytz``, the ``pytz.AmbiguousTimeError`` exception is 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 using ``pytz``, the ``pytz.NonExistentTimeError`` exception is raised
+    if you try to make ``value`` aware during a DST transition such that the
+    time never occurred. For example, if the 2:00 hour is skipped during a DST
+    transition, trying to make 2:30 aware in that time zone will raise an
+    exception. To avoid that you can use ``is_dst`` to specify how
+    ``make_aware()`` should interpret such a nonexistent time. If
+    ``is_dst=True`` then the above time would be interpreted as 2:30 DST time
+    (equivalent to 1:30 local time). Conversely, if ``is_dst=False`` the 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)
 

+ 3 - 0
docs/releases/3.2.txt

@@ -657,6 +657,9 @@ MySQL 5.7 and higher.
 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
   to ``proj_version()``.
 

+ 11 - 3
docs/topics/i18n/timezones.txt

@@ -26,8 +26,15 @@ to this problem is to use UTC in the code and use local time only when
 interacting with end users.
 
 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
-installed when you install Django.
+True <USE_TZ>` in your settings file. By default, time zone support uses pytz_,
+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::
 
@@ -680,7 +687,8 @@ Usage
 
    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
-   interest.
+   interest. :mod:`zoneinfo` also provides similar functionality via
+   :func:`zoneinfo.available_timezones`.
 
 .. _pytz: http://pytz.sourceforge.net/
 .. _more examples: http://pytz.sourceforge.net/#example-usage

+ 32 - 14
tests/admin_views/tests.py

@@ -7,6 +7,14 @@ from urllib.parse import parse_qsl, urljoin, urlparse
 
 import pytz
 
+try:
+    import zoneinfo
+except ImportError:
+    try:
+        from backports import zoneinfo
+    except ImportError:
+        zoneinfo = None
+
 from django.contrib import admin
 from django.contrib.admin import AdminSite, ModelAdmin
 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"'
 
 
+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:
     """
     Helper methods for extracting data from AdminForm.
@@ -995,24 +1011,26 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
     @override_settings(TIME_ZONE='America/Sao_Paulo', USE_TZ=True)
     def test_date_hierarchy_timezone_dst(self):
         # 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)
-        q = Question.objects.create(question='Why?', expires=date)
-        Answer2.objects.create(question=q, answer='Because.')
-        response = self.client.get(reverse('admin:admin_views_answer2_changelist'))
-        self.assertContains(response, 'question__expires__day=16')
-        self.assertContains(response, 'question__expires__month=10')
-        self.assertContains(response, 'question__expires__year=2016')
+        for date in make_aware_datetimes(datetime.datetime(2016, 10, 16, 15), 'America/Sao_Paulo'):
+            with self.subTest(repr(date.tzinfo)):
+                q = Question.objects.create(question='Why?', expires=date)
+                Answer2.objects.create(question=q, answer='Because.')
+                response = self.client.get(reverse('admin:admin_views_answer2_changelist'))
+                self.assertContains(response, 'question__expires__day=16')
+                self.assertContains(response, 'question__expires__month=10')
+                self.assertContains(response, 'question__expires__year=2016')
 
     @override_settings(TIME_ZONE='America/Los_Angeles', USE_TZ=True)
     def test_date_hierarchy_local_date_differ_from_utc(self):
         # This datetime is 2017-01-01 in UTC.
-        date = pytz.timezone('America/Los_Angeles').localize(datetime.datetime(2016, 12, 31, 16))
-        q = Question.objects.create(question='Why?', expires=date)
-        Answer2.objects.create(question=q, answer='Because.')
-        response = self.client.get(reverse('admin:admin_views_answer2_changelist'))
-        self.assertContains(response, 'question__expires__day=31')
-        self.assertContains(response, 'question__expires__month=12')
-        self.assertContains(response, 'question__expires__year=2016')
+        for date in make_aware_datetimes(datetime.datetime(2016, 12, 31, 16), 'America/Los_Angeles'):
+            with self.subTest(repr(date.tzinfo)):
+                q = Question.objects.create(question='Why?', expires=date)
+                Answer2.objects.create(question=q, answer='Because.')
+                response = self.client.get(reverse('admin:admin_views_answer2_changelist'))
+                self.assertContains(response, 'question__expires__day=31')
+                self.assertContains(response, 'question__expires__month=12')
+                self.assertContains(response, 'question__expires__year=2016')
 
     def test_sortable_by_columns_subset(self):
         expected_sortable_fields = ('date', 'callable_year')

+ 204 - 179
tests/db_functions/datetime/test_extract_trunc.py

@@ -2,6 +2,14 @@ from datetime import datetime, timedelta, timezone as datetime_timezone
 
 import pytz
 
+try:
+    import zoneinfo
+except ImportError:
+    try:
+        from backports import zoneinfo
+    except ImportError:
+        zoneinfo = None
+
 from django.conf import settings
 from django.db.models import (
     DateField, DateTimeField, F, IntegerField, Max, OuterRef, Subquery,
@@ -21,6 +29,10 @@ from django.utils import timezone
 
 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):
     # Convert to target timezone before truncation
@@ -1039,7 +1051,7 @@ class DateFunctionTests(TestCase):
         outer = Author.objects.annotate(
             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(
             outer.order_by('name').values('name', 'newest_fan_year'),
             [
@@ -1052,63 +1064,68 @@ class DateFunctionTests(TestCase):
 @override_settings(USE_TZ=True, TIME_ZONE='UTC')
 class DateFunctionWithTimeZoneTests(DateFunctionTests):
 
+    def get_timezones(self, key):
+        for constructor in ZONE_CONSTRUCTORS:
+            yield constructor(key)
+
     def test_extract_func_with_timezone(self):
         start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
         end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123)
         start_datetime = timezone.make_aware(start_datetime, is_dst=False)
         end_datetime = timezone.make_aware(end_datetime, is_dst=False)
         self.create_model(start_datetime, end_datetime)
-        melb = pytz.timezone('Australia/Melbourne')
         delta_tzinfo_pos = datetime_timezone(timedelta(hours=5))
         delta_tzinfo_neg = datetime_timezone(timedelta(hours=-5, minutes=17))
 
-        qs = DTModel.objects.annotate(
-            day=Extract('start_datetime', 'day'),
-            day_melb=Extract('start_datetime', 'day', tzinfo=melb),
-            week=Extract('start_datetime', 'week', tzinfo=melb),
-            isoyear=ExtractIsoYear('start_datetime', tzinfo=melb),
-            weekday=ExtractWeekDay('start_datetime'),
-            weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
-            isoweekday=ExtractIsoWeekDay('start_datetime'),
-            isoweekday_melb=ExtractIsoWeekDay('start_datetime', tzinfo=melb),
-            quarter=ExtractQuarter('start_datetime', tzinfo=melb),
-            hour=ExtractHour('start_datetime'),
-            hour_melb=ExtractHour('start_datetime', tzinfo=melb),
-            hour_with_delta_pos=ExtractHour('start_datetime', tzinfo=delta_tzinfo_pos),
-            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()
-        self.assertEqual(utc_model.day, 15)
-        self.assertEqual(utc_model.day_melb, 16)
-        self.assertEqual(utc_model.week, 25)
-        self.assertEqual(utc_model.isoyear, 2015)
-        self.assertEqual(utc_model.weekday, 2)
-        self.assertEqual(utc_model.weekday_melb, 3)
-        self.assertEqual(utc_model.isoweekday, 1)
-        self.assertEqual(utc_model.isoweekday_melb, 2)
-        self.assertEqual(utc_model.quarter, 2)
-        self.assertEqual(utc_model.hour, 23)
-        self.assertEqual(utc_model.hour_melb, 9)
-        self.assertEqual(utc_model.hour_with_delta_pos, 4)
-        self.assertEqual(utc_model.hour_with_delta_neg, 18)
-        self.assertEqual(utc_model.minute_with_delta_neg, 47)
-
-        with timezone.override(melb):
-            melb_model = qs.get()
-
-        self.assertEqual(melb_model.day, 16)
-        self.assertEqual(melb_model.day_melb, 16)
-        self.assertEqual(melb_model.week, 25)
-        self.assertEqual(melb_model.isoyear, 2015)
-        self.assertEqual(melb_model.weekday, 3)
-        self.assertEqual(melb_model.isoweekday, 2)
-        self.assertEqual(melb_model.quarter, 2)
-        self.assertEqual(melb_model.weekday_melb, 3)
-        self.assertEqual(melb_model.isoweekday_melb, 2)
-        self.assertEqual(melb_model.hour, 9)
-        self.assertEqual(melb_model.hour_melb, 9)
+        for melb in self.get_timezones('Australia/Melbourne'):
+            with self.subTest(repr(melb)):
+                qs = DTModel.objects.annotate(
+                    day=Extract('start_datetime', 'day'),
+                    day_melb=Extract('start_datetime', 'day', tzinfo=melb),
+                    week=Extract('start_datetime', 'week', tzinfo=melb),
+                    isoyear=ExtractIsoYear('start_datetime', tzinfo=melb),
+                    weekday=ExtractWeekDay('start_datetime'),
+                    weekday_melb=ExtractWeekDay('start_datetime', tzinfo=melb),
+                    isoweekday=ExtractIsoWeekDay('start_datetime'),
+                    isoweekday_melb=ExtractIsoWeekDay('start_datetime', tzinfo=melb),
+                    quarter=ExtractQuarter('start_datetime', tzinfo=melb),
+                    hour=ExtractHour('start_datetime'),
+                    hour_melb=ExtractHour('start_datetime', tzinfo=melb),
+                    hour_with_delta_pos=ExtractHour('start_datetime', tzinfo=delta_tzinfo_pos),
+                    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()
+                self.assertEqual(utc_model.day, 15)
+                self.assertEqual(utc_model.day_melb, 16)
+                self.assertEqual(utc_model.week, 25)
+                self.assertEqual(utc_model.isoyear, 2015)
+                self.assertEqual(utc_model.weekday, 2)
+                self.assertEqual(utc_model.weekday_melb, 3)
+                self.assertEqual(utc_model.isoweekday, 1)
+                self.assertEqual(utc_model.isoweekday_melb, 2)
+                self.assertEqual(utc_model.quarter, 2)
+                self.assertEqual(utc_model.hour, 23)
+                self.assertEqual(utc_model.hour_melb, 9)
+                self.assertEqual(utc_model.hour_with_delta_pos, 4)
+                self.assertEqual(utc_model.hour_with_delta_neg, 18)
+                self.assertEqual(utc_model.minute_with_delta_neg, 47)
+
+                with timezone.override(melb):
+                    melb_model = qs.get()
+
+                self.assertEqual(melb_model.day, 16)
+                self.assertEqual(melb_model.day_melb, 16)
+                self.assertEqual(melb_model.week, 25)
+                self.assertEqual(melb_model.isoyear, 2015)
+                self.assertEqual(melb_model.weekday, 3)
+                self.assertEqual(melb_model.isoweekday, 2)
+                self.assertEqual(melb_model.quarter, 2)
+                self.assertEqual(melb_model.weekday_melb, 3)
+                self.assertEqual(melb_model.isoweekday_melb, 2)
+                self.assertEqual(melb_model.hour, 9)
+                self.assertEqual(melb_model.hour_melb, 9)
 
     def test_extract_func_explicit_timezone_priority(self):
         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)
         end_datetime = timezone.make_aware(end_datetime, is_dst=False)
         self.create_model(start_datetime, end_datetime)
-        melb = pytz.timezone('Australia/Melbourne')
 
-        with timezone.override(melb):
-            model = DTModel.objects.annotate(
-                day_melb=Extract('start_datetime', 'day'),
-                day_utc=Extract('start_datetime', 'day', tzinfo=timezone.utc),
-            ).order_by('start_datetime').get()
-            self.assertEqual(model.day_melb, 16)
-            self.assertEqual(model.day_utc, 15)
+        for melb in self.get_timezones('Australia/Melbourne'):
+            with self.subTest(repr(melb)):
+                with timezone.override(melb):
+                    model = DTModel.objects.annotate(
+                        day_melb=Extract('start_datetime', 'day'),
+                        day_utc=Extract('start_datetime', 'day', tzinfo=timezone.utc),
+                    ).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):
-        melb = pytz.timezone('Australia/Melbourne')
-        msg = 'tzinfo can only be used with DateTimeField.'
-        with self.assertRaisesMessage(ValueError, msg):
-            DTModel.objects.annotate(
-                day_melb=Extract('start_date', 'day', tzinfo=melb),
-            ).get()
-        with self.assertRaisesMessage(ValueError, msg):
-            DTModel.objects.annotate(
-                hour_melb=Extract('start_time', 'hour', tzinfo=melb),
-            ).get()
+        for melb in self.get_timezones('Australia/Melbourne'):
+            with self.subTest(repr(melb)):
+                msg = 'tzinfo can only be used with DateTimeField.'
+                with self.assertRaisesMessage(ValueError, msg):
+                    DTModel.objects.annotate(
+                        day_melb=Extract('start_date', 'day', tzinfo=melb),
+                    ).get()
+                with self.assertRaisesMessage(ValueError, msg):
+                    DTModel.objects.annotate(
+                        hour_melb=Extract('start_time', 'hour', tzinfo=melb),
+                    ).get()
 
     def test_trunc_timezone_applied_before_truncation(self):
         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)
         self.create_model(start_datetime, end_datetime)
 
-        melb = pytz.timezone('Australia/Melbourne')
-        pacific = pytz.timezone('US/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()
-
-        melb_start_datetime = start_datetime.astimezone(melb)
-        pacific_start_datetime = start_datetime.astimezone(pacific)
-        self.assertEqual(model.start_datetime, start_datetime)
-        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())
+        for melb, pacific in zip(
+            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()
+
+                melb_start_datetime = start_datetime.astimezone(melb)
+                pacific_start_datetime = start_datetime.astimezone(pacific)
+                self.assertEqual(model.start_datetime, start_datetime)
+                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):
         sao = pytz.timezone('America/Sao_Paulo')
-        utc = pytz.timezone('UTC')
-        start_datetime = utc.localize(datetime(2016, 10, 16, 13))
-        end_datetime = utc.localize(datetime(2016, 2, 21, 1))
+        utc = timezone.utc
+        start_datetime = datetime(2016, 10, 16, 13, tzinfo=utc)
+        end_datetime = datetime(2016, 2, 21, 1, tzinfo=utc)
         self.create_model(start_datetime, end_datetime)
         with timezone.override(sao):
             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(end_datetime, start_datetime)
 
-        melb = pytz.timezone('Australia/Melbourne')
-
-        def test_datetime_kind(kind):
-            self.assertQuerysetEqual(
-                DTModel.objects.annotate(
-                    truncated=Trunc('start_datetime', kind, output_field=DateTimeField(), tzinfo=melb)
-                ).order_by('start_datetime'),
-                [
-                    (start_datetime, truncate_to(start_datetime.astimezone(melb), kind, melb)),
-                    (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),
-            )
+        for melb in self.get_timezones('Australia/Melbourne'):
+            with self.subTest(repr(melb)):
+                def test_datetime_kind(kind):
+                    self.assertQuerysetEqual(
+                        DTModel.objects.annotate(
+                            truncated=Trunc(
+                                'start_datetime', kind, output_field=DateTimeField(), tzinfo=melb
+                            )
+                        ).order_by('start_datetime'),
+                        [
+                            (start_datetime, truncate_to(start_datetime.astimezone(melb), kind, melb)),
+                            (end_datetime, truncate_to(end_datetime.astimezone(melb), kind, melb))
+                        ],
+                        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,
+                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),
                     )
-                ).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')
+                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),
+                    )
 
-        qs = DTModel.objects.filter(start_datetime__date=Trunc('start_datetime', 'day', output_field=DateField()))
-        self.assertEqual(qs.count(), 2)
+                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):
-        melb = pytz.timezone('Australia/Melbourne')
-        msg = 'tzinfo can only be used with DateTimeField.'
-        with self.assertRaisesMessage(ValueError, msg):
-            DTModel.objects.annotate(
-                day_melb=Trunc('start_date', 'day', tzinfo=melb),
-            ).get()
-        with self.assertRaisesMessage(ValueError, msg):
-            DTModel.objects.annotate(
-                hour_melb=Trunc('start_time', 'hour', tzinfo=melb),
-            ).get()
+        for melb in self.get_timezones('Australia/Melbourne'):
+            with self.subTest(repr(melb)):
+                msg = 'tzinfo can only be used with DateTimeField.'
+                with self.assertRaisesMessage(ValueError, msg):
+                    DTModel.objects.annotate(
+                        day_melb=Trunc('start_date', 'day', tzinfo=melb),
+                    ).get()
+                with self.assertRaisesMessage(ValueError, msg):
+                    DTModel.objects.annotate(
+                        hour_melb=Trunc('start_time', 'hour', tzinfo=melb),
+                    ).get()

+ 2 - 0
tests/requirements/py3.txt

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

+ 90 - 67
tests/timezones/tests.py

@@ -7,6 +7,14 @@ from xml.dom.minidom import parseString
 
 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.core import serializers
 from django.db import connection
@@ -51,6 +59,14 @@ UTC = timezone.utc
 EAT = timezone.get_fixed_timezone(180)      # Africa/Nairobi
 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
 def override_database_connection_timezone(timezone):
@@ -326,16 +342,17 @@ class NewDatabaseTests(TestCase):
         self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0)
 
     def test_query_filter_with_pytz_timezones(self):
-        tz = pytz.timezone('Europe/Paris')
-        dt = datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=tz)
-        Event.objects.create(dt=dt)
-        next = dt + datetime.timedelta(seconds=3)
-        prev = dt - datetime.timedelta(seconds=3)
-        self.assertEqual(Event.objects.filter(dt__exact=dt).count(), 1)
-        self.assertEqual(Event.objects.filter(dt__exact=next).count(), 0)
-        self.assertEqual(Event.objects.filter(dt__in=(prev, next)).count(), 0)
-        self.assertEqual(Event.objects.filter(dt__in=(prev, dt, next)).count(), 1)
-        self.assertEqual(Event.objects.filter(dt__range=(prev, next)).count(), 1)
+        for tz in get_timezones('Europe/Paris'):
+            with self.subTest(repr(tz)):
+                dt = datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=tz)
+                Event.objects.create(dt=dt)
+                next = dt + datetime.timedelta(seconds=3)
+                prev = dt - datetime.timedelta(seconds=3)
+                self.assertEqual(Event.objects.filter(dt__exact=dt).count(), 1)
+                self.assertEqual(Event.objects.filter(dt__exact=next).count(), 0)
+                self.assertEqual(Event.objects.filter(dt__in=(prev, next)).count(), 0)
+                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):
         # Connection timezone is equal to the current timezone, datetime
@@ -543,7 +560,7 @@ class NewDatabaseTests(TestCase):
             with connection.cursor() as cursor:
                 cursor.execute('SELECT CURRENT_TIMESTAMP')
                 now = cursor.fetchone()[0]
-                self.assertEqual(now.tzinfo.zone, 'Europe/Paris')
+                self.assertEqual(str(now.tzinfo), 'Europe/Paris')
 
     @requires_tz_support
     def test_filter_date_field_with_aware_datetime(self):
@@ -871,32 +888,26 @@ class TemplateTests(SimpleTestCase):
                     expected = results[k1][k2]
                     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 }}")
         ctx = Context({'dt': datetime.datetime(2011, 9, 1, 12, 20, 30)})
 
         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")
 
-        # Use a pytz timezone as argument
-        tpl = Template("{% load tz %}{{ dt|timezone:tz }}")
-        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")
-
-        # Use a pytz timezone name as argument
-        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")
+        # Use an IANA timezone as argument
+        for tz in get_timezones('Europe/Paris'):
+            with self.subTest(repr(tz)):
+                tpl = Template("{% load tz %}{{ dt|timezone:tz }}")
+                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):
         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"
         )
 
-    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 %}")
 
-        # Use a pytz timezone as argument
-        ctx = Context({
-            'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
-            'tz': pytz.timezone('Europe/Paris'),
-        })
-        self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
+        # Use a IANA timezone as argument
+        for tz in get_timezones('Europe/Paris'):
+            with self.subTest(repr(tz)):
+                ctx = Context({
+                    'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
+                    '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({
             'dt': datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
             'tz': 'Europe/Paris',
@@ -991,13 +1004,15 @@ class TemplateTests(SimpleTestCase):
         with timezone.override(UTC):
             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.
         """
         tpl = Template("{% load tz %}{% get_current_timezone as time_zone %}{{ time_zone }}")
-        with timezone.override(pytz.timezone('Europe/Paris')):
-            self.assertEqual(tpl.render(Context()), "Europe/Paris")
+        for tz in get_timezones('Europe/Paris'):
+            with self.subTest(repr(tz)):
+                with timezone.override(tz):
+                    self.assertEqual(tpl.render(Context()), "Europe/Paris")
 
         tpl = Template(
             "{% load tz %}{% timezone 'Europe/Paris' %}"
@@ -1059,17 +1074,21 @@ class LegacyFormsTests(TestCase):
 
     def test_form_with_non_existent_time(self):
         form = EventForm({'dt': '2011-03-27 02:30:00'})
-        with timezone.override(pytz.timezone('Europe/Paris')):
-            # This is a bug.
-            self.assertTrue(form.is_valid())
-            self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 3, 27, 2, 30, 0))
+        for tz in get_timezones('Europe/Paris'):
+            with self.subTest(repr(tz)):
+                with timezone.override(tz):
+                    # 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):
         form = EventForm({'dt': '2011-10-30 02:30:00'})
-        with timezone.override(pytz.timezone('Europe/Paris')):
-            # This is a bug.
-            self.assertTrue(form.is_valid())
-            self.assertEqual(form.cleaned_data['dt'], datetime.datetime(2011, 10, 30, 2, 30, 0))
+        for tz in get_timezones('Europe/Paris'):
+            with self.subTest(repr(tz)):
+                with timezone.override(tz):
+                    # 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):
         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))
 
     def test_form_with_non_existent_time(self):
-        with timezone.override(pytz.timezone('Europe/Paris')):
-            form = EventForm({'dt': '2011-03-27 02:30:00'})
-            self.assertFalse(form.is_valid())
-            self.assertEqual(
-                form.errors['dt'], [
-                    '2011-03-27 02:30:00 couldn’t be interpreted in time zone '
-                    'Europe/Paris; it may be ambiguous or it may not exist.'
-                ]
-            )
+        for tz in get_timezones('Europe/Paris'):
+            with self.subTest(repr(tz)):
+                with timezone.override(tz):
+                    form = EventForm({'dt': '2011-03-27 02:30:00'})
+                    self.assertFalse(form.is_valid())
+                    self.assertEqual(
+                        form.errors['dt'], [
+                            '2011-03-27 02:30:00 couldn’t be interpreted in time zone '
+                            'Europe/Paris; it may be ambiguous or it may not exist.'
+                        ]
+                    )
 
     def test_form_with_ambiguous_time(self):
-        with timezone.override(pytz.timezone('Europe/Paris')):
-            form = EventForm({'dt': '2011-10-30 02:30:00'})
-            self.assertFalse(form.is_valid())
-            self.assertEqual(
-                form.errors['dt'], [
-                    '2011-10-30 02:30:00 couldn’t be interpreted in time zone '
-                    'Europe/Paris; it may be ambiguous or it may not exist.'
-                ]
-            )
+        for tz in get_timezones('Europe/Paris'):
+            with self.subTest(repr(tz)):
+                with timezone.override(tz):
+                    form = EventForm({'dt': '2011-10-30 02:30:00'})
+                    self.assertFalse(form.is_valid())
+                    self.assertEqual(
+                        form.errors['dt'], [
+                            '2011-10-30 02:30:00 couldn’t be interpreted in time zone '
+                            'Europe/Paris; it may be ambiguous or it may not exist.'
+                        ]
+                    )
 
     @requires_tz_support
     def test_split_form(self):

+ 78 - 4
tests/utils_tests/test_timezone.py

@@ -1,14 +1,38 @@
 import datetime
+import unittest
 from unittest import mock
 
 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.utils import timezone
 
 CET = pytz.timezone("Europe/Paris")
 EAT = timezone.get_fixed_timezone(180)      # Africa/Nairobi
 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):
@@ -142,13 +166,21 @@ class TimezoneTests(SimpleTestCase):
         )
 
     def test_make_aware2(self):
-        self.assertEqual(
-            timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), CET),
-            CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)))
+        CEST = datetime.timezone(datetime.timedelta(hours=2), 'CEST')
+        for tz in PARIS_IMPLS:
+            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):
             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(
             timezone.make_naive(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET),
             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'):
             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):
         # 2:30 happens twice, once before DST ends and once after
         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(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):
         # 2:30 never happened due to DST
         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(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):
         self.assertEqual(timezone.get_default_timezone_name(), 'America/Chicago')