Browse Source

Refs #32365 -- Removed support for pytz timezones per deprecation timeline.

Mariusz Felisiak 2 years ago
parent
commit
e6f82438d4

+ 0 - 11
django/conf/__init__.py

@@ -24,12 +24,6 @@ DEFAULT_STORAGE_ALIAS = "default"
 STATICFILES_STORAGE_ALIAS = "staticfiles"
 
 # RemovedInDjango50Warning
-USE_DEPRECATED_PYTZ_DEPRECATED_MSG = (
-    "The USE_DEPRECATED_PYTZ setting, and support for pytz timezones is "
-    "deprecated in favor of the stdlib zoneinfo module. Please update your "
-    "code to use zoneinfo and remove the USE_DEPRECATED_PYTZ setting."
-)
-
 CSRF_COOKIE_MASKED_DEPRECATED_MSG = (
     "The CSRF_COOKIE_MASKED transitional setting is deprecated. Support for "
     "it will be removed in Django 5.0."
@@ -217,9 +211,6 @@ class Settings:
                 setattr(self, setting, setting_value)
                 self._explicit_settings.add(setting)
 
-        if self.is_overridden("USE_DEPRECATED_PYTZ"):
-            warnings.warn(USE_DEPRECATED_PYTZ_DEPRECATED_MSG, RemovedInDjango50Warning)
-
         if self.is_overridden("CSRF_COOKIE_MASKED"):
             warnings.warn(CSRF_COOKIE_MASKED_DEPRECATED_MSG, RemovedInDjango50Warning)
 
@@ -294,8 +285,6 @@ class UserSettingsHolder:
             }
             warnings.warn(STATICFILES_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning)
         super().__setattr__(name, value)
-        if name == "USE_DEPRECATED_PYTZ":
-            warnings.warn(USE_DEPRECATED_PYTZ_DEPRECATED_MSG, RemovedInDjango50Warning)
         # RemovedInDjango51Warning.
         if name == "STORAGES":
             self.STORAGES.setdefault(

+ 0 - 5
django/conf/global_settings.py

@@ -43,11 +43,6 @@ TIME_ZONE = "America/Chicago"
 # If you set this to True, Django will use timezone-aware datetimes.
 USE_TZ = True
 
-# RemovedInDjango50Warning: It's a transitional setting helpful in migrating
-# from pytz tzinfo to ZoneInfo(). Set True to continue using pytz tzinfo
-# objects during the Django 4.x release cycle.
-USE_DEPRECATED_PYTZ = False
-
 # Language code for this installation. All choices can be found here:
 # http://www.i18nguy.com/unicode/language-identifiers.html
 LANGUAGE_CODE = "en-us"

+ 3 - 12
django/contrib/admin/templatetags/admin_list.py

@@ -1,6 +1,5 @@
 import datetime
 
-from django.conf import settings
 from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
 from django.contrib.admin.utils import (
     display_for_field,
@@ -357,10 +356,8 @@ def date_hierarchy(cl):
         field = get_fields_from_path(cl.model, field_name)[-1]
         if isinstance(field, models.DateTimeField):
             dates_or_datetimes = "datetimes"
-            qs_kwargs = {"is_dst": True} if settings.USE_DEPRECATED_PYTZ else {}
         else:
             dates_or_datetimes = "dates"
-            qs_kwargs = {}
         year_field = "%s__year" % field_name
         month_field = "%s__month" % field_name
         day_field = "%s__day" % field_name
@@ -401,9 +398,7 @@ def date_hierarchy(cl):
                 ],
             }
         elif year_lookup and month_lookup:
-            days = getattr(cl.queryset, dates_or_datetimes)(
-                field_name, "day", **qs_kwargs
-            )
+            days = getattr(cl.queryset, dates_or_datetimes)(field_name, "day")
             return {
                 "show": True,
                 "back": {
@@ -425,9 +420,7 @@ def date_hierarchy(cl):
                 ],
             }
         elif year_lookup:
-            months = getattr(cl.queryset, dates_or_datetimes)(
-                field_name, "month", **qs_kwargs
-            )
+            months = getattr(cl.queryset, dates_or_datetimes)(field_name, "month")
             return {
                 "show": True,
                 "back": {"link": link({}), "title": _("All dates")},
@@ -444,9 +437,7 @@ def date_hierarchy(cl):
                 ],
             }
         else:
