diff --git a/django/conf/__init__.py b/django/conf/__init__.py index f8018f723b..80f3115d47 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -21,6 +21,13 @@ from django.utils.functional import LazyObject, empty ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE" +# 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.' +) + USE_L10N_DEPRECATED_MSG = ( 'The USE_L10N setting is deprecated. Starting with Django 5.0, localized ' 'formatting of data will always be enabled. For example Django will ' @@ -196,6 +203,9 @@ class Settings: category=RemovedInDjango50Warning, ) + if self.is_overridden('USE_DEPRECATED_PYTZ'): + warnings.warn(USE_DEPRECATED_PYTZ_DEPRECATED_MSG, RemovedInDjango50Warning) + if hasattr(time, 'tzset') and self.TIME_ZONE: # When we can, attempt to validate the timezone. If we can't find # this file, no check happens and it's harmless. @@ -245,6 +255,8 @@ class UserSettingsHolder: if name == 'USE_L10N': warnings.warn(USE_L10N_DEPRECATED_MSG, RemovedInDjango50Warning) super().__setattr__(name, value) + if name == 'USE_DEPRECATED_PYTZ': + warnings.warn(USE_DEPRECATED_PYTZ_DEPRECATED_MSG, RemovedInDjango50Warning) def __delattr__(self, name): self._deleted.add(name) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index ab69316314..d2c1e97e16 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -43,6 +43,11 @@ TIME_ZONE = 'America/Chicago' # If you set this to True, Django will use timezone-aware datetimes. USE_TZ = False +# 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 b82440e9f8..bc74a2a3d6 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -1,5 +1,6 @@ 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, display_for_value, get_fields_from_path, @@ -328,7 +329,7 @@ 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} + qs_kwargs = {'is_dst': True} if settings.USE_DEPRECATED_PYTZ else {} else: dates_or_datetimes = 'dates' qs_kwargs = {} diff --git a/django/db/backends/base/base.py b/django/db/backends/base/base.py index 42b4b178bf..b6800e5519 100644 --- a/django/db/backends/base/base.py +++ b/django/db/backends/base/base.py @@ -6,7 +6,10 @@ import warnings from collections import deque from contextlib import contextmanager -import pytz +try: + import zoneinfo +except ImportError: + from backports import zoneinfo from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -23,6 +26,14 @@ from django.utils.functional import cached_property NO_DB_ALIAS = '__no_db__' +# 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.""" # Mapping of Field objects to their column types. @@ -135,7 +146,7 @@ class BaseDatabaseWrapper: elif self.settings_dict['TIME_ZONE'] is None: return timezone.utc else: - return pytz.timezone(self.settings_dict['TIME_ZONE']) + return timezone_constructor(self.settings_dict['TIME_ZONE']) @cached_property def timezone_name(self): diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index a64190f0d0..ddf5f40c8e 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -14,12 +14,12 @@ import warnings from itertools import chain from sqlite3 import dbapi2 as Database -import pytz - from django.core.exceptions import ImproperlyConfigured from django.db import IntegrityError from django.db.backends import utils as backend_utils -from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.backends.base.base import ( + BaseDatabaseWrapper, timezone_constructor, +) from django.utils import timezone from django.utils.asyncio import async_unsafe from django.utils.dateparse import parse_datetime, parse_time @@ -431,7 +431,7 @@ def _sqlite_datetime_parse(dt, tzname=None, conn_tzname=None): except (TypeError, ValueError): return None if conn_tzname: - dt = dt.replace(tzinfo=pytz.timezone(conn_tzname)) + dt = dt.replace(tzinfo=timezone_constructor(conn_tzname)) if tzname is not None and tzname != conn_tzname: sign_index = tzname.find('+') + tzname.find('-') + 1 if sign_index > -1: @@ -441,7 +441,7 @@ def _sqlite_datetime_parse(dt, tzname=None, conn_tzname=None): hours, minutes = offset.split(':') offset_delta = datetime.timedelta(hours=int(hours), minutes=int(minutes)) dt += offset_delta if sign == '+' else -offset_delta - dt = timezone.localtime(dt, pytz.timezone(tzname)) + dt = timezone.localtime(dt, timezone_constructor(tzname)) return dt diff --git a/django/db/migrations/serializer.py b/django/db/migrations/serializer.py index 6e78462e95..9c58f38e28 100644 --- a/django/db/migrations/serializer.py +++ b/django/db/migrations/serializer.py @@ -67,7 +67,7 @@ class DatetimeDatetimeSerializer(BaseSerializer): imports = ["import datetime"] if self.value.tzinfo is not None: imports.append("from django.utils.timezone import utc") - return repr(self.value).replace('', 'utc'), set(imports) + return repr(self.value).replace('datetime.timezone.utc', 'utc'), set(imports) class DecimalSerializer(BaseSerializer): diff --git a/django/db/models/functions/datetime.py b/django/db/models/functions/datetime.py index 20161bef38..07f884f78d 100644 --- a/django/db/models/functions/datetime.py +++ b/django/db/models/functions/datetime.py @@ -188,7 +188,9 @@ class TruncBase(TimezoneMixin, Transform): kind = None tzinfo = None - def __init__(self, expression, output_field=None, tzinfo=None, is_dst=None, **extra): + # RemovedInDjango50Warning: when the deprecation ends, remove is_dst + # argument. + def __init__(self, expression, output_field=None, tzinfo=None, is_dst=timezone.NOT_PASSED, **extra): self.tzinfo = tzinfo self.is_dst = is_dst super().__init__(expression, output_field=output_field, **extra) @@ -264,7 +266,9 @@ class TruncBase(TimezoneMixin, Transform): class Trunc(TruncBase): - def __init__(self, expression, kind, output_field=None, tzinfo=None, is_dst=None, **extra): + # RemovedInDjango50Warning: when the deprecation ends, remove is_dst + # argument. + def __init__(self, expression, kind, output_field=None, tzinfo=None, is_dst=timezone.NOT_PASSED, **extra): self.kind = kind super().__init__( expression, output_field=output_field, tzinfo=tzinfo, diff --git a/django/db/models/query.py b/django/db/models/query.py index 88cfc3de38..1bc8e2ed2a 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -916,7 +916,9 @@ class QuerySet: 'datefield', flat=True ).distinct().filter(plain_field__isnull=False).order_by(('-' if order == 'DESC' else '') + 'datefield') - def datetimes(self, field_name, kind, order='ASC', tzinfo=None, is_dst=None): + # RemovedInDjango50Warning: when the deprecation ends, remove is_dst + # argument. + def datetimes(self, field_name, kind, order='ASC', tzinfo=None, is_dst=timezone.NOT_PASSED): """ Return a list of datetime objects representing all available datetimes for the given field_name, scoped to 'kind'. diff --git a/django/templatetags/tz.py b/django/templatetags/tz.py index e1d2fe8507..455c2ed389 100644 --- a/django/templatetags/tz.py +++ b/django/templatetags/tz.py @@ -1,13 +1,37 @@ from datetime import datetime, tzinfo -import pytz +try: + import zoneinfo +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): @@ -61,8 +85,8 @@ def do_timezone(value, arg): tz = arg elif isinstance(arg, str): try: - tz = pytz.timezone(arg) - except pytz.UnknownTimeZoneError: + tz = timezone_constructor(arg) + except UnknownTimezoneException: return '' else: return '' diff --git a/django/utils/timezone.py b/django/utils/timezone.py index 0f5d012c59..0c23486c1f 100644 --- a/django/utils/timezone.py +++ b/django/utils/timezone.py @@ -3,13 +3,21 @@ Timezone-related classes and functions. """ import functools +import sys +import warnings + +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + from contextlib import ContextDecorator from datetime import datetime, timedelta, timezone, tzinfo -import pytz from asgiref.local import Local from django.conf import settings +from django.utils.deprecation import RemovedInDjango50Warning __all__ = [ 'utc', 'get_fixed_timezone', @@ -20,14 +28,11 @@ __all__ = [ 'is_aware', 'is_naive', 'make_aware', 'make_naive', ] +# RemovedInDjango50Warning: sentinel for deprecation of is_dst parameters. +NOT_PASSED = object() -# UTC time zone as a tzinfo instance. -utc = pytz.utc -_PYTZ_BASE_CLASSES = (pytz.tzinfo.BaseTzInfo, pytz._FixedOffset) -# In releases prior to 2018.4, pytz.UTC was not a subclass of BaseTzInfo -if not isinstance(pytz.UTC, pytz._FixedOffset): - _PYTZ_BASE_CLASSES = _PYTZ_BASE_CLASSES + (type(pytz.UTC),) +utc = timezone.utc def get_fixed_timezone(offset): @@ -49,7 +54,10 @@ def get_default_timezone(): This is the time zone defined by settings.TIME_ZONE. """ - return pytz.timezone(settings.TIME_ZONE) + if settings.USE_DEPRECATED_PYTZ: + import pytz + return pytz.timezone(settings.TIME_ZONE) + return zoneinfo.ZoneInfo(settings.TIME_ZONE) # This function exists for consistency with get_current_timezone_name @@ -94,7 +102,11 @@ def activate(timezone): if isinstance(timezone, tzinfo): _active.value = timezone elif isinstance(timezone, str): - _active.value = pytz.timezone(timezone) + if settings.USE_DEPRECATED_PYTZ: + import pytz + _active.value = pytz.timezone(timezone) + else: + _active.value = zoneinfo.ZoneInfo(timezone) else: raise ValueError("Invalid timezone: %r" % timezone) @@ -229,8 +241,17 @@ def is_naive(value): return value.utcoffset() is None -def make_aware(value, timezone=None, is_dst=None): +def make_aware(value, timezone=None, is_dst=NOT_PASSED): """Make a naive datetime.datetime in a given time zone aware.""" + if is_dst is NOT_PASSED: + is_dst = None + else: + warnings.warn( + 'The is_dst argument to make_aware(), used by the Trunc() ' + 'database functions and QuerySet.datetimes(), is deprecated as it ' + 'has no effect with zoneinfo time zones.', + RemovedInDjango50Warning, + ) if timezone is None: timezone = get_current_timezone() if _is_pytz_zone(timezone): @@ -255,13 +276,45 @@ 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 = _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): diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 6154fefa3b..09be9e66e4 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -36,6 +36,24 @@ details on these changes. * The ``USE_L10N`` setting will be removed. +* The ``USE_DEPRECATED_PYTZ`` transitional setting will be removed. + +* Support for ``pytz`` timezones will be removed. + +* The ``is_dst`` argument will be removed from: + + * ``QuerySet.datetimes()`` + * ``django.utils.timezone.make_aware()`` + * ``django.db.models.functions.Trunc()`` + * ``django.db.models.functions.TruncSecond()`` + * ``django.db.models.functions.TruncMinute()`` + * ``django.db.models.functions.TruncHour()`` + * ``django.db.models.functions.TruncDay()`` + * ``django.db.models.functions.TruncWeek()`` + * ``django.db.models.functions.TruncMonth()`` + * ``django.db.models.functions.TruncQuarter()`` + * ``django.db.models.functions.TruncYear()`` + .. _deprecation-removed-in-4.1: 4.1 diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt index 18dfdae976..3d2e436b67 100644 --- a/docs/ref/models/database-functions.txt +++ b/docs/ref/models/database-functions.txt @@ -242,7 +242,8 @@ Takes an ``expression`` representing a ``DateField``, ``DateTimeField``, of the date referenced by ``lookup_name`` as an ``IntegerField``. Django usually uses the databases' extract function, so you may use any ``lookup_name`` that your database supports. A ``tzinfo`` subclass, usually -provided by ``pytz``, can be passed to extract a value in a specific timezone. +provided by :mod:`zoneinfo`, can be passed to extract a value in a specific +timezone. Given the datetime ``2015-06-15 23:30:01.000321+00:00``, the built-in ``lookup_name``\s return: @@ -450,8 +451,8 @@ to that timezone before the value is extracted. The example below converts to the Melbourne timezone (UTC +10:00), which changes the day, weekday, and hour values that are returned:: - >>> import pytz - >>> melb = pytz.timezone('Australia/Melbourne') # UTC+10:00 + >>> import zoneinfo + >>> melb = zoneinfo.ZoneInfo('Australia/Melbourne') # UTC+10:00 >>> with timezone.override(melb): ... Experiment.objects.annotate( ... day=ExtractDay('start_datetime'), @@ -466,8 +467,8 @@ values that are returned:: Explicitly passing the timezone to the ``Extract`` function behaves in the same way, and takes priority over an active timezone:: - >>> import pytz - >>> melb = pytz.timezone('Australia/Melbourne') + >>> import zoneinfo + >>> melb = zoneinfo.ZoneInfo('Australia/Melbourne') >>> Experiment.objects.annotate( ... day=ExtractDay('start_datetime', tzinfo=melb), ... weekday=ExtractWeekDay('start_datetime', tzinfo=melb), @@ -517,12 +518,16 @@ part, and an ``output_field`` that's either ``DateTimeField()``, ``TimeField()``, or ``DateField()``. It returns a datetime, date, or time depending on ``output_field``, with fields up to ``kind`` set to their minimum value. If ``output_field`` is omitted, it will default to the ``output_field`` -of ``expression``. A ``tzinfo`` subclass, usually provided by ``pytz``, can be -passed to truncate a value in a specific timezone. +of ``expression``. A ``tzinfo`` subclass, usually provided by :mod:`zoneinfo`, +can be passed to truncate a value in a specific timezone. -The ``is_dst`` parameter indicates whether or not ``pytz`` should interpret -nonexistent and ambiguous datetimes in daylight saving time. By default (when -``is_dst=None``), ``pytz`` raises an exception for such datetimes. +.. deprecated:: 4.0 + + The ``is_dst`` parameter indicates whether or not ``pytz`` should interpret + nonexistent and ambiguous datetimes in daylight saving time. By default + (when ``is_dst=None``), ``pytz`` raises an exception for such datetimes. + + The ``is_dst`` parameter is deprecated and will be removed in Django 5.0. Given the datetime ``2015-06-15 14:30:50.000321+00:00``, the built-in ``kind``\s return: @@ -607,6 +612,10 @@ Usage example:: .. attribute:: kind = 'quarter' +.. deprecated:: 4.0 + + The ``is_dst`` parameter is deprecated and will be removed in Django 5.0. + These are logically equivalent to ``Trunc('date_field', kind)``. They truncate all parts of the date up to ``kind`` which allows grouping or filtering dates with less precision. ``expression`` can have an ``output_field`` of either @@ -634,8 +643,8 @@ that deal with date-parts can be used with ``DateField``:: 2014-01-01 1 2015-01-01 2 - >>> import pytz - >>> melb = pytz.timezone('Australia/Melbourne') + >>> import zoneinfo + >>> melb = zoneinfo.ZoneInfo('Australia/Melbourne') >>> experiments_per_month = Experiment.objects.annotate( ... month=TruncMonth('start_datetime', tzinfo=melb)).values('month').annotate( ... experiments=Count('id')) @@ -691,6 +700,10 @@ truncate function. It's also registered as a transform on ``DateTimeField`` as .. attribute:: kind = 'second' +.. deprecated:: 4.0 + + The ``is_dst`` parameter is deprecated and will be removed in Django 5.0. + These are logically equivalent to ``Trunc('datetime_field', kind)``. They truncate all parts of the date up to ``kind`` and allow grouping or filtering datetimes with less precision. ``expression`` must have an ``output_field`` of @@ -704,10 +717,10 @@ Usage example:: ... TruncDate, TruncDay, TruncHour, TruncMinute, TruncSecond, ... ) >>> from django.utils import timezone - >>> import pytz + >>> import zoneinfo >>> start1 = datetime(2014, 6, 15, 14, 30, 50, 321, tzinfo=timezone.utc) >>> Experiment.objects.create(start_datetime=start1, start_date=start1.date()) - >>> melb = pytz.timezone('Australia/Melbourne') + >>> melb = zoneinfo.ZoneInfo('Australia/Melbourne') >>> Experiment.objects.annotate( ... date=TruncDate('start_datetime'), ... day=TruncDay('start_datetime', tzinfo=melb), @@ -716,10 +729,10 @@ Usage example:: ... second=TruncSecond('start_datetime'), ... ).values('date', 'day', 'hour', 'minute', 'second').get() {'date': datetime.date(2014, 6, 15), - 'day': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=), - 'hour': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=), - 'minute': 'minute': datetime.datetime(2014, 6, 15, 14, 30, tzinfo=), - 'second': datetime.datetime(2014, 6, 15, 14, 30, 50, tzinfo=) + 'day': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=zoneinfo.ZoneInfo('Australia/Melbourne')), + 'hour': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=zoneinfo.ZoneInfo('Australia/Melbourne')), + 'minute': 'minute': datetime.datetime(2014, 6, 15, 14, 30, tzinfo=zoneinfo.ZoneInfo('UTC')), + 'second': datetime.datetime(2014, 6, 15, 14, 30, 50, tzinfo=zoneinfo.ZoneInfo('UTC')) } ``TimeField`` truncation @@ -740,6 +753,10 @@ Usage example:: .. attribute:: kind = 'second' +.. deprecated:: 4.0 + + The ``is_dst`` parameter is deprecated and will be removed in Django 5.0. + These are logically equivalent to ``Trunc('time_field', kind)``. They truncate all parts of the time up to ``kind`` which allows grouping or filtering times with less precision. ``expression`` can have an ``output_field`` of either @@ -767,8 +784,8 @@ that deal with time-parts can be used with ``TimeField``:: 14:00:00 2 17:00:00 1 - >>> import pytz - >>> melb = pytz.timezone('Australia/Melbourne') + >>> import zoneinfo + >>> melb = zoneinfo.ZoneInfo('Australia/Melbourne') >>> experiments_per_hour = Experiment.objects.annotate( ... hour=TruncHour('start_datetime', tzinfo=melb), ... ).values('hour').annotate(experiments=Count('id')) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 583d313b5b..6a4ec0fb05 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -834,6 +834,10 @@ object. If it's ``None``, Django uses the :ref:`current time zone ambiguous datetimes in daylight saving time. By default (when ``is_dst=None``), ``pytz`` raises an exception for such datetimes. +.. deprecated:: 4.0 + + The ``is_dst`` parameter is deprecated and will be removed in Django 5.0. + .. _database-time-zone-definitions: .. note:: @@ -842,13 +846,11 @@ ambiguous datetimes in daylight saving time. By default (when ``is_dst=None``), As a consequence, your database must be able to interpret the value of ``tzinfo.tzname(None)``. This translates into the following requirements: - - SQLite: no requirements. Conversions are performed in Python with pytz_ - (installed when you install Django). + - SQLite: no requirements. Conversions are performed in Python. - PostgreSQL: no requirements (see `Time Zones`_). - Oracle: no requirements (see `Choosing a Time Zone File`_). - MySQL: load the time zone tables with `mysql_tzinfo_to_sql`_. - .. _pytz: http://pytz.sourceforge.net/ .. _Time Zones: https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-TIMEZONES .. _Choosing a Time Zone File: https://docs.oracle.com/en/database/oracle/ oracle-database/18/nlspg/datetime-data-types-and-time-zone-support.html diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 0639c423bb..f534b8746c 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -677,8 +677,8 @@ When :setting:`USE_TZ` is ``False``, it is an error to set this option. otherwise, you should leave this option unset. It's best to store datetimes in UTC because it avoids ambiguous or nonexistent datetimes during daylight saving time changes. Also, receiving datetimes in UTC keeps datetime - arithmetic simple — there's no need for the ``normalize()`` method provided - by pytz. + arithmetic simple — there's no need to consider potential offset changes + over a DST transition. * If you're connecting to a third-party database that stores datetimes in a local time rather than UTC, then you must set this option to the @@ -695,8 +695,8 @@ When :setting:`USE_TZ` is ``False``, it is an error to set this option. as ``date_trunc``, because their results depend on the time zone. However, this has a downside: receiving all datetimes in local time makes - datetime arithmetic more tricky — you must call the ``normalize()`` method - provided by pytz after each operation. + datetime arithmetic more tricky — you must account for possible offset + changes over DST transitions. Consider converting to local time explicitly with ``AT TIME ZONE`` in raw SQL queries instead of setting the ``TIME_ZONE`` option. @@ -2758,6 +2758,23 @@ 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`` +----------------------- + +.. versionadded:: 4.0 + +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/ref/utils.txt b/docs/ref/utils.txt index 3a7f58c5f7..d316c8a8bf 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -941,24 +941,30 @@ appropriate entities. :class:`~datetime.datetime`. If ``timezone`` is set to ``None``, it defaults to the :ref:`current time zone `. - When using ``pytz``, the ``pytz.AmbiguousTimeError`` exception is raised if - you try to make ``value`` aware during a DST transition where the same time - occurs twice (when reverting from DST). Setting ``is_dst`` to ``True`` or - ``False`` will avoid the exception by choosing if the time is - pre-transition or post-transition respectively. + .. deprecated:: 4.0 - When using ``pytz``, the ``pytz.NonExistentTimeError`` exception is raised - if you try to make ``value`` aware during a DST transition such that the - time never occurred. For example, if the 2:00 hour is skipped during a DST - transition, trying to make 2:30 aware in that time zone will raise an - exception. To avoid that you can use ``is_dst`` to specify how - ``make_aware()`` should interpret such a nonexistent time. If - ``is_dst=True`` then the above time would be interpreted as 2:30 DST time - (equivalent to 1:30 local time). Conversely, if ``is_dst=False`` the time - would be interpreted as 2:30 standard time (equivalent to 3:30 local time). + When using ``pytz``, the ``pytz.AmbiguousTimeError`` exception is + raised if you try to make ``value`` aware during a DST transition where + the same time occurs twice (when reverting from DST). Setting + ``is_dst`` to ``True`` or ``False`` will avoid the exception by + choosing if the time is pre-transition or post-transition respectively. - The ``is_dst`` parameter has no effect when using non-``pytz`` timezone - implementations. + When using ``pytz``, the ``pytz.NonExistentTimeError`` exception is + raised if you try to make ``value`` aware during a DST transition such + that the time never occurred. For example, if the 2:00 hour is skipped + during a DST transition, trying to make 2:30 aware in that time zone + will raise an exception. To avoid that you can use ``is_dst`` to + specify how ``make_aware()`` should interpret such a nonexistent time. + If ``is_dst=True`` then the above time would be interpreted as 2:30 DST + time (equivalent to 1:30 local time). Conversely, if ``is_dst=False`` + the time would be interpreted as 2:30 standard time (equivalent to 3:30 + local time). + + The ``is_dst`` parameter has no effect when using non-``pytz`` timezone + implementations. + + The ``is_dst`` parameter is deprecated and will be removed in Django + 5.0. .. function:: make_naive(value, timezone=None) diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index 540500af47..af168a6d33 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -28,6 +28,46 @@ The Django 3.2.x series is the last to support Python 3.6 and 3.7. What's new in Django 4.0 ======================== +``zoneinfo`` default timezone implementation +-------------------------------------------- + +The Python standard library's :mod:`zoneinfo` is now the default timezone +implementation in Django. + +This is the next step in the migration from using ``pytz`` to using +:mod:`zoneinfo`. Django 3.2 allowed the use of non-``pytz`` time zones. Django +4.0 makes ``zoneinfo`` the default implementation. Support for ``pytz`` is now +deprecated and will be removed in Django 5.0. + +:mod:`zoneinfo` is part of the Python standard library from Python 3.9. The +``backports.zoneinfo`` package is automatically installed alongside Django if +you are using Python 3.8. + +The move to ``zoneinfo`` should be largely transparent. Selection of the +current timezone, conversion of datetime instances to the current timezone in +forms and templates, as well as operations on aware datetimes in UTC are +unaffected. + +However, if you are you are working with non-UTC time zones, and using the +``pytz`` ``normalize()`` and ``localize()`` APIs, possibly with the +:setting:`DATABASE-TIME_ZONE` setting, you will need to audit your code, since +``pytz`` and ``zoneinfo`` are not entirely equivalent. + +To give time for such an audit, the transitional :setting:`USE_DEPRECATED_PYTZ` +setting allows continued use of ``pytz`` during the 4.x release cycle. This +setting will be removed in Django 5.0. + +In addition, a `pytz_deprecation_shim`_ package, created by the ``zoneinfo`` +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` +transitional setting is recommended if you need a gradual update path. + +.. _pytz_deprecation_shim: https://pytz-deprecation-shim.readthedocs.io/en/latest/index.html +.. _migration guide: https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html + Functional unique constraints ----------------------------- @@ -595,11 +635,37 @@ Miscellaneous * The default value of the ``USE_L10N`` setting is changed to ``True``. See the :ref:`Localization section ` above for more details. +* As part of the :ref:`move to zoneinfo `, + :attr:`django.utils.timezone.utc` is changed to alias + :attr:`datetime.timezone.utc`. + .. _deprecated-features-4.0: Features deprecated in 4.0 ========================== +Use of ``pytz`` time zones +-------------------------- + +As part of the :ref:`move to zoneinfo `, use of ``pytz`` time +zones is deprecated. + +Accordingly, the ``is_dst`` arguments to the following are also deprecated: + +* :meth:`django.db.models.query.QuerySet.datetimes()` +* :func:`django.db.models.functions.Trunc()` +* :func:`django.db.models.functions.TruncSecond()` +* :func:`django.db.models.functions.TruncMinute()` +* :func:`django.db.models.functions.TruncHour()` +* :func:`django.db.models.functions.TruncDay()` +* :func:`django.db.models.functions.TruncWeek()` +* :func:`django.db.models.functions.TruncMonth()` +* :func:`django.db.models.functions.TruncQuarter()` +* :func:`django.db.models.functions.TruncYear()` +* :func:`django.utils.timezone.make_aware()` + +Support for use of ``pytz`` will be removed in Django 5.0. + Time zone support ----------------- diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt index 5b475bd44f..6eda217f56 100644 --- a/docs/topics/i18n/timezones.txt +++ b/docs/topics/i18n/timezones.txt @@ -19,10 +19,9 @@ practice to store data in UTC in your database. The main reason is daylight saving time (DST). Many countries have a system of DST, where clocks are moved forward in spring and backward in autumn. If you're working in local time, you're likely to encounter errors twice a year, when the transitions happen. -(The pytz_ documentation discusses `these issues`_ in greater detail.) This -probably doesn't matter for your blog, but it's a problem if you over-bill or -under-bill your customers by one hour, twice a year, every year. The solution -to this problem is to use UTC in the code and use local time only when +This probably doesn't matter for your blog, but it's a problem if you over bill +or under bill your customers by one hour, twice a year, every year. The +solution to this problem is to use UTC in the code and use local time only when interacting with end users. Time zone support is disabled by default. To enable it, set :setting:`USE_TZ = @@ -32,15 +31,20 @@ True ` in your settings file. In Django 5.0, time zone support will be enabled by default. -By default, time zone support uses pytz_, which is installed when you install -Django; Django also supports the use of other time zone implementations like -:mod:`zoneinfo` by passing :class:`~datetime.tzinfo` objects directly to -functions in :mod:`django.utils.timezone`. +Time zone support uses :mod:`zoneinfo`, which is part of the Python standard +library from Python 3.9. The ``backports.zoneinfo`` package is automatically +installed alongside Django if you are using Python 3.8. .. versionchanged:: 3.2 Support for non-``pytz`` timezone implementations was added. +.. versionchanged:: 4.0 + + :mod:`zoneinfo` was made the default timezone implementation. You may + continue to use `pytz`_ during the 4.x release cycle via the + :setting:`USE_DEPRECATED_PYTZ` setting. + .. note:: The default :file:`settings.py` file created by :djadmin:`django-admin @@ -88,8 +92,8 @@ should be aware too. In this mode, the example above becomes:: Dealing with aware datetime objects isn't always intuitive. For instance, the ``tzinfo`` argument of the standard datetime constructor doesn't work reliably for time zones with DST. Using UTC is generally safe; if you're - using other time zones, you should review the `pytz`_ documentation - carefully. + using other time zones, you should review the :mod:`zoneinfo` + documentation carefully. .. note:: @@ -113,8 +117,10 @@ receives one, it attempts to make it aware by interpreting it in the :ref:`default time zone ` and raises a warning. Unfortunately, during DST transitions, some datetimes don't exist or are -ambiguous. In such situations, pytz_ raises an exception. That's why you should -always create aware datetime objects when time zone support is enabled. +ambiguous. That's why you should always create aware datetime objects when time +zone support is enabled. (See the :mod:`Using ZoneInfo section of the zoneinfo +docs ` for examples using the ``fold`` attribute to specify the +offset that should apply to a datetime during a DST transition.) In practice, this is rarely an issue. Django gives you aware datetime objects in the models and forms, and most often, new datetime objects are created from @@ -163,16 +169,16 @@ selection logic that makes sense for you. Most websites that care about time zones ask users in which time zone they live and store this information in the user's profile. For anonymous users, they use -the time zone of their primary audience or UTC. pytz_ provides helpers_, like a -list of time zones per country, that you can use to pre-select the most likely -choices. +the time zone of their primary audience or UTC. +:func:`zoneinfo.available_timezones` provides a set of available timezones that +you can use to build a map from likely locations to time zones. Here's an example that stores the current timezone in the session. (It skips error handling entirely for the sake of simplicity.) Add the following middleware to :setting:`MIDDLEWARE`:: - import pytz + import zoneinfo from django.utils import timezone @@ -183,7 +189,7 @@ Add the following middleware to :setting:`MIDDLEWARE`:: def __call__(self, request): tzname = request.session.get('django_timezone') if tzname: - timezone.activate(pytz.timezone(tzname)) + timezone.activate(zoneinfo.ZoneInfo(tzname)) else: timezone.deactivate() return self.get_response(request) @@ -192,12 +198,19 @@ Create a view that can set the current timezone:: from django.shortcuts import redirect, render + # Prepare a map of common locations to timezone choices you wish to offer. + common_timezones = { + 'London': 'Europe/London', + 'Paris': 'Europe/Paris', + 'New York': 'America/New_York', + } + def set_timezone(request): if request.method == 'POST': request.session['django_timezone'] = request.POST['timezone'] return redirect('/') else: - return render(request, 'template.html', {'timezones': pytz.common_timezones}) + return render(request, 'template.html', {'timezones': common_timezones}) Include a form in ``template.html`` that will ``POST`` to this view: @@ -209,8 +222,8 @@ Include a form in ``template.html`` that will ``POST`` to this view: {% csrf_token %} @@ -225,9 +238,8 @@ When you enable time zone support, Django interprets datetimes entered in forms in the :ref:`current time zone ` and returns aware datetime objects in ``cleaned_data``. -If the current time zone raises an exception for datetimes that don't exist or -are ambiguous because they fall in a DST transition (the timezones provided by -pytz_ do this), such datetimes will be reported as invalid values. +Converted datetimes that don't exist or are ambiguous because they fall in a +DST transition will be reported as invalid values. .. _time-zones-in-templates: @@ -583,20 +595,20 @@ Troubleshooting None of this is true in a time zone aware environment:: >>> import datetime - >>> import pytz - >>> paris_tz = pytz.timezone("Europe/Paris") - >>> new_york_tz = pytz.timezone("America/New_York") - >>> paris = paris_tz.localize(datetime.datetime(2012, 3, 3, 1, 30)) - # This is the correct way to convert between time zones with pytz. - >>> new_york = new_york_tz.normalize(paris.astimezone(new_york_tz)) + >>> import zoneinfo + >>> paris_tz = zoneinfo.ZoneInfo("Europe/Paris") + >>> new_york_tz = zoneinfo.ZoneInfo("America/New_York") + >>> paris = datetime.datetime(2012, 3, 3, 1, 30, tzinfo=paris_tz) + # This is the correct way to convert between time zones. + >>> new_york = paris.astimezone(new_york_tz) >>> paris == new_york, paris.date() == new_york.date() (True, False) >>> paris - new_york, paris.date() - new_york.date() (datetime.timedelta(0), datetime.timedelta(1)) >>> paris - datetime.datetime(2012, 3, 3, 1, 30, tzinfo=) + datetime.datetime(2012, 3, 3, 1, 30, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris')) >>> new_york - datetime.datetime(2012, 3, 2, 19, 30, tzinfo=) + datetime.datetime(2012, 3, 2, 19, 30, tzinfo=zoneinfo.ZoneInfo(key='America/New_York')) As this example shows, the same datetime has a different date, depending on the time zone in which it is represented. But the real problem is more @@ -621,14 +633,13 @@ Troubleshooting will be the current timezone:: >>> from django.utils import timezone - >>> timezone.activate(pytz.timezone("Asia/Singapore")) + >>> timezone.activate(zoneinfo.ZoneInfo("Asia/Singapore")) # For this example, we set the time zone to Singapore, but here's how # you would obtain the current time zone in the general case. >>> current_tz = timezone.get_current_timezone() - # Again, this is the correct way to convert between time zones with pytz. - >>> local = current_tz.normalize(paris.astimezone(current_tz)) + >>> local = paris.astimezone(current_tz) >>> local - datetime.datetime(2012, 3, 3, 8, 30, tzinfo=) + datetime.datetime(2012, 3, 3, 8, 30, tzinfo=zoneinfo.ZoneInfo(key='Asia/Singapore')) >>> local.date() datetime.date(2012, 3, 3) @@ -645,18 +656,14 @@ Usage ``"Europe/Helsinki"`` **time zone. How do I turn that into an aware datetime?** - This is exactly what pytz_ is for. + Here you need to create the required ``ZoneInfo`` instance and attach it to + the naïve datetime:: + >>> import zoneinfo >>> from django.utils.dateparse import parse_datetime >>> naive = parse_datetime("2012-02-21 10:28:45") - >>> import pytz - >>> pytz.timezone("Europe/Helsinki").localize(naive, is_dst=None) - datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=) - - Note that ``localize`` is a pytz extension to the :class:`~datetime.tzinfo` - API. Also, you may want to catch ``pytz.InvalidTimeError``. The - documentation of pytz contains `more examples`_. You should review it - before attempting to manipulate aware datetimes. + >>> naive.replace(tzinfo=zoneinfo.ZoneInfo("Europe/Helsinki")) + datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=zoneinfo.ZoneInfo(key='Europe/Helsinki')) #. **How can I obtain the local time in the current time zone?** @@ -677,19 +684,14 @@ Usage >>> from django.utils import timezone >>> timezone.localtime(timezone.now()) - datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=) + datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris')) In this example, the current time zone is ``"Europe/Paris"``. #. **How can I see all available time zones?** - pytz_ provides helpers_, including a list of current time zones and a list - of all available time zones -- some of which are only of historical - interest. :mod:`zoneinfo` also provides similar functionality via - :func:`zoneinfo.available_timezones`. + :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/ -.. _more examples: http://pytz.sourceforge.net/#example-usage -.. _these issues: http://pytz.sourceforge.net/#problems-with-localtime -.. _helpers: http://pytz.sourceforge.net/#helpers -.. _tz database: https://en.wikipedia.org/wiki/Tz_database diff --git a/setup.cfg b/setup.cfg index 5cfffb5d89..cc2f2cb5a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,8 +38,9 @@ include_package_data = true zip_safe = false install_requires = asgiref >= 3.3.2 - pytz + backports.zoneinfo; python_version<"3.9" sqlparse >= 0.2.2 + tzdata; sys_platform == 'win32' [options.entry_points] console_scripts = diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index a8524379f8..9f9e924b62 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -5,15 +5,15 @@ import unittest from unittest import mock from urllib.parse import parse_qsl, urljoin, urlparse -import pytz - try: import zoneinfo except ImportError: - try: - from backports import zoneinfo - except ImportError: - zoneinfo = None + from backports import zoneinfo + +try: + import pytz +except ImportError: + pytz = None from django.contrib import admin from django.contrib.admin import AdminSite, ModelAdmin @@ -73,10 +73,10 @@ MULTIPART_ENCTYPE = 'enctype="multipart/form-data"' def make_aware_datetimes(dt, iana_key): """Makes one aware datetime for each supported time zone provider.""" - yield pytz.timezone(iana_key).localize(dt, is_dst=None) + yield dt.replace(tzinfo=zoneinfo.ZoneInfo(iana_key)) - if zoneinfo is not None: - 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/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 1bb4c9a9b1..3a57227caa 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -4,7 +4,10 @@ import re from datetime import datetime, timedelta from importlib import import_module -import pytz +try: + import zoneinfo +except ImportError: + from backports import zoneinfo from django import forms from django.conf import settings @@ -967,8 +970,8 @@ class DateTimePickerShortcutsSeleniumTests(AdminWidgetSeleniumTestCase): error_margin = timedelta(seconds=10) # If we are neighbouring a DST, we add an hour of error margin. - tz = pytz.timezone('America/Chicago') - utc_now = datetime.now(pytz.utc) + tz = zoneinfo.ZoneInfo('America/Chicago') + utc_now = datetime.now(zoneinfo.ZoneInfo('UTC')) tz_yesterday = (utc_now - timedelta(days=1)).astimezone(tz).tzname() tz_tomorrow = (utc_now + timedelta(days=1)).astimezone(tz).tzname() if tz_yesterday != tz_tomorrow: diff --git a/tests/datetimes/tests.py b/tests/datetimes/tests.py index 7f98032e0a..95828f8e09 100644 --- a/tests/datetimes/tests.py +++ b/tests/datetimes/tests.py @@ -1,9 +1,14 @@ import datetime +import unittest -import pytz +try: + import pytz +except ImportError: + pytz = None -from django.test import TestCase, override_settings +from django.test import TestCase, ignore_warnings, override_settings from django.utils import timezone +from django.utils.deprecation import RemovedInDjango50Warning from .models import Article, Category, Comment @@ -91,7 +96,9 @@ class DateTimesTests(TestCase): qs = Article.objects.datetimes('pub_date', 'second') self.assertEqual(qs[0], now) - @override_settings(USE_TZ=True, TIME_ZONE='UTC') + @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 diff --git a/tests/db_functions/datetime/test_extract_trunc.py b/tests/db_functions/datetime/test_extract_trunc.py index ced9da0c78..a129448470 100644 --- a/tests/db_functions/datetime/test_extract_trunc.py +++ b/tests/db_functions/datetime/test_extract_trunc.py @@ -1,14 +1,15 @@ +import unittest from datetime import datetime, timedelta, timezone as datetime_timezone -import pytz - try: import zoneinfo except ImportError: - try: - from backports import zoneinfo - except ImportError: - zoneinfo = None + from backports import zoneinfo + +try: + import pytz +except ImportError: + pytz = None from django.conf import settings from django.db.models import ( @@ -23,15 +24,24 @@ from django.db.models.functions import ( TruncYear, ) from django.test import ( - TestCase, override_settings, skipIfDBFeature, skipUnlessDBFeature, + TestCase, ignore_warnings, override_settings, skipIfDBFeature, + skipUnlessDBFeature, ) from django.utils import timezone +from django.utils.deprecation import RemovedInDjango50Warning from ..models import Author, DTModel, Fan -ZONE_CONSTRUCTORS = (pytz.timezone,) -if zoneinfo is not None: - ZONE_CONSTRUCTORS += (zoneinfo.ZoneInfo,) +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): @@ -98,8 +108,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 10) end_datetime = datetime(2016, 6, 15, 14, 10) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) @@ -135,8 +145,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 10) end_datetime = datetime(2016, 6, 15, 14, 10) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) @@ -158,8 +168,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 10) end_datetime = datetime(2016, 6, 15, 14, 10) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) @@ -181,8 +191,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) @@ -280,8 +290,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -319,8 +329,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -339,8 +349,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -359,14 +369,14 @@ class DateFunctionTests(TestCase): def test_extract_iso_year_func_boundaries(self): end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + end_datetime = timezone.make_aware(end_datetime) week_52_day_2014 = datetime(2014, 12, 27, 13, 0) # Sunday week_1_day_2014_2015 = datetime(2014, 12, 31, 13, 0) # Wednesday week_53_day_2015 = datetime(2015, 12, 31, 13, 0) # Thursday if settings.USE_TZ: - week_1_day_2014_2015 = timezone.make_aware(week_1_day_2014_2015, is_dst=False) - week_52_day_2014 = timezone.make_aware(week_52_day_2014, is_dst=False) - week_53_day_2015 = timezone.make_aware(week_53_day_2015, is_dst=False) + week_1_day_2014_2015 = timezone.make_aware(week_1_day_2014_2015) + week_52_day_2014 = timezone.make_aware(week_52_day_2014) + week_53_day_2015 = timezone.make_aware(week_53_day_2015) days = [week_52_day_2014, week_1_day_2014_2015, week_53_day_2015] obj_1_iso_2014 = self.create_model(week_52_day_2014, end_datetime) obj_1_iso_2015 = self.create_model(week_1_day_2014_2015, end_datetime) @@ -397,8 +407,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -417,8 +427,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -437,8 +447,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -458,8 +468,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 8, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -477,13 +487,13 @@ class DateFunctionTests(TestCase): def test_extract_quarter_func_boundaries(self): end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + end_datetime = timezone.make_aware(end_datetime) last_quarter_2014 = datetime(2014, 12, 31, 13, 0) first_quarter_2015 = datetime(2015, 1, 1, 13, 0) if settings.USE_TZ: - last_quarter_2014 = timezone.make_aware(last_quarter_2014, is_dst=False) - first_quarter_2015 = timezone.make_aware(first_quarter_2015, is_dst=False) + last_quarter_2014 = timezone.make_aware(last_quarter_2014) + first_quarter_2015 = timezone.make_aware(first_quarter_2015) dates = [last_quarter_2014, first_quarter_2015] self.create_model(last_quarter_2014, end_datetime) self.create_model(first_quarter_2015, end_datetime) @@ -498,15 +508,15 @@ class DateFunctionTests(TestCase): def test_extract_week_func_boundaries(self): end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + end_datetime = timezone.make_aware(end_datetime) week_52_day_2014 = datetime(2014, 12, 27, 13, 0) # Sunday week_1_day_2014_2015 = datetime(2014, 12, 31, 13, 0) # Wednesday week_53_day_2015 = datetime(2015, 12, 31, 13, 0) # Thursday if settings.USE_TZ: - week_1_day_2014_2015 = timezone.make_aware(week_1_day_2014_2015, is_dst=False) - week_52_day_2014 = timezone.make_aware(week_52_day_2014, is_dst=False) - week_53_day_2015 = timezone.make_aware(week_53_day_2015, is_dst=False) + week_1_day_2014_2015 = timezone.make_aware(week_1_day_2014_2015) + week_52_day_2014 = timezone.make_aware(week_52_day_2014) + week_53_day_2015 = timezone.make_aware(week_53_day_2015) days = [week_52_day_2014, week_1_day_2014_2015, week_53_day_2015] self.create_model(week_53_day_2015, end_datetime) @@ -525,8 +535,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -551,8 +561,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -586,8 +596,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -606,8 +616,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -626,8 +636,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -646,8 +656,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) @@ -752,8 +762,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'year') if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -786,10 +796,10 @@ class DateFunctionTests(TestCase): last_quarter_2015 = truncate_to(datetime(2015, 12, 31, 14, 10, 50, 123), 'quarter') first_quarter_2016 = truncate_to(datetime(2016, 1, 1, 14, 10, 50, 123), 'quarter') if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) - last_quarter_2015 = timezone.make_aware(last_quarter_2015, is_dst=False) - first_quarter_2016 = timezone.make_aware(first_quarter_2016, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) + last_quarter_2015 = timezone.make_aware(last_quarter_2015) + first_quarter_2016 = timezone.make_aware(first_quarter_2016) self.create_model(start_datetime=start_datetime, end_datetime=end_datetime) self.create_model(start_datetime=end_datetime, end_datetime=start_datetime) self.create_model(start_datetime=last_quarter_2015, end_datetime=end_datetime) @@ -825,8 +835,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'month') if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -857,8 +867,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'week') if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -881,8 +891,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -909,8 +919,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -937,8 +947,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 26) # 0 microseconds. end_datetime = datetime(2015, 6, 15, 14, 30, 26, 321) if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.assertIs( DTModel.objects.filter( @@ -962,8 +972,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'day') if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -986,8 +996,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'hour') if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -1018,8 +1028,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'minute') if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -1050,8 +1060,8 @@ class DateFunctionTests(TestCase): start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'second') if settings.USE_TZ: - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) self.assertQuerysetEqual( @@ -1085,9 +1095,9 @@ class DateFunctionTests(TestCase): fan_since_2 = datetime(2015, 2, 3, 15, 0, 0) fan_since_3 = datetime(2017, 2, 3, 15, 0, 0) if settings.USE_TZ: - fan_since_1 = timezone.make_aware(fan_since_1, is_dst=False) - fan_since_2 = timezone.make_aware(fan_since_2, is_dst=False) - fan_since_3 = timezone.make_aware(fan_since_3, is_dst=False) + fan_since_1 = timezone.make_aware(fan_since_1) + fan_since_2 = timezone.make_aware(fan_since_2) + fan_since_3 = timezone.make_aware(fan_since_3) Fan.objects.create(author=author_1, name='Tom', fan_since=fan_since_1) Fan.objects.create(author=author_1, name='Emma', fan_since=fan_since_2) Fan.objects.create(author=author_2, name='Isabella', fan_since=fan_since_3) @@ -1113,9 +1123,9 @@ class DateFunctionTests(TestCase): datetime_2 = datetime(2001, 3, 5) datetime_3 = datetime(2002, 1, 3) if settings.USE_TZ: - datetime_1 = timezone.make_aware(datetime_1, is_dst=False) - datetime_2 = timezone.make_aware(datetime_2, is_dst=False) - datetime_3 = timezone.make_aware(datetime_3, is_dst=False) + datetime_1 = timezone.make_aware(datetime_1) + datetime_2 = timezone.make_aware(datetime_2) + datetime_3 = timezone.make_aware(datetime_3) obj_1 = self.create_model(datetime_1, datetime_3) obj_2 = self.create_model(datetime_2, datetime_1) obj_3 = self.create_model(datetime_3, datetime_2) @@ -1144,8 +1154,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests): def test_extract_func_with_timezone(self): start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321) end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123) - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) 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)) @@ -1203,8 +1213,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests): def test_extract_func_explicit_timezone_priority(self): start_datetime = datetime(2015, 6, 15, 23, 30, 1, 321) end_datetime = datetime(2015, 6, 16, 13, 11, 27, 123) - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + 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'): @@ -1233,8 +1243,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests): def test_trunc_timezone_applied_before_truncation(self): start_datetime = datetime(2016, 1, 1, 1, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) for melb, pacific in zip( @@ -1263,6 +1273,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests): 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') utc = timezone.utc @@ -1294,8 +1306,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests): """ start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) - start_datetime = timezone.make_aware(start_datetime, is_dst=False) - end_datetime = timezone.make_aware(end_datetime, is_dst=False) + start_datetime = timezone.make_aware(start_datetime) + end_datetime = timezone.make_aware(end_datetime) self.create_model(start_datetime, end_datetime) self.create_model(end_datetime, start_datetime) diff --git a/tests/migrations/test_writer.py b/tests/migrations/test_writer.py index 471194ffc2..f21c560208 100644 --- a/tests/migrations/test_writer.py +++ b/tests/migrations/test_writer.py @@ -10,6 +10,16 @@ import sys import uuid from unittest import mock +try: + import zoneinfo +except ImportError: + from backports import zoneinfo + +try: + import pytz +except ImportError: + pytz = None + import custom_migration_operations.more_operations import custom_migration_operations.operations @@ -503,6 +513,22 @@ class WriterTests(SimpleTestCase): ) ) + self.assertSerializedResultEqual( + datetime.datetime(2012, 1, 1, 2, 1, tzinfo=zoneinfo.ZoneInfo('Europe/Paris')), + ( + "datetime.datetime(2012, 1, 1, 1, 1, tzinfo=utc)", + {'import datetime', 'from django.utils.timezone import utc'}, + ) + ) + if pytz: + self.assertSerializedResultEqual( + pytz.timezone('Europe/Paris').localize(datetime.datetime(2012, 1, 1, 2, 1)), + ( + "datetime.datetime(2012, 1, 1, 1, 1, tzinfo=utc)", + {'import datetime', 'from django.utils.timezone import utc'}, + ) + ) + def test_serialize_fields(self): self.assertSerializedFieldEqual(models.CharField(max_length=255)) self.assertSerializedResultEqual( diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index 899059764e..e958a984fa 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -4,7 +4,10 @@ import unittest from types import ModuleType, SimpleNamespace from unittest import mock -from django.conf import ENVIRONMENT_VARIABLE, LazySettings, Settings, settings +from django.conf import ( + ENVIRONMENT_VARIABLE, USE_DEPRECATED_PYTZ_DEPRECATED_MSG, LazySettings, + Settings, settings, +) from django.core.exceptions import ImproperlyConfigured from django.http import HttpRequest from django.test import ( @@ -348,6 +351,21 @@ class SettingsTests(SimpleTestCase): finally: del sys.modules['fake_settings_module'] + def test_use_deprecated_pytz_deprecation(self): + settings_module = ModuleType('fake_settings_module') + settings_module.USE_DEPRECATED_PYTZ = True + settings_module.USE_TZ = 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 98c5cdac33..71bc94f490 100644 --- a/tests/timezones/tests.py +++ b/tests/timezones/tests.py @@ -5,15 +5,15 @@ from contextlib import contextmanager from unittest import SkipTest, skipIf from xml.dom.minidom import parseString -import pytz - try: import zoneinfo except ImportError: - try: - from backports import zoneinfo - except ImportError: - zoneinfo = None + from backports import zoneinfo + +try: + import pytz +except ImportError: + pytz = None from django.contrib.auth.models import User from django.core import serializers @@ -61,9 +61,9 @@ UTC = timezone.utc EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok -ZONE_CONSTRUCTORS = (pytz.timezone,) -if zoneinfo is not None: - ZONE_CONSTRUCTORS += (zoneinfo.ZoneInfo,) +ZONE_CONSTRUCTORS = (zoneinfo.ZoneInfo,) +if pytz is not None: + ZONE_CONSTRUCTORS += (pytz.timezone,) def get_timezones(key): @@ -363,6 +363,23 @@ class NewDatabaseTests(TestCase): 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_convert_timezones(self): # Connection timezone is equal to the current timezone, datetime # shouldn't be converted. @@ -921,7 +938,7 @@ class TemplateTests(SimpleTestCase): tpl = Template("{% load tz %}{{ dt|timezone:tz }}") ctx = Context({ 'dt': datetime.datetime(2011, 9, 1, 13, 20, 30), - 'tz': pytz.timezone('Europe/Paris'), + 'tz': tz, }) self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00") @@ -994,11 +1011,15 @@ 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(pytz.UnknownTimeZoneError): + 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): diff --git a/tests/utils_tests/test_autoreload.py b/tests/utils_tests/test_autoreload.py index 1e43cecea9..7c28a9d4d0 100644 --- a/tests/utils_tests/test_autoreload.py +++ b/tests/utils_tests/test_autoreload.py @@ -14,7 +14,10 @@ from pathlib import Path from subprocess import CompletedProcess from unittest import mock, skip, skipIf -import pytz +try: + import zoneinfo +except ImportError: + from backports import zoneinfo import django.__main__ from django.apps.registry import Apps @@ -247,7 +250,7 @@ class TestChildArguments(SimpleTestCase): class TestUtilities(SimpleTestCase): def test_is_django_module(self): for module, expected in ( - (pytz, False), + (zoneinfo, False), (sys, False), (autoreload, True) ): @@ -256,7 +259,7 @@ class TestUtilities(SimpleTestCase): def test_is_django_path(self): for module, expected in ( - (pytz.__file__, False), + (zoneinfo.__file__, False), (contextlib.__file__, False), (autoreload.__file__, True) ): diff --git a/tests/utils_tests/test_timezone.py b/tests/utils_tests/test_timezone.py index 0972135dcc..a61bb4acb0 100644 --- a/tests/utils_tests/test_timezone.py +++ b/tests/utils_tests/test_timezone.py @@ -2,41 +2,58 @@ import datetime import unittest from unittest import mock -import pytz +try: + import pytz +except ImportError: + pytz = None try: import zoneinfo except ImportError: - try: - from backports import zoneinfo - except ImportError: - zoneinfo = None + from backports import zoneinfo -from django.test import SimpleTestCase, override_settings +from django.test import SimpleTestCase, ignore_warnings, override_settings from django.utils import timezone +from django.utils.deprecation import RemovedInDjango50Warning -CET = pytz.timezone("Europe/Paris") +PARIS_ZI = zoneinfo.ZoneInfo('Europe/Paris') EAT = timezone.get_fixed_timezone(180) # Africa/Nairobi ICT = timezone.get_fixed_timezone(420) # Asia/Bangkok UTC = datetime.timezone.utc -HAS_ZONEINFO = zoneinfo is not None +HAS_PYTZ = pytz is not None +if not HAS_PYTZ: + CET = None + PARIS_IMPLS = (PARIS_ZI,) -if not HAS_ZONEINFO: - PARIS_ZI = None - PARIS_IMPLS = (CET,) - - needs_zoneinfo = unittest.skip("Test requires zoneinfo") + needs_pytz = unittest.skip('Test requires pytz') else: - PARIS_ZI = zoneinfo.ZoneInfo('Europe/Paris') - PARIS_IMPLS = (CET, PARIS_ZI) + CET = pytz.timezone('Europe/Paris') + PARIS_IMPLS = (PARIS_ZI, CET) - def needs_zoneinfo(f): + 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())) @@ -173,13 +190,14 @@ class TimezoneTests(SimpleTestCase): timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), tz), datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=CEST)) - with self.assertRaises(ValueError): - timezone.make_aware(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET) - - if HAS_ZONEINFO: + if HAS_PYTZ: with self.assertRaises(ValueError): - timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=PARIS_ZI), PARIS_ZI) + timezone.make_aware(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET) + 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), @@ -192,7 +210,6 @@ class TimezoneTests(SimpleTestCase): with self.assertRaisesMessage(ValueError, 'make_naive() cannot be applied to a naive datetime'): timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30), CET) - @needs_zoneinfo def test_make_naive_zoneinfo(self): self.assertEqual( timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=PARIS_ZI), PARIS_ZI), @@ -204,6 +221,8 @@ 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) @@ -217,7 +236,6 @@ class TimezoneTests(SimpleTestCase): self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1)) self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2)) - @needs_zoneinfo def test_make_aware_zoneinfo_ambiguous(self): # 2:30 happens twice, once before DST ends and once after ambiguous = datetime.datetime(2015, 10, 25, 2, 30) @@ -232,6 +250,8 @@ 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) @@ -245,7 +265,6 @@ class TimezoneTests(SimpleTestCase): self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1)) self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2)) - @needs_zoneinfo def test_make_aware_zoneinfo_non_existent(self): # 2:30 never happened due to DST non_existent = datetime.datetime(2015, 3, 29, 2, 30) @@ -260,6 +279,15 @@ class TimezoneTests(SimpleTestCase): self.assertEqual(std.utcoffset(), datetime.timedelta(hours=1)) self.assertEqual(dst.utcoffset(), datetime.timedelta(hours=2)) + def test_make_aware_is_dst_deprecation_warning(self): + msg = ( + 'The is_dst argument to make_aware(), used by the Trunc() ' + 'database functions and QuerySet.datetimes(), is deprecated as it ' + 'has no effect with zoneinfo time zones.' + ) + with self.assertRaisesMessage(RemovedInDjango50Warning, msg): + timezone.make_aware(datetime.datetime(2011, 9, 1, 13, 20, 30), EAT, is_dst=True) + def test_get_timezone_name(self): """ The _get_timezone_name() helper must return the offset for fixed offset @@ -271,15 +299,15 @@ class TimezoneTests(SimpleTestCase): # datetime.timezone, fixed offset with and without `name`. (datetime.timezone(datetime.timedelta(hours=10)), 'UTC+10:00'), (datetime.timezone(datetime.timedelta(hours=10), name='Etc/GMT-10'), 'Etc/GMT-10'), - # pytz, named and fixed offset. - (pytz.timezone('Europe/Madrid'), 'Europe/Madrid'), - (pytz.timezone('Etc/GMT-10'), '+10'), + # zoneinfo, named and fixed offset. + (zoneinfo.ZoneInfo('Europe/Madrid'), 'Europe/Madrid'), + (zoneinfo.ZoneInfo('Etc/GMT-10'), '+10'), ] - if HAS_ZONEINFO: + if HAS_PYTZ: tests += [ - # zoneinfo, named and fixed offset. - (zoneinfo.ZoneInfo('Europe/Madrid'), 'Europe/Madrid'), - (zoneinfo.ZoneInfo('Etc/GMT-10'), '+10'), + # 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): @@ -288,10 +316,6 @@ class TimezoneTests(SimpleTestCase): def test_get_default_timezone(self): self.assertEqual(timezone.get_default_timezone_name(), 'America/Chicago') - def test_get_default_timezone_utc(self): - with override_settings(USE_TZ=True, TIME_ZONE='UTC'): - self.assertIs(timezone.get_default_timezone(), timezone.utc) - def test_fixedoffset_timedelta(self): delta = datetime.timedelta(hours=1) self.assertEqual(timezone.get_fixed_timezone(delta).utcoffset(None), delta)