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

This commit is contained in:
Mariusz Felisiak 2023-01-09 09:03:38 +01:00
parent 8d98f99a4a
commit e6f82438d4
21 changed files with 314 additions and 781 deletions

View File

@ -24,12 +24,6 @@ DEFAULT_STORAGE_ALIAS = "default"
STATICFILES_STORAGE_ALIAS = "staticfiles" STATICFILES_STORAGE_ALIAS = "staticfiles"
# RemovedInDjango50Warning # 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 = ( CSRF_COOKIE_MASKED_DEPRECATED_MSG = (
"The CSRF_COOKIE_MASKED transitional setting is deprecated. Support for " "The CSRF_COOKIE_MASKED transitional setting is deprecated. Support for "
"it will be removed in Django 5.0." "it will be removed in Django 5.0."
@ -217,9 +211,6 @@ class Settings:
setattr(self, setting, setting_value) setattr(self, setting, setting_value)
self._explicit_settings.add(setting) 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"): if self.is_overridden("CSRF_COOKIE_MASKED"):
warnings.warn(CSRF_COOKIE_MASKED_DEPRECATED_MSG, RemovedInDjango50Warning) warnings.warn(CSRF_COOKIE_MASKED_DEPRECATED_MSG, RemovedInDjango50Warning)
@ -294,8 +285,6 @@ class UserSettingsHolder:
} }
warnings.warn(STATICFILES_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning) warnings.warn(STATICFILES_STORAGE_DEPRECATED_MSG, RemovedInDjango51Warning)
super().__setattr__(name, value) super().__setattr__(name, value)
if name == "USE_DEPRECATED_PYTZ":
warnings.warn(USE_DEPRECATED_PYTZ_DEPRECATED_MSG, RemovedInDjango50Warning)
# RemovedInDjango51Warning. # RemovedInDjango51Warning.
if name == "STORAGES": if name == "STORAGES":
self.STORAGES.setdefault( self.STORAGES.setdefault(

View File

@ -43,11 +43,6 @@ TIME_ZONE = "America/Chicago"
# If you set this to True, Django will use timezone-aware datetimes. # If you set this to True, Django will use timezone-aware datetimes.
USE_TZ = True 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: # Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html # http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"

View File

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

View File

@ -32,15 +32,6 @@ RAN_DB_VERSION_CHECK = set()
logger = logging.getLogger("django.db.backends.base") 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: class BaseDatabaseWrapper:
"""Represent a database connection.""" """Represent a database connection."""
@ -166,7 +157,7 @@ class BaseDatabaseWrapper:
elif self.settings_dict["TIME_ZONE"] is None: elif self.settings_dict["TIME_ZONE"] is None:
return datetime.timezone.utc return datetime.timezone.utc
else: else:
return timezone_constructor(self.settings_dict["TIME_ZONE"]) return zoneinfo.ZoneInfo(self.settings_dict["TIME_ZONE"])
@cached_property @cached_property
def timezone_name(self): def timezone_name(self):

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ Timezone-related classes and functions.
""" """
import functools import functools
import sys
import warnings import warnings
try: try:
@ -75,10 +74,6 @@ def get_default_timezone():
This is the time zone defined by settings.TIME_ZONE. 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) return zoneinfo.ZoneInfo(settings.TIME_ZONE)
@ -125,11 +120,6 @@ def activate(timezone):
if isinstance(timezone, tzinfo): if isinstance(timezone, tzinfo):
_active.value = timezone _active.value = timezone
elif isinstance(timezone, str): 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: else:
raise ValueError("Invalid timezone: %r" % timezone) raise ValueError("Invalid timezone: %r" % timezone)
@ -282,10 +272,6 @@ def make_aware(value, timezone=None, is_dst=NOT_PASSED):
) )
if timezone is None: if timezone is None:
timezone = get_current_timezone() 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. # Check that we won't overwrite the timezone of an aware datetime.
if is_aware(value): if is_aware(value):
raise ValueError("make_aware expects a naive datetime, got %s" % value) raise ValueError("make_aware expects a naive datetime, got %s" % value)
@ -303,53 +289,7 @@ def make_naive(value, timezone=None):
return value.astimezone(timezone).replace(tzinfo=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): 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) return tz.utcoffset(dt.replace(fold=not dt.fold)) != tz.utcoffset(dt)