-            years = getattr(cl.queryset, dates_or_datetimes)(
-                field_name, "year", **qs_kwargs
-            )
+            years = getattr(cl.queryset, dates_or_datetimes)(field_name, "year")
             return {
                 "show": True,
                 "back": None,

+ 1 - 10
django/db/backends/base/base.py

@@ -32,15 +32,6 @@ RAN_DB_VERSION_CHECK = set()
 logger = logging.getLogger("django.db.backends.base")
 
 
-# RemovedInDjango50Warning
-def timezone_constructor(tzname):
-    if settings.USE_DEPRECATED_PYTZ:
-        import pytz
-
-        return pytz.timezone(tzname)
-    return zoneinfo.ZoneInfo(tzname)
-
-
 class BaseDatabaseWrapper:
     """Represent a database connection."""
 
@@ -166,7 +157,7 @@ class BaseDatabaseWrapper:
         elif self.settings_dict["TIME_ZONE"] is None:
             return datetime.timezone.utc
         else:
-            return timezone_constructor(self.settings_dict["TIME_ZONE"])
+            return zoneinfo.ZoneInfo(self.settings_dict["TIME_ZONE"])
 
     @cached_property
     def timezone_name(self):

+ 7 - 3
django/db/backends/sqlite3/_functions.py

@@ -26,7 +26,6 @@ from math import (
 )
 from re import search as re_search
 
-from django.db.backends.base.base import timezone_constructor
 from django.db.backends.utils import (
     split_tzname_delta,
     typecast_time,
@@ -36,6 +35,11 @@ from django.utils import timezone
 from django.utils.crypto import md5
 from django.utils.duration import duration_microseconds
 
+try:
+    import zoneinfo
+except ImportError:
+    from backports import zoneinfo
+
 
 def register(connection):
     create_deterministic_function = functools.partial(
@@ -111,14 +115,14 @@ def _sqlite_datetime_parse(dt, tzname=None, conn_tzname=None):
     except (TypeError, ValueError):
         return None
     if conn_tzname:
-        dt = dt.replace(tzinfo=timezone_constructor(conn_tzname))
+        dt = dt.replace(tzinfo=zoneinfo.ZoneInfo(conn_tzname))
     if tzname is not None and tzname != conn_tzname:
         tzname, sign, offset = split_tzname_delta(tzname)
         if offset:
             hours, minutes = offset.split(":")
             offset_delta = timedelta(hours=int(hours), minutes=int(minutes))
             dt += offset_delta if sign == "+" else -offset_delta
-        dt = timezone.localtime(dt, timezone_constructor(tzname))
+        dt = timezone.localtime(dt, zoneinfo.ZoneInfo(tzname))
     return dt
 
 

+ 1 - 3
django/forms/utils.py

@@ -215,9 +215,7 @@ 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):
+            if 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:

+ 3 - 26
django/templatetags/tz.py

@@ -7,34 +7,12 @@ try:
 except ImportError:
     from backports import zoneinfo
 
-from django.conf import settings
 from django.template import Library, Node, TemplateSyntaxError
 from django.utils import timezone
 
 register = Library()
 
 
-# RemovedInDjango50Warning: shim to allow catching the exception in the calling
-# scope if pytz is not installed.
-class UnknownTimezoneException(BaseException):
-    pass
-
-
-# RemovedInDjango50Warning
-def timezone_constructor(tzname):
-    if settings.USE_DEPRECATED_PYTZ:
-        import pytz
-
-        try:
-            return pytz.timezone(tzname)
-        except pytz.UnknownTimeZoneError:
-            raise UnknownTimezoneException
-    try:
-        return zoneinfo.ZoneInfo(tzname)
-    except zoneinfo.ZoneInfoNotFoundError:
-        raise UnknownTimezoneException
-
-
 # HACK: datetime instances cannot be assigned new attributes. Define a subclass
 # in order to define new attributes in do_timezone().
 class datetimeobject(datetime):
@@ -79,8 +57,7 @@ def do_timezone(value, arg):
         if timezone.is_naive(value):
             default_timezone = timezone.get_default_timezone()
             value = timezone.make_aware(value, default_timezone)
-    # Filters must never raise exceptions, and pytz' exceptions inherit
-    # Exception directly, not a specific subclass. So catch everything.
+    # Filters must never raise exceptionsm, so catch everything.
     except Exception:
         return ""
 
@@ -89,8 +66,8 @@ def do_timezone(value, arg):
         tz = arg
     elif isinstance(arg, str):
         try:
-            tz = timezone_constructor(arg)
-        except UnknownTimezoneException:
+            tz = zoneinfo.ZoneInfo(arg)
+        except zoneinfo.ZoneInfoNotFoundError:
             return ""
     else:
         return ""

+ 6 - 66
django/utils/timezone.py

@@ -3,7 +3,6 @@ Timezone-related classes and functions.
 """
 
 import functools
-import sys
 import warnings
 
 try:
@@ -75,10 +74,6 @@ def get_default_timezone():
 
     This is the time zone defined by settings.TIME_ZONE.
     """
-    if settings.USE_DEPRECATED_PYTZ:
-        import pytz
-
-        return pytz.timezone(settings.TIME_ZONE)
     return zoneinfo.ZoneInfo(settings.TIME_ZONE)
 
 
@@ -125,12 +120,7 @@ def activate(timezone):
     if isinstance(timezone, tzinfo):
         _active.value = timezone
     elif isinstance(timezone, str):
-        if settings.USE_DEPRECATED_PYTZ:
-            import pytz
-
-            _active.value = pytz.timezone(timezone)
-        else:
-            _active.value = zoneinfo.ZoneInfo(timezone)
+        _active.value = zoneinfo.ZoneInfo(timezone)
     else:
         raise ValueError("Invalid timezone: %r" % timezone)
 
@@ -282,15 +272,11 @@ def make_aware(value, timezone=None, is_dst=NOT_PASSED):
         )
     if timezone is None:
         timezone = get_current_timezone()
-    if _is_pytz_zone(timezone):
-        # This method is available for pytz time zones.
-        return timezone.localize(value, is_dst=is_dst)
-    else:
-        # Check that we won't overwrite the timezone of an aware datetime.
-        if is_aware(value):
-            raise ValueError("make_aware expects a naive datetime, got %s" % value)
-        # This may be wrong around DST changes!
-        return value.replace(tzinfo=timezone)
+    # Check that we won't overwrite the timezone of an aware datetime.
+    if is_aware(value):
+        raise ValueError("make_aware expects a naive datetime, got %s" % value)
+    # This may be wrong around DST changes!
+    return value.replace(tzinfo=timezone)
 
 
 def make_naive(value, timezone=None):
@@ -303,53 +289,7 @@ def make_naive(value, timezone=None):
     return value.astimezone(timezone).replace(tzinfo=None)
 
 
-_PYTZ_IMPORTED = False
-
-
-def _pytz_imported():
-    """
-    Detects whether or not pytz has been imported without importing pytz.
-
-    Copied from pytz_deprecation_shim with thanks to Paul Ganssle.
-    """
-    global _PYTZ_IMPORTED
-
-    if not _PYTZ_IMPORTED and "pytz" in sys.modules:
-        _PYTZ_IMPORTED = True
-
-    return _PYTZ_IMPORTED
-
-
-def _is_pytz_zone(tz):
-    """Checks if a zone is a pytz zone."""
-    # See if pytz was already imported rather than checking
-    # settings.USE_DEPRECATED_PYTZ to *allow* manually passing a pytz timezone,
-    # which some of the test cases (at least) rely on.
-    if not _pytz_imported():
-        return False
-
-    # If tz could be pytz, then pytz is needed here.
-    import pytz
-
-    _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 += (type(pytz.UTC),)
-
-    return isinstance(tz, _PYTZ_BASE_CLASSES)
-
-
 def _datetime_ambiguous_or_imaginary(dt, tz):
-    if _is_pytz_zone(tz):
-        import pytz
-
-        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)
 
 

