diff --git a/django/conf/__init__.py b/django/conf/__init__.py index 3d1d65c44d..ea63a0dfb2 100644 --- a/django/conf/__init__.py +++ b/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( diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 411a5a4fe2..6d4ea3db5c 100644 --- a/django/conf/global_settings.py +++ b/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" diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 5865843dce..ee6f3a7666 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/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, diff --git a/django/db/backends/base/base.py b/django/db/backends/base/base.py index 3b845ec9b3..5f2e7bcd4d 100644 --- a/django/db/backends/base/base.py +++ b/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): diff --git a/django/db/backends/sqlite3/_functions.py b/django/db/backends/sqlite3/_functions.py index b590111ec5..c60549f8af 100644 --- a/django/db/backends/sqlite3/_functions.py +++ b/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 diff --git a/django/forms/utils.py b/django/forms/utils.py index 905babce4d..7577c0bbda 100644 --- a/django/forms/utils.py +++ b/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: diff --git a/django/templatetags/tz.py b/django/templatetags/tz.py index cb7d22e5f2..92240b2a39 100644 --- a/django/templatetags/tz.py +++ b/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 "" diff --git a/django/utils/timezone.py b/django/utils/timezone.py index f3eac0e7b2..73813fa20e 100644 --- a/django/utils/timezone.py +++ b/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) diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index f315e5ef6c..109cc887f7 100644 --- a/docs/ref/settings.txt +++ b/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`` diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index d64e74941c..74dd4dbb93 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -53,7 +53,7 @@ However, if you are working with non-UTC time zones, and using the ``pytz`` ` 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 diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt index 07ab33c439..f137c27398 100644 --- a/docs/releases/5.0.txt +++ b/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. diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt index c0063eb12d..10170fcdea 100644 --- a/docs/topics/i18n/timezones.txt +++ b/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/ diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 4d1506d257..f7764a7f36 100644 --- a/tests/admin_views/tests.py +++ b/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: """ diff --git a/tests/datetimes/tests.py b/tests/datetimes/tests.py index 806c28d581..3a1301a031 100644 --- a/tests/datetimes/tests.py +++ b/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), diff --git a/tests/db_functions/datetime/test_extract_trunc.py b/tests/db_functions/datetime/test_extract_trunc.py index b2327931f0..80043fe3f4 100644 --- a/tests/db_functions/datetime/test_extract_trunc.py +++ b/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") + 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) + 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() + 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) + 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") - with timezone.override(ust_nera): - ust_nera_model = qs.get() + utc_model = qs.get() + self.assertEqual(utc_model.hour, 23) + self.assertEqual(utc_model.hour_tz, 9) - self.assertEqual(ust_nera_model.hour, 9) - self.assertEqual(ust_nera_model.hour_tz, 9) + 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) 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() diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 2a965ef7e0..ce1c024b13 100644 --- a/tests/migrations/test_writer.py +++ b/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)) diff --git a/tests/requirements/py3.txt b/tests/requirements/py3.txt index f80bf8b2a7..397aaa06b1 100644 --- a/tests/requirements/py3.txt +++ b/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 diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index 32a9c6cc19..d6666537de 100644 --- a/tests/settings_tests/tests.py +++ b/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): diff --git a/tests/timezones/tests.py b/tests/timezones/tests.py index f5c8d17a3c..89e87172f1 100644 --- a/tests/timezones/tests.py +++ b/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): diff --git a/tests/utils_tests/test_dateformat.py b/tests/utils_tests/test_dateformat.py index 9563790b20..dce678e172 100644 --- a/tests/utils_tests/test_dateformat.py +++ b/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. diff --git a/tests/utils_tests/test_timezone.py b/tests/utils_tests/test_timezone.py index ed8386945d..6ec8828561 100644 --- a/tests/utils_tests/test_timezone.py +++ b/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)