View File

@ -2849,21 +2849,6 @@ the correct environment.
.. _list of time zones: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones .. _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 .. setting:: USE_I18N
``USE_I18N`` ``USE_I18N``

View File

@ -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`` <DATABASE-TIME_ZONE>` setting, you will need to audit your code, since ``pytz``
and ``zoneinfo`` are not entirely equivalent. 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 allows continued use of ``pytz`` during the 4.x release cycle. This
setting will be removed in Django 5.0. 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 provides shims to help you safely remove ``pytz``, and has a detailed
`migration guide`_ showing how to move to the new ``zoneinfo`` APIs. `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. transitional setting is recommended if you need a gradual update path.
.. _pytz_deprecation_shim: https://pytz-deprecation-shim.readthedocs.io/en/latest/index.html .. _pytz_deprecation_shim: https://pytz-deprecation-shim.readthedocs.io/en/latest/index.html

View File

@ -276,6 +276,10 @@ to remove usage of these features.
* The ``USE_L10N`` setting is removed. * 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 See :ref:`deprecated-features-4.1` for details on these changes, including how
to remove usage of these features. to remove usage of these features.

View File

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

View File

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

View File

@ -1,14 +1,7 @@
import datetime import datetime
import unittest
try: from django.test import TestCase, override_settings
import pytz
except ImportError:
pytz = None
from django.test import TestCase, ignore_warnings, override_settings
from django.utils import timezone from django.utils import timezone
from django.utils.deprecation import RemovedInDjango50Warning
from .models import Article, Category, Comment from .models import Article, Category, Comment
@ -102,46 +95,6 @@ class DateTimesTests(TestCase):
qs = Article.objects.datetimes("pub_date", "second") qs = Article.objects.datetimes("pub_date", "second")
self.assertEqual(qs[0], now) 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): def test_datetimes_returns_available_dates_for_given_scope_and_given_field(self):
pub_dates = [ pub_dates = [
datetime.datetime(2005, 7, 28, 12, 15), datetime.datetime(2005, 7, 28, 12, 15),

View File

@ -1,4 +1,3 @@
import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from datetime import timezone as datetime_timezone from datetime import timezone as datetime_timezone
@ -7,11 +6,6 @@ try:
except ImportError: except ImportError:
from backports import zoneinfo from backports import zoneinfo
try:
import pytz
except ImportError:
pytz = None
from django.conf import settings from django.conf import settings
from django.db import DataError, OperationalError from django.db import DataError, OperationalError
from django.db.models import ( from django.db.models import (
@ -51,29 +45,14 @@ from django.db.models.functions import (
) )
from django.test import ( from django.test import (
TestCase, TestCase,
ignore_warnings,
override_settings, override_settings,
skipIfDBFeature, skipIfDBFeature,
skipUnlessDBFeature, skipUnlessDBFeature,
) )
from django.utils import timezone from django.utils import timezone
from django.utils.deprecation import RemovedInDjango50Warning
from ..models import Author, DTModel, Fan 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): def truncate_to(value, kind, tzinfo=None):
# Convert to target timezone before truncation # Convert to target timezone before truncation
@ -1690,10 +1669,6 @@ class DateFunctionTests(TestCase):
@override_settings(USE_TZ=True, TIME_ZONE="UTC") @override_settings(USE_TZ=True, TIME_ZONE="UTC")
class DateFunctionWithTimeZoneTests(DateFunctionTests): class DateFunctionWithTimeZoneTests(DateFunctionTests):
def get_timezones(self, key):
for constructor in ZONE_CONSTRUCTORS:
yield constructor(key)
def test_extract_func_with_timezone(self): def test_extract_func_with_timezone(self):
start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321) start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321)
end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123) end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123)
@ -1702,9 +1677,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
delta_tzinfo_pos = datetime_timezone(timedelta(hours=5)) delta_tzinfo_pos = datetime_timezone(timedelta(hours=5))
delta_tzinfo_neg = datetime_timezone(timedelta(hours=-5, minutes=17)) delta_tzinfo_neg = datetime_timezone(timedelta(hours=-5, minutes=17))
melb = zoneinfo.ZoneInfo("Australia/Melbourne")
for melb in self.get_timezones("Australia/Melbourne"):
with self.subTest(repr(melb)):
qs = DTModel.objects.annotate( qs = DTModel.objects.annotate(
day=Extract("start_datetime", "day"), day=Extract("start_datetime", "day"),
day_melb=Extract("start_datetime", "day", tzinfo=melb), day_melb=Extract("start_datetime", "day", tzinfo=melb),
@ -1717,12 +1691,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
quarter=ExtractQuarter("start_datetime", tzinfo=melb), quarter=ExtractQuarter("start_datetime", tzinfo=melb),
hour=ExtractHour("start_datetime"), hour=ExtractHour("start_datetime"),
hour_melb=ExtractHour("start_datetime", tzinfo=melb), hour_melb=ExtractHour("start_datetime", tzinfo=melb),
hour_with_delta_pos=ExtractHour( hour_with_delta_pos=ExtractHour("start_datetime", tzinfo=delta_tzinfo_pos),
"start_datetime", tzinfo=delta_tzinfo_pos hour_with_delta_neg=ExtractHour("start_datetime", tzinfo=delta_tzinfo_neg),
),
hour_with_delta_neg=ExtractHour(
"start_datetime", tzinfo=delta_tzinfo_neg
),
minute_with_delta_neg=ExtractMinute( minute_with_delta_neg=ExtractMinute(
"start_datetime", tzinfo=delta_tzinfo_neg "start_datetime", tzinfo=delta_tzinfo_neg
), ),
@ -1765,8 +1735,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
start_datetime = timezone.make_aware(start_datetime) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime) end_datetime = timezone.make_aware(end_datetime)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
for ust_nera in self.get_timezones("Asia/Ust-Nera"): ust_nera = zoneinfo.ZoneInfo("Asia/Ust-Nera")
with self.subTest(repr(ust_nera)):
qs = DTModel.objects.annotate( qs = DTModel.objects.annotate(
hour=ExtractHour("start_datetime"), hour=ExtractHour("start_datetime"),
hour_tz=ExtractHour("start_datetime", tzinfo=ust_nera), hour_tz=ExtractHour("start_datetime", tzinfo=ust_nera),
@ -1788,9 +1758,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
start_datetime = timezone.make_aware(start_datetime) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime) end_datetime = timezone.make_aware(end_datetime)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
melb = zoneinfo.ZoneInfo("Australia/Melbourne")
for melb in self.get_timezones("Australia/Melbourne"):
with self.subTest(repr(melb)):
with timezone.override(melb): with timezone.override(melb):
model = ( model = (
DTModel.objects.annotate( DTModel.objects.annotate(
@ -1806,8 +1774,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
self.assertEqual(model.day_utc, 15) self.assertEqual(model.day_utc, 15)
def test_extract_invalid_field_with_timezone(self): def test_extract_invalid_field_with_timezone(self):
for melb in self.get_timezones("Australia/Melbourne"): melb = zoneinfo.ZoneInfo("Australia/Melbourne")
with self.subTest(repr(melb)):
msg = "tzinfo can only be used with DateTimeField." msg = "tzinfo can only be used with DateTimeField."
with self.assertRaisesMessage(ValueError, msg): with self.assertRaisesMessage(ValueError, msg):
DTModel.objects.annotate( DTModel.objects.annotate(
@ -1824,12 +1791,9 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
start_datetime = timezone.make_aware(start_datetime) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime) end_datetime = timezone.make_aware(end_datetime)
self.create_model(start_datetime, 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 = ( model = (
DTModel.objects.annotate( DTModel.objects.annotate(
melb_year=TruncYear("start_datetime", tzinfo=melb), melb_year=TruncYear("start_datetime", tzinfo=melb),
@ -1846,9 +1810,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
melb_start_datetime = start_datetime.astimezone(melb) melb_start_datetime = start_datetime.astimezone(melb)
pacific_start_datetime = start_datetime.astimezone(pacific) pacific_start_datetime = start_datetime.astimezone(pacific)
self.assertEqual(model.start_datetime, start_datetime) self.assertEqual(model.start_datetime, start_datetime)
self.assertEqual( self.assertEqual(model.melb_year, truncate_to(start_datetime, "year", melb))
model.melb_year, truncate_to(start_datetime, "year", melb)
)
self.assertEqual( self.assertEqual(
model.pacific_year, truncate_to(start_datetime, "year", pacific) model.pacific_year, truncate_to(start_datetime, "year", pacific)
) )
@ -1860,39 +1822,6 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
self.assertEqual(model.melb_time, melb_start_datetime.time()) self.assertEqual(model.melb_time, melb_start_datetime.time())
self.assertEqual(model.pacific_time, pacific_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))
def test_trunc_func_with_timezone(self): def test_trunc_func_with_timezone(self):
""" """
If the truncated datetime transitions to a different offset (daylight If the truncated datetime transitions to a different offset (daylight
@ -1904,9 +1833,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
end_datetime = timezone.make_aware(end_datetime) end_datetime = timezone.make_aware(end_datetime)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
self.create_model(end_datetime, start_datetime) self.create_model(end_datetime, start_datetime)
melb = zoneinfo.ZoneInfo("Australia/Melbourne")
for melb in self.get_timezones("Australia/Melbourne"):
with self.subTest(repr(melb)):
def test_datetime_kind(kind): def test_datetime_kind(kind):
self.assertQuerySetEqual( self.assertQuerySetEqual(
@ -1921,9 +1848,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
[ [
( (
start_datetime, start_datetime,
truncate_to( truncate_to(start_datetime.astimezone(melb), kind, melb),
start_datetime.astimezone(melb), kind, melb
),
), ),
( (
end_datetime, end_datetime,
@ -1946,9 +1871,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
[ [
( (
start_datetime, start_datetime,
truncate_to( truncate_to(start_datetime.astimezone(melb).date(), kind),
start_datetime.astimezone(melb).date(), kind
),
), ),
( (
end_datetime, end_datetime,
@ -1971,9 +1894,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
[ [
( (
start_datetime, start_datetime,
truncate_to( truncate_to(start_datetime.astimezone(melb).time(), kind),
start_datetime.astimezone(melb).time(), kind
),
), ),
( (
end_datetime, end_datetime,
@ -2008,8 +1929,7 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
self.assertEqual(qs.count(), 2) self.assertEqual(qs.count(), 2)
def test_trunc_invalid_field_with_timezone(self): def test_trunc_invalid_field_with_timezone(self):
for melb in self.get_timezones("Australia/Melbourne"): melb = zoneinfo.ZoneInfo("Australia/Melbourne")
with self.subTest(repr(melb)):
msg = "tzinfo can only be used with DateTimeField." msg = "tzinfo can only be used with DateTimeField."
with self.assertRaisesMessage(ValueError, msg): with self.assertRaisesMessage(ValueError, msg):
DTModel.objects.annotate( DTModel.objects.annotate(

View File

@ -15,11 +15,6 @@ try:
except ImportError: except ImportError:
from backports import zoneinfo from backports import zoneinfo
try:
import pytz
except ImportError:
pytz = None
import custom_migration_operations.more_operations import custom_migration_operations.more_operations
import custom_migration_operations.operations import custom_migration_operations.operations
@ -595,16 +590,6 @@ class WriterTests(SimpleTestCase):
{"import datetime"}, {"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): def test_serialize_fields(self):
self.assertSerializedFieldEqual(models.CharField(max_length=255)) self.assertSerializedFieldEqual(models.CharField(max_length=255))

View File

@ -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/libmemcached can't be built on Windows.
pylibmc; sys.platform != 'win32' pylibmc; sys.platform != 'win32'
pymemcache >= 3.4.0 pymemcache >= 3.4.0
pytz
pywatchman; sys.platform != 'win32' pywatchman; sys.platform != 'win32'
PyYAML PyYAML
redis >= 3.4.0 redis >= 3.4.0

View File

@ -4,13 +4,7 @@ import unittest
from types import ModuleType, SimpleNamespace from types import ModuleType, SimpleNamespace
from unittest import mock from unittest import mock
from django.conf import ( from django.conf import ENVIRONMENT_VARIABLE, LazySettings, Settings, settings
ENVIRONMENT_VARIABLE,
USE_DEPRECATED_PYTZ_DEPRECATED_MSG,
LazySettings,
Settings,
settings,
)
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest from django.http import HttpRequest
from django.test import ( from django.test import (
@ -23,7 +17,6 @@ from django.test import (
) )
from django.test.utils import requires_tz_support from django.test.utils import requires_tz_support
from django.urls import clear_script_prefix, set_script_prefix 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"]}) @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"): with self.assertRaisesMessage(ValueError, "Incorrect timezone setting: test"):
settings._setup() 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): class TestComplexSettingOverride(SimpleTestCase):
def setUp(self): def setUp(self):

View File

@ -10,11 +10,6 @@ try:
except ImportError: except ImportError:
from backports import zoneinfo from backports import zoneinfo
try:
import pytz
except ImportError:
pytz = None
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import serializers from django.core import serializers
from django.db import connection from django.db import connection
@ -31,7 +26,6 @@ from django.test import (
SimpleTestCase, SimpleTestCase,
TestCase, TestCase,
TransactionTestCase, TransactionTestCase,
ignore_warnings,
override_settings, override_settings,
skipIfDBFeature, skipIfDBFeature,
skipUnlessDBFeature, skipUnlessDBFeature,
@ -79,14 +73,6 @@ UTC = datetime.timezone.utc
EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi
ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok
ZONE_CONSTRUCTORS = (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): class UTCAliasTests(SimpleTestCase):
def test_alias_deprecation_warning(self): def test_alias_deprecation_warning(self):
@ -413,9 +399,8 @@ class NewDatabaseTests(TestCase):
self.assertEqual(Event.objects.filter(dt__gte=dt2).count(), 1) self.assertEqual(Event.objects.filter(dt__gte=dt2).count(), 1)
self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0) self.assertEqual(Event.objects.filter(dt__gt=dt2).count(), 0)
def test_query_filter_with_pytz_timezones(self): def test_query_filter_with_timezones(self):
for tz in get_timezones("Europe/Paris"): tz = zoneinfo.ZoneInfo("Europe/Paris")
with self.subTest(repr(tz)):
dt = datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=tz) dt = datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=tz)
Event.objects.create(dt=dt) Event.objects.create(dt=dt)
next = dt + datetime.timedelta(seconds=3) next = dt + datetime.timedelta(seconds=3)
@ -423,29 +408,8 @@ class NewDatabaseTests(TestCase):
self.assertEqual(Event.objects.filter(dt__exact=dt).count(), 1) 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__exact=next).count(), 0)
self.assertEqual(Event.objects.filter(dt__in=(prev, next)).count(), 0) self.assertEqual(Event.objects.filter(dt__in=(prev, next)).count(), 0)
self.assertEqual( self.assertEqual(Event.objects.filter(dt__in=(prev, dt, next)).count(), 1)
Event.objects.filter(dt__in=(prev, dt, next)).count(), 1 self.assertEqual(Event.objects.filter(dt__range=(prev, 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_convert_timezones(self): def test_query_convert_timezones(self):
# Connection timezone is equal to the current timezone, datetime # Connection timezone is equal to the current timezone, datetime
@ -1075,8 +1039,7 @@ class TemplateTests(SimpleTestCase):
) )
# Use an IANA timezone as argument # Use an IANA timezone as argument
for tz in get_timezones("Europe/Paris"): tz = zoneinfo.ZoneInfo("Europe/Paris")
with self.subTest(repr(tz)):
tpl = Template("{% load tz %}{{ dt|timezone:tz }}") tpl = Template("{% load tz %}{{ dt|timezone:tz }}")
ctx = Context( ctx = Context(
{ {
@ -1147,8 +1110,7 @@ class TemplateTests(SimpleTestCase):
tpl = Template("{% load tz %}{% timezone tz %}{{ dt }}{% endtimezone %}") tpl = Template("{% load tz %}{% timezone tz %}{{ dt }}{% endtimezone %}")
# Use a IANA timezone as argument # Use a IANA timezone as argument
for tz in get_timezones("Europe/Paris"): tz = zoneinfo.ZoneInfo("Europe/Paris")
with self.subTest(repr(tz)):
ctx = Context( ctx = Context(
{ {
"dt": datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT), "dt": datetime.datetime(2011, 9, 1, 13, 20, 30, tzinfo=EAT),
@ -1166,22 +1128,6 @@ class TemplateTests(SimpleTestCase):
) )
self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00") 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") @skipIf(sys.platform == "win32", "Windows uses non-standard time zone names")
def test_get_current_timezone_templatetag(self): def test_get_current_timezone_templatetag(self):
""" """
@ -1205,14 +1151,10 @@ class TemplateTests(SimpleTestCase):
self.assertEqual(tpl.render(Context({"tz": ICT})), "+0700") self.assertEqual(tpl.render(Context({"tz": ICT})), "+0700")
def test_get_current_timezone_templatetag_with_iana(self): def test_get_current_timezone_templatetag_with_iana(self):
"""
Test the {% get_current_timezone %} templatetag with pytz.
"""
tpl = Template( tpl = Template(
"{% load tz %}{% get_current_timezone as time_zone %}{{ time_zone }}" "{% load tz %}{% get_current_timezone as time_zone %}{{ time_zone }}"
) )
for tz in get_timezones("Europe/Paris"): tz = zoneinfo.ZoneInfo("Europe/Paris")
with self.subTest(repr(tz)):
with timezone.override(tz): with timezone.override(tz):
self.assertEqual(tpl.render(Context()), "Europe/Paris") self.assertEqual(tpl.render(Context()), "Europe/Paris")
@ -1282,8 +1224,7 @@ class LegacyFormsTests(TestCase):
def test_form_with_non_existent_time(self): def test_form_with_non_existent_time(self):
form = EventForm({"dt": "2011-03-27 02:30:00"}) form = EventForm({"dt": "2011-03-27 02:30:00"})
for tz in get_timezones("Europe/Paris"): tz = zoneinfo.ZoneInfo("Europe/Paris")
with self.subTest(repr(tz)):
with timezone.override(tz): with timezone.override(tz):
# This is a bug. # This is a bug.
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
@ -1294,8 +1235,7 @@ class LegacyFormsTests(TestCase):
def test_form_with_ambiguous_time(self): def test_form_with_ambiguous_time(self):
form = EventForm({"dt": "2011-10-30 02:30:00"}) form = EventForm({"dt": "2011-10-30 02:30:00"})
for tz in get_timezones("Europe/Paris"): tz = zoneinfo.ZoneInfo("Europe/Paris")
with self.subTest(repr(tz)):
with timezone.override(tz): with timezone.override(tz):
# This is a bug. # This is a bug.
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
@ -1338,8 +1278,7 @@ class NewFormsTests(TestCase):
) )
def test_form_with_non_existent_time(self): def test_form_with_non_existent_time(self):
for tz in get_timezones("Europe/Paris"): tz = zoneinfo.ZoneInfo("Europe/Paris")
with self.subTest(repr(tz)):
with timezone.override(tz): with timezone.override(tz):
form = EventForm({"dt": "2011-03-27 02:30:00"}) form = EventForm({"dt": "2011-03-27 02:30:00"})
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())
@ -1352,8 +1291,7 @@ class NewFormsTests(TestCase):
) )
def test_form_with_ambiguous_time(self): def test_form_with_ambiguous_time(self):
for tz in get_timezones("Europe/Paris"): tz = zoneinfo.ZoneInfo("Europe/Paris")
with self.subTest(repr(tz)):
with timezone.override(tz): with timezone.override(tz):
form = EventForm({"dt": "2011-10-30 02:30:00"}) form = EventForm({"dt": "2011-10-30 02:30:00"})
self.assertFalse(form.is_valid()) self.assertFalse(form.is_valid())

View File

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

View File

@ -1,18 +1,12 @@
import datetime import datetime
import unittest
from unittest import mock from unittest import mock
try:
import pytz
except ImportError:
pytz = None
try: try:
import zoneinfo import zoneinfo
except ImportError: except ImportError:
from backports import zoneinfo 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 import timezone
from django.utils.deprecation import RemovedInDjango50Warning 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 ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok
UTC = datetime.timezone.utc 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): 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): def test_default_timezone_is_zoneinfo(self):
self.assertIsInstance(timezone.get_default_timezone(), zoneinfo.ZoneInfo) 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): def test_now(self):
with override_settings(USE_TZ=True): with override_settings(USE_TZ=True):
self.assertTrue(timezone.is_aware(timezone.now())) self.assertTrue(timezone.is_aware(timezone.now()))
@ -208,46 +175,15 @@ class TimezoneTests(SimpleTestCase):
def test_make_aware2(self): def test_make_aware2(self):
CEST = datetime.timezone(datetime.timedelta(hours=2), "CEST") CEST = datetime.timezone(datetime.timedelta(hours=2), "CEST")
for tz in PARIS_IMPLS:
with self.subTest(repr(tz)):
self.assertEqual( self.assertEqual(
timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), tz), timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), PARIS_ZI),
datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=CEST), 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
)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
timezone.make_aware( timezone.make_aware(
datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=PARIS_ZI), PARIS_ZI 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): def test_make_naive_zoneinfo(self):
self.assertEqual( self.assertEqual(
timezone.make_naive( timezone.make_naive(
@ -264,21 +200,6 @@ class TimezoneTests(SimpleTestCase):
datetime.datetime(2011, 9, 1, 12, 20, 30, fold=1), 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): def test_make_aware_zoneinfo_ambiguous(self):
# 2:30 happens twice, once before DST ends and once after # 2:30 happens twice, once before DST ends and once after
ambiguous = datetime.datetime(2015, 10, 25, 2, 30) ambiguous = datetime.datetime(2015, 10, 25, 2, 30)
@ -292,21 +213,6 @@ class TimezoneTests(SimpleTestCase):
self.assertEqual(std.utcoffset(), datetime.timedelta(hours=1)) self.assertEqual(std.utcoffset(), datetime.timedelta(hours=1))
self.assertEqual(dst.utcoffset(), datetime.timedelta(hours=2)) 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): def test_make_aware_zoneinfo_non_existent(self):
# 2:30 never happened due to DST # 2:30 never happened due to DST
non_existent = datetime.datetime(2015, 3, 29, 2, 30) non_existent = datetime.datetime(2015, 3, 29, 2, 30)
@ -349,12 +255,6 @@ class TimezoneTests(SimpleTestCase):
(zoneinfo.ZoneInfo("Europe/Madrid"), "Europe/Madrid"), (zoneinfo.ZoneInfo("Europe/Madrid"), "Europe/Madrid"),
(zoneinfo.ZoneInfo("Etc/GMT-10"), "+10"), (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: for tz, expected in tests:
with self.subTest(tz=tz, expected=expected): with self.subTest(tz=tz, expected=expected):
self.assertEqual(timezone._get_timezone_name(tz), expected) self.assertEqual(timezone._get_timezone_name(tz), expected)