+ 0 - 15
docs/ref/settings.txt

@@ -2849,21 +2849,6 @@ the correct environment.
 
 .. _list of time zones: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
 
-.. setting:: USE_DEPRECATED_PYTZ
-
-``USE_DEPRECATED_PYTZ``
------------------------
-
-Default: ``False``
-
-A boolean that specifies whether to use ``pytz``, rather than :mod:`zoneinfo`,
-as the default time zone implementation.
-
-.. deprecated:: 4.0
-
-    This transitional setting is deprecated. Support for using ``pytz`` will be
-    removed in Django 5.0.
-
 .. setting:: USE_I18N
 
 ``USE_I18N``

+ 2 - 2
docs/releases/4.0.txt

@@ -53,7 +53,7 @@ However, if you are working with non-UTC time zones, and using the ``pytz``
 <DATABASE-TIME_ZONE>` setting, you will need to audit your code, since ``pytz``
 and ``zoneinfo`` are not entirely equivalent.
 
-To give time for such an audit, the transitional :setting:`USE_DEPRECATED_PYTZ`
+To give time for such an audit, the transitional ``USE_DEPRECATED_PYTZ``
 setting allows continued use of ``pytz`` during the 4.x release cycle. This
 setting will be removed in Django 5.0.
 
@@ -62,7 +62,7 @@ author, can be used to assist with the migration from ``pytz``. This package
 provides shims to help you safely remove ``pytz``, and has a detailed
 `migration guide`_ showing how to move to the new ``zoneinfo`` APIs.
 
-Using `pytz_deprecation_shim`_ and the :setting:`USE_DEPRECATED_PYTZ`
+Using `pytz_deprecation_shim`_ and the ``USE_DEPRECATED_PYTZ``
 transitional setting is recommended if you need a gradual update path.
 
 .. _pytz_deprecation_shim: https://pytz-deprecation-shim.readthedocs.io/en/latest/index.html

+ 4 - 0
docs/releases/5.0.txt

@@ -276,6 +276,10 @@ to remove usage of these features.
 
 * The ``USE_L10N`` setting is removed.
 
+* The ``USE_DEPRECATED_PYTZ`` transitional setting is removed.
+
+* Support for ``pytz`` timezones is removed.
+
 See :ref:`deprecated-features-4.1` for details on these changes, including how
 to remove usage of these features.
 

+ 0 - 2
docs/topics/i18n/timezones.txt

@@ -677,5 +677,3 @@ Usage
    :func:`zoneinfo.available_timezones` provides the set of all valid keys for
    IANA time zones available to your system. See the docs for usage
    considerations.
-
-.. _pytz: http://pytz.sourceforge.net/

+ 0 - 8
tests/admin_views/tests.py

@@ -10,11 +10,6 @@ try:
 except ImportError:
     from backports import zoneinfo
 
-try:
-    import pytz
-except ImportError:
-    pytz = None
-
 from django.contrib import admin
 from django.contrib.admin import AdminSite, ModelAdmin
 from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
@@ -158,9 +153,6 @@ def make_aware_datetimes(dt, iana_key):
     """Makes one aware datetime for each supported time zone provider."""
     yield dt.replace(tzinfo=zoneinfo.ZoneInfo(iana_key))
 
-    if pytz is not None:
-        yield pytz.timezone(iana_key).localize(dt, is_dst=None)
-
 
 class AdminFieldExtractionMixin:
     """

+ 1 - 48
tests/datetimes/tests.py

@@ -1,14 +1,7 @@
 import datetime
-import unittest
 
-try:
-    import pytz
-except ImportError:
-    pytz = None
-
-from django.test import TestCase, ignore_warnings, override_settings
+from django.test import TestCase, override_settings
 from django.utils import timezone
-from django.utils.deprecation import RemovedInDjango50Warning
 
 from .models import Article, Category, Comment
 
@@ -102,46 +95,6 @@ class DateTimesTests(TestCase):
         qs = Article.objects.datetimes("pub_date", "second")
         self.assertEqual(qs[0], now)
 
-    @unittest.skipUnless(pytz is not None, "Test requires pytz")
-    @ignore_warnings(category=RemovedInDjango50Warning)
-    @override_settings(USE_TZ=True, TIME_ZONE="UTC", USE_DEPRECATED_PYTZ=True)
-    def test_datetimes_ambiguous_and_invalid_times(self):
-        sao = pytz.timezone("America/Sao_Paulo")
-        utc = pytz.UTC
-        article = Article.objects.create(
-            title="Article 1",
-            pub_date=utc.localize(datetime.datetime(2016, 2, 21, 1)),
-        )
-        Comment.objects.create(
-            article=article,
-            pub_date=utc.localize(datetime.datetime(2016, 10, 16, 13)),
-        )
-        with timezone.override(sao):
-            with self.assertRaisesMessage(
-                pytz.AmbiguousTimeError, "2016-02-20 23:00:00"
-            ):
-                Article.objects.datetimes("pub_date", "hour").get()
-            with self.assertRaisesMessage(
-                pytz.NonExistentTimeError, "2016-10-16 00:00:00"
-            ):
-                Comment.objects.datetimes("pub_date", "day").get()
-            self.assertEqual(
-                Article.objects.datetimes("pub_date", "hour", is_dst=False).get().dst(),
-                datetime.timedelta(0),
-            )
-            self.assertEqual(
-                Comment.objects.datetimes("pub_date", "day", is_dst=False).get().dst(),
-                datetime.timedelta(0),
-            )
-            self.assertEqual(
-                Article.objects.datetimes("pub_date", "hour", is_dst=True).get().dst(),
-                datetime.timedelta(0, 3600),
-            )
-            self.assertEqual(
-                Comment.objects.datetimes("pub_date", "hour", is_dst=True).get().dst(),
-                datetime.timedelta(0, 3600),
-            )
-
     def test_datetimes_returns_available_dates_for_given_scope_and_given_field(self):
         pub_dates = [
             datetime.datetime(2005, 7, 28, 12, 15),

+ 212 - 292
tests/db_functions/datetime/test_extract_trunc.py

@@ -1,4 +1,3 @@
-import unittest
 from datetime import datetime, timedelta
 from datetime import timezone as datetime_timezone
 
@@ -7,11 +6,6 @@ try:
 except ImportError:
     from backports import zoneinfo
 
-try:
-    import pytz
-except ImportError:
-    pytz = None
-
 from django.conf import settings
 from django.db import DataError, OperationalError
 from django.db.models import (
@@ -51,29 +45,14 @@ from django.db.models.functions import (
 )
 from django.test import (
     TestCase,
-    ignore_warnings,
     override_settings,
     skipIfDBFeature,
     skipUnlessDBFeature,
 )
 from django.utils import timezone
-from django.utils.deprecation import RemovedInDjango50Warning
 
 from ..models import Author, DTModel, Fan
 
-HAS_PYTZ = pytz is not None
-if not HAS_PYTZ:
-    needs_pytz = unittest.skip("Test requires pytz")
-else:
-
-    def needs_pytz(f):
-        return f
-
-
-ZONE_CONSTRUCTORS = (zoneinfo.ZoneInfo,)
-if HAS_PYTZ:
-    ZONE_CONSTRUCTORS += (pytz.timezone,)
-
 
 def truncate_to(value, kind, tzinfo=None):
     # Convert to target timezone before truncation
@@ -1690,10 +1669,6 @@ 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)
@@ -1702,62 +1677,57 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
         self.create_model(start_datetime, end_datetime)
         delta_tzinfo_pos = datetime_timezone(timedelta(hours=5))
         delta_tzinfo_neg = datetime_timezone(timedelta(hours=-5, minutes=17))
+        melb = zoneinfo.ZoneInfo("Australia/Melbourne")
 
-        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)
+        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_with_timezone_minus_no_offset(self):
         start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
@@ -1765,22 +1735,22 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
         start_datetime = timezone.make_aware(start_datetime)
         end_datetime = timezone.make_aware(end_datetime)
         self.create_model(start_datetime, end_datetime)
-        for ust_nera in self.get_timezones("Asia/Ust-Nera"):
-            with self.subTest(repr(ust_nera)):
-                qs = DTModel.objects.annotate(
-                    hour=ExtractHour("start_datetime"),
-                    hour_tz=ExtractHour("start_datetime", tzinfo=ust_nera),
-                ).order_by("start_datetime")
+        ust_nera = zoneinfo.ZoneInfo("Asia/Ust-Nera")
 
-                utc_model = qs.get()
-                self.assertEqual(utc_model.hour, 23)
-                self.assertEqual(utc_model.hour_tz, 9)
+        qs = DTModel.objects.annotate(
+            hour=ExtractHour("start_datetime"),
+            hour_tz=ExtractHour("start_datetime", tzinfo=ust_nera),
+        ).order_by("start_datetime")
+
+        utc_model = qs.get()
+        self.assertEqual(utc_model.hour, 23)
+        self.assertEqual(utc_model.hour_tz, 9)
 
-                with timezone.override(ust_nera):
-                    ust_nera_model = qs.get()
+        with timezone.override(ust_nera):
+            ust_nera_model = qs.get()
 
-                self.assertEqual(ust_nera_model.hour, 9)
-                self.assertEqual(ust_nera_model.hour_tz, 9)
+        self.assertEqual(ust_nera_model.hour, 9)
+        self.assertEqual(ust_nera_model.hour_tz, 9)
 
     def test_extract_func_explicit_timezone_priority(self):
         start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
@@ -1788,35 +1758,32 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
         start_datetime = timezone.make_aware(start_datetime)
         end_datetime = timezone.make_aware(end_datetime)
         self.create_model(start_datetime, end_datetime)
-
-        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=datetime_timezone.utc
-                            ),
-                        )
-                        .order_by("start_datetime")
-                        .get()
-                    )
-                    self.assertEqual(model.day_melb, 16)
-                    self.assertEqual(model.day_utc, 15)
+        melb = zoneinfo.ZoneInfo("Australia/Melbourne")
+        with timezone.override(melb):
+            model = (
+                DTModel.objects.annotate(
+                    day_melb=Extract("start_datetime", "day"),
+                    day_utc=Extract(
+                        "start_datetime", "day", tzinfo=datetime_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):
-        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()
+        melb = zoneinfo.ZoneInfo("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()
 
     def test_trunc_timezone_applied_before_truncation(self):
         start_datetime = datetime(2016, 1, 1, 1, 30, 50, 321)
@@ -1824,74 +1791,36 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
         start_datetime = timezone.make_aware(start_datetime)
         end_datetime = timezone.make_aware(end_datetime)
         self.create_model(start_datetime, end_datetime)
+        melb = zoneinfo.ZoneInfo("Australia/Melbourne")
+        pacific = zoneinfo.ZoneInfo("America/Los_Angeles")
 
-        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()
-                )
+        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())
-
-    @needs_pytz
-    @ignore_warnings(category=RemovedInDjango50Warning)
-    def test_trunc_ambiguous_and_invalid_times(self):
-        sao = pytz.timezone("America/Sao_Paulo")
-        start_datetime = datetime(2016, 10, 16, 13, tzinfo=datetime_timezone.utc)
-        end_datetime = datetime(2016, 2, 21, 1, tzinfo=datetime_timezone.utc)
-        self.create_model(start_datetime, end_datetime)
-        with timezone.override(sao):
-            with self.assertRaisesMessage(
-                pytz.NonExistentTimeError, "2016-10-16 00:00:00"
-            ):
-                model = DTModel.objects.annotate(
-                    truncated_start=TruncDay("start_datetime")
-                ).get()
-            with self.assertRaisesMessage(
-                pytz.AmbiguousTimeError, "2016-02-20 23:00:00"
-            ):
-                model = DTModel.objects.annotate(
-                    truncated_end=TruncHour("end_datetime")
-                ).get()
-            model = DTModel.objects.annotate(
-                truncated_start=TruncDay("start_datetime", is_dst=False),
-                truncated_end=TruncHour("end_datetime", is_dst=False),
-            ).get()
-            self.assertEqual(model.truncated_start.dst(), timedelta(0))
-            self.assertEqual(model.truncated_end.dst(), timedelta(0))
-            model = DTModel.objects.annotate(
-                truncated_start=TruncDay("start_datetime", is_dst=True),
-                truncated_end=TruncHour("end_datetime", is_dst=True),
-            ).get()
-            self.assertEqual(model.truncated_start.dst(), timedelta(0, 3600))
-            self.assertEqual(model.truncated_end.dst(), timedelta(0, 3600))
+        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_func_with_timezone(self):
         """
@@ -1904,118 +1833,109 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
         end_datetime = timezone.make_aware(end_datetime)
         self.create_model(start_datetime, end_datetime)
         self.create_model(end_datetime, start_datetime)
+        melb = zoneinfo.ZoneInfo("Australia/Melbourne")
 
-        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_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),
-                    )
+        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),
+        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")
-                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")
+        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)
+        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):
-        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()
+        melb = zoneinfo.ZoneInfo("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()

+ 0 - 15
tests/migrations/test_writer.py

@@ -15,11 +15,6 @@ try:
 except ImportError:
     from backports import zoneinfo
 
-try:
-    import pytz
-except ImportError:
-    pytz = None
-
 import custom_migration_operations.more_operations
 import custom_migration_operations.operations
 
@@ -595,16 +590,6 @@ class WriterTests(SimpleTestCase):
                 {"import datetime"},
             ),
         )
-        if pytz:
-            self.assertSerializedResultEqual(
-                pytz.timezone("Europe/Paris").localize(
-                    datetime.datetime(2012, 1, 1, 2, 1)
-                ),
-                (
-                    "datetime.datetime(2012, 1, 1, 1, 1, tzinfo=datetime.timezone.utc)",
-                    {"import datetime"},
-                ),
-            )
 
     def test_serialize_fields(self):
         self.assertSerializedFieldEqual(models.CharField(max_length=255))

+ 0 - 1
tests/requirements/py3.txt

@@ -12,7 +12,6 @@ Pillow >= 6.2.1; sys.platform != 'win32' or python_version < '3.12'
 # pylibmc/libmemcached can't be built on Windows.
 pylibmc; sys.platform != 'win32'
 pymemcache >= 3.4.0
-pytz
 pywatchman; sys.platform != 'win32'
 PyYAML
 redis >= 3.4.0

+ 1 - 26
tests/settings_tests/tests.py

@@ -4,13 +4,7 @@ import unittest
 from types import ModuleType, SimpleNamespace
 from unittest import mock
 
-from django.conf import (
-    ENVIRONMENT_VARIABLE,
-    USE_DEPRECATED_PYTZ_DEPRECATED_MSG,
-    LazySettings,
-    Settings,
-    settings,
-)
+from django.conf import ENVIRONMENT_VARIABLE, LazySettings, Settings, settings
 from django.core.exceptions import ImproperlyConfigured
 from django.http import HttpRequest
 from django.test import (
@@ -23,7 +17,6 @@ from django.test import (
 )
 from django.test.utils import requires_tz_support
 from django.urls import clear_script_prefix, set_script_prefix
-from django.utils.deprecation import RemovedInDjango50Warning
 
 
 @modify_settings(ITEMS={"prepend": ["b"], "append": ["d"], "remove": ["a", "e"]})
@@ -348,24 +341,6 @@ class SettingsTests(SimpleTestCase):
         with self.assertRaisesMessage(ValueError, "Incorrect timezone setting: test"):
             settings._setup()
 
-    def test_use_deprecated_pytz_deprecation(self):
-        settings_module = ModuleType("fake_settings_module")
-        settings_module.USE_DEPRECATED_PYTZ = True
-        sys.modules["fake_settings_module"] = settings_module
-        try:
-            with self.assertRaisesMessage(
-                RemovedInDjango50Warning, USE_DEPRECATED_PYTZ_DEPRECATED_MSG
-            ):
-                Settings("fake_settings_module")
-        finally:
-            del sys.modules["fake_settings_module"]
-
-        holder = LazySettings()
-        with self.assertRaisesMessage(
-            RemovedInDjango50Warning, USE_DEPRECATED_PYTZ_DEPRECATED_MSG
-        ):
-            holder.configure(USE_DEPRECATED_PYTZ=True)
-
 
 class TestComplexSettingOverride(SimpleTestCase):
     def setUp(self):

+ 69 - 131
tests/timezones/tests.py

@@ -10,11 +10,6 @@ try:
 except ImportError:
     from backports import zoneinfo
 
-try:
-    import pytz
-except ImportError:
-    pytz = None
-
 from django.contrib.auth.models import User
 from django.core import serializers
 from django.db import connection
@@ -31,7 +26,6 @@ from django.test import (
     SimpleTestCase,
     TestCase,
     TransactionTestCase,
-    ignore_warnings,
     override_settings,
     skipIfDBFeature,
     skipUnlessDBFeature,
@@ -79,14 +73,6 @@ UTC = datetime.timezone.utc
 EAT = timezone.get_fixed_timezone(180)  # Africa/Nairobi
 ICT = timezone.get_fixed_timezone(420)  # Asia/Bangkok
 
-ZONE_CONSTRUCTORS = (zoneinfo.ZoneInfo,)
-if pytz is not None:
-    ZONE_CONSTRUCTORS += (pytz.timezone,)
-
-
-def get_timezones(key):
-    return [constructor(key) for constructor in ZONE_CONSTRUCTORS]
-
 
 class UTCAliasTests(SimpleTestCase):
     def test_alias_deprecation_warning(self):
@@ -413,39 +399,17 @@ class NewDatabaseTests(TestCase):
         self.assertEqual(Event.objects.filter(dt__gte=dt2).count(), 1)
         self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0)
 
-    def test_query_filter_with_pytz_timezones(self):
-        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
-                )
-
-    @ignore_warnings(category=RemovedInDjango50Warning)
-    def test_connection_timezone(self):
-        tests = [
-            (False, None, datetime.timezone),
-            (False, "Africa/Nairobi", zoneinfo.ZoneInfo),
-        ]
-        if pytz is not None:
-            tests += [
-                (True, None, datetime.timezone),
-                (True, "Africa/Nairobi", pytz.BaseTzInfo),
-            ]
-        for use_pytz, connection_tz, expected_type in tests:
-            with self.subTest(use_pytz=use_pytz, connection_tz=connection_tz):
-                with self.settings(USE_DEPRECATED_PYTZ=use_pytz):
-                    with override_database_connection_timezone(connection_tz):
-                        self.assertIsInstance(connection.timezone, expected_type)
+    def test_query_filter_with_timezones(self):
+        tz = zoneinfo.ZoneInfo("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)
 
     def test_query_convert_timezones(self):
         # Connection timezone is equal to the current timezone, datetime
@@ -1075,16 +1039,15 @@ class TemplateTests(SimpleTestCase):
             )
 
         # 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": tz,
-                    }
-                )
-                self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
+        tz = zoneinfo.ZoneInfo("Europe/Paris")
+        tpl = Template("{% load tz %}{{ dt|timezone:tz }}")
+        ctx = Context(
+            {
+                "dt": datetime.datetime(2011, 9, 1, 13, 20, 30),
+                "tz": tz,
+            }
+        )
+        self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
 
     def test_localtime_templatetag_invalid_argument(self):
         with self.assertRaises(TemplateSyntaxError):
@@ -1147,15 +1110,14 @@ class TemplateTests(SimpleTestCase):
         tpl = Template("{% load tz %}{% timezone tz %}{{ dt }}{% endtimezone %}")
 
         # 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")
+        tz = zoneinfo.ZoneInfo("Europe/Paris")
+        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 IANA timezone name as argument
         ctx = Context(
@@ -1166,22 +1128,6 @@ class TemplateTests(SimpleTestCase):
         )
         self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
 
-    @ignore_warnings(category=RemovedInDjango50Warning)
-    def test_timezone_templatetag_invalid_argument(self):
-        with self.assertRaises(TemplateSyntaxError):
-            Template("{% load tz %}{% timezone %}{% endtimezone %}").render()
-        with self.assertRaises(zoneinfo.ZoneInfoNotFoundError):
-            Template("{% load tz %}{% timezone tz %}{% endtimezone %}").render(
-                Context({"tz": "foobar"})
-            )
-        if pytz is not None:
-            with override_settings(USE_DEPRECATED_PYTZ=True), self.assertRaises(
-                pytz.UnknownTimeZoneError
-            ):
-                Template("{% load tz %}{% timezone tz %}{% endtimezone %}").render(
-                    Context({"tz": "foobar"})
-                )
-
     @skipIf(sys.platform == "win32", "Windows uses non-standard time zone names")
     def test_get_current_timezone_templatetag(self):
         """
@@ -1205,16 +1151,12 @@ class TemplateTests(SimpleTestCase):
             self.assertEqual(tpl.render(Context({"tz": ICT})), "+0700")
 
     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 }}"
         )
-        for tz in get_timezones("Europe/Paris"):
-            with self.subTest(repr(tz)):
-                with timezone.override(tz):
-                    self.assertEqual(tpl.render(Context()), "Europe/Paris")
+        tz = zoneinfo.ZoneInfo("Europe/Paris")
+        with timezone.override(tz):
+            self.assertEqual(tpl.render(Context()), "Europe/Paris")
 
         tpl = Template(
             "{% load tz %}{% timezone 'Europe/Paris' %}"
@@ -1282,27 +1224,25 @@ class LegacyFormsTests(TestCase):
 
     def test_form_with_non_existent_time(self):
         form = EventForm({"dt": "2011-03-27 02:30:00"})
-        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),
-                    )
+        tz = zoneinfo.ZoneInfo("Europe/Paris")
+        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"})
-        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),
-                    )
+        tz = zoneinfo.ZoneInfo("Europe/Paris")
+        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"})
@@ -1338,32 +1278,30 @@ class NewFormsTests(TestCase):
             )
 
     def test_form_with_non_existent_time(self):
-        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."
-                        ],
-                    )
+        tz = zoneinfo.ZoneInfo("Europe/Paris")
+        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):
-        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."
-                        ],
-                    )
+        tz = zoneinfo.ZoneInfo("Europe/Paris")
+        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):

+ 1 - 2
tests/utils_tests/test_dateformat.py

@@ -25,8 +25,7 @@ class DateFormatTests(SimpleTestCase):
         self.assertEqual(datetime.fromtimestamp(int(format(dt, "U"))), dt)
 
     def test_naive_ambiguous_datetime(self):
-        # dt is ambiguous in Europe/Copenhagen. pytz raises an exception for
-        # the ambiguity, which results in an empty string.
+        # dt is ambiguous in Europe/Copenhagen.
         dt = datetime(2015, 10, 25, 2, 30, 0)
 
         # Try all formatters that involve self.timezone.

+ 5 - 105
tests/utils_tests/test_timezone.py

@@ -1,18 +1,12 @@
 import datetime
-import unittest
 from unittest import mock
 
-try:
-    import pytz
-except ImportError:
-    pytz = None
-
 try:
     import zoneinfo
 except ImportError:
     from backports import zoneinfo
 
-from django.test import SimpleTestCase, ignore_warnings, override_settings
+from django.test import SimpleTestCase, override_settings
 from django.utils import timezone
 from django.utils.deprecation import RemovedInDjango50Warning
 
@@ -21,38 +15,11 @@ EAT = timezone.get_fixed_timezone(180)  # Africa/Nairobi
 ICT = timezone.get_fixed_timezone(420)  # Asia/Bangkok
 UTC = datetime.timezone.utc
 
-HAS_PYTZ = pytz is not None
-if not HAS_PYTZ:
-    CET = None
-    PARIS_IMPLS = (PARIS_ZI,)
-
-    needs_pytz = unittest.skip("Test requires pytz")
-else:
-    CET = pytz.timezone("Europe/Paris")
-    PARIS_IMPLS = (PARIS_ZI, CET)
-
-    def needs_pytz(f):
-        return f
-
 
 class TimezoneTests(SimpleTestCase):
-    def setUp(self):
-        # RemovedInDjango50Warning
-        timezone.get_default_timezone.cache_clear()
-
-    def tearDown(self):
-        # RemovedInDjango50Warning
-        timezone.get_default_timezone.cache_clear()
-
     def test_default_timezone_is_zoneinfo(self):
         self.assertIsInstance(timezone.get_default_timezone(), zoneinfo.ZoneInfo)
 
-    @needs_pytz
-    @ignore_warnings(category=RemovedInDjango50Warning)
-    @override_settings(USE_DEPRECATED_PYTZ=True)
-    def test_setting_allows_fallback_to_pytz(self):
-        self.assertIsInstance(timezone.get_default_timezone(), pytz.BaseTzInfo)
-
     def test_now(self):
         with override_settings(USE_TZ=True):
             self.assertTrue(timezone.is_aware(timezone.now()))
@@ -208,46 +175,15 @@ class TimezoneTests(SimpleTestCase):
 
     def test_make_aware2(self):
         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),
-                )
-
-        if HAS_PYTZ:
-            with self.assertRaises(ValueError):
-                timezone.make_aware(
-                    CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET
-                )
-
+        self.assertEqual(
+            timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), PARIS_ZI),
+            datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=CEST),
+        )
         with self.assertRaises(ValueError):
             timezone.make_aware(
                 datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=PARIS_ZI), PARIS_ZI
             )
 
-    @needs_pytz
-    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),
-        )
-        self.assertEqual(
-            timezone.make_naive(
-                pytz.timezone("Asia/Bangkok").localize(
-                    datetime.datetime(2011, 9, 1, 17, 20, 30)
-                ),
-                CET,
-            ),
-            datetime.datetime(2011, 9, 1, 12, 20, 30),
-        )
-        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)
-
     def test_make_naive_zoneinfo(self):
         self.assertEqual(
             timezone.make_naive(
@@ -264,21 +200,6 @@ class TimezoneTests(SimpleTestCase):
             datetime.datetime(2011, 9, 1, 12, 20, 30, fold=1),
         )
 
-    @needs_pytz
-    @ignore_warnings(category=RemovedInDjango50Warning)
-    def test_make_aware_pytz_ambiguous(self):
-        # 2:30 happens twice, once before DST ends and once after
-        ambiguous = datetime.datetime(2015, 10, 25, 2, 30)
-
-        with self.assertRaises(pytz.AmbiguousTimeError):
-            timezone.make_aware(ambiguous, timezone=CET)
-
-        std = timezone.make_aware(ambiguous, timezone=CET, is_dst=False)
-        dst = timezone.make_aware(ambiguous, timezone=CET, is_dst=True)
-        self.assertEqual(std - dst, datetime.timedelta(hours=1))
-        self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
-        self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
-
     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)
@@ -292,21 +213,6 @@ class TimezoneTests(SimpleTestCase):
         self.assertEqual(std.utcoffset(), datetime.timedelta(hours=1))
         self.assertEqual(dst.utcoffset(), datetime.timedelta(hours=2))
 
-    @needs_pytz
-    @ignore_warnings(category=RemovedInDjango50Warning)
-    def test_make_aware_pytz_non_existent(self):
-        # 2:30 never happened due to DST
-        non_existent = datetime.datetime(2015, 3, 29, 2, 30)
-
-        with self.assertRaises(pytz.NonExistentTimeError):
-            timezone.make_aware(non_existent, timezone=CET)
-
-        std = timezone.make_aware(non_existent, timezone=CET, is_dst=False)
-        dst = timezone.make_aware(non_existent, timezone=CET, is_dst=True)
-        self.assertEqual(std - dst, datetime.timedelta(hours=1))
-        self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
-        self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
-
     def test_make_aware_zoneinfo_non_existent(self):
         # 2:30 never happened due to DST
         non_existent = datetime.datetime(2015, 3, 29, 2, 30)
@@ -349,12 +255,6 @@ class TimezoneTests(SimpleTestCase):
             (zoneinfo.ZoneInfo("Europe/Madrid"), "Europe/Madrid"),
             (zoneinfo.ZoneInfo("Etc/GMT-10"), "+10"),
         ]
-        if HAS_PYTZ:
-            tests += [
-                # pytz, named and fixed offset.
-                (pytz.timezone("Europe/Madrid"), "Europe/Madrid"),
-                (pytz.timezone("Etc/GMT-10"), "+10"),
-            ]
         for tz, expected in tests:
             with self.subTest(tz=tz, expected=expected):
                 self.assertEqual(timezone._get_timezone_name(tz), expected)