Fixed #32365 -- Made zoneinfo the default timezone implementation.

Thanks to Adam Johnson, Aymeric Augustin, David Smith, Mariusz Felisiak, Nick
Pope, and Paul Ganssle for reviews.
This commit is contained in:
Carlton Gibson 2021-09-09 15:15:44 +02:00
parent 7132d17de1
commit 306607d5b9
27 changed files with 635 additions and 280 deletions

View File

@ -21,6 +21,13 @@ from django.utils.functional import LazyObject, empty
ENVIRONMENT_VARIABLE = "DJANGO_SETTINGS_MODULE" 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 = ( USE_L10N_DEPRECATED_MSG = (
'The USE_L10N setting is deprecated. Starting with Django 5.0, localized ' 'The USE_L10N setting is deprecated. Starting with Django 5.0, localized '
'formatting of data will always be enabled. For example Django will ' 'formatting of data will always be enabled. For example Django will '
@ -196,6 +203,9 @@ class Settings:
category=RemovedInDjango50Warning, 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: if hasattr(time, 'tzset') and self.TIME_ZONE:
# When we can, attempt to validate the timezone. If we can't find # When we can, attempt to validate the timezone. If we can't find
# this file, no check happens and it's harmless. # this file, no check happens and it's harmless.
@ -245,6 +255,8 @@ class UserSettingsHolder:
if name == 'USE_L10N': if name == 'USE_L10N':
warnings.warn(USE_L10N_DEPRECATED_MSG, RemovedInDjango50Warning) warnings.warn(USE_L10N_DEPRECATED_MSG, RemovedInDjango50Warning)
super().__setattr__(name, value) super().__setattr__(name, value)
if name == 'USE_DEPRECATED_PYTZ':
warnings.warn(USE_DEPRECATED_PYTZ_DEPRECATED_MSG, RemovedInDjango50Warning)
def __delattr__(self, name): def __delattr__(self, name):
self._deleted.add(name) self._deleted.add(name)

View File

@ -43,6 +43,11 @@ 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 = False 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: # 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,5 +1,6 @@
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_value, get_fields_from_path, 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] 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} qs_kwargs = {'is_dst': True} if settings.USE_DEPRECATED_PYTZ else {}
else: else:
dates_or_datetimes = 'dates' dates_or_datetimes = 'dates'
qs_kwargs = {} qs_kwargs = {}

View File

@ -6,7 +6,10 @@ import warnings
from collections import deque from collections import deque
from contextlib import contextmanager from contextlib import contextmanager
import pytz try:
import zoneinfo
except ImportError:
from backports import zoneinfo
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
@ -23,6 +26,14 @@ from django.utils.functional import cached_property
NO_DB_ALIAS = '__no_db__' 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: class BaseDatabaseWrapper:
"""Represent a database connection.""" """Represent a database connection."""
# Mapping of Field objects to their column types. # Mapping of Field objects to their column types.
@ -135,7 +146,7 @@ class BaseDatabaseWrapper:
elif self.settings_dict['TIME_ZONE'] is None: elif self.settings_dict['TIME_ZONE'] is None:
return timezone.utc return timezone.utc
else: else:
return pytz.timezone(self.settings_dict['TIME_ZONE']) return timezone_constructor(self.settings_dict['TIME_ZONE'])
@cached_property @cached_property
def timezone_name(self): def timezone_name(self):

View File

@ -14,12 +14,12 @@ import warnings
from itertools import chain from itertools import chain
from sqlite3 import dbapi2 as Database from sqlite3 import dbapi2 as Database
import pytz
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import IntegrityError from django.db import IntegrityError
from django.db.backends import utils as backend_utils 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 import timezone
from django.utils.asyncio import async_unsafe from django.utils.asyncio import async_unsafe
from django.utils.dateparse import parse_datetime, parse_time 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): except (TypeError, ValueError):
return None return None
if conn_tzname: 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: if tzname is not None and tzname != conn_tzname:
sign_index = tzname.find('+') + tzname.find('-') + 1 sign_index = tzname.find('+') + tzname.find('-') + 1
if sign_index > -1: if sign_index > -1:
@ -441,7 +441,7 @@ def _sqlite_datetime_parse(dt, tzname=None, conn_tzname=None):
hours, minutes = offset.split(':') hours, minutes = offset.split(':')
offset_delta = datetime.timedelta(hours=int(hours), minutes=int(minutes)) offset_delta = datetime.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, pytz.timezone(tzname)) dt = timezone.localtime(dt, timezone_constructor(tzname))
return dt return dt

View File

@ -67,7 +67,7 @@ class DatetimeDatetimeSerializer(BaseSerializer):
imports = ["import datetime"] imports = ["import datetime"]
if self.value.tzinfo is not None: if self.value.tzinfo is not None:
imports.append("from django.utils.timezone import utc") imports.append("from django.utils.timezone import utc")
return repr(self.value).replace('<UTC>', 'utc'), set(imports) return repr(self.value).replace('datetime.timezone.utc', 'utc'), set(imports)
class DecimalSerializer(BaseSerializer): class DecimalSerializer(BaseSerializer):

View File

@ -188,7 +188,9 @@ class TruncBase(TimezoneMixin, Transform):
kind = None kind = None
tzinfo = 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.tzinfo = tzinfo
self.is_dst = is_dst self.is_dst = is_dst
super().__init__(expression, output_field=output_field, **extra) super().__init__(expression, output_field=output_field, **extra)
@ -264,7 +266,9 @@ class TruncBase(TimezoneMixin, Transform):
class Trunc(TruncBase): 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 self.kind = kind
super().__init__( super().__init__(
expression, output_field=output_field, tzinfo=tzinfo, expression, output_field=output_field, tzinfo=tzinfo,

View File

@ -916,7 +916,9 @@ class QuerySet:
'datefield', flat=True 'datefield', flat=True
).distinct().filter(plain_field__isnull=False).order_by(('-' if order == 'DESC' else '') + 'datefield') ).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 Return a list of datetime objects representing all available
datetimes for the given field_name, scoped to 'kind'. datetimes for the given field_name, scoped to 'kind'.

View File

@ -1,13 +1,37 @@
from datetime import datetime, tzinfo 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.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):
@ -61,8 +85,8 @@ def do_timezone(value, arg):
tz = arg tz = arg
elif isinstance(arg, str): elif isinstance(arg, str):
try: try:
tz = pytz.timezone(arg) tz = timezone_constructor(arg)
except pytz.UnknownTimeZoneError: except UnknownTimezoneException:
return '' return ''
else: else:
return '' return ''

View File

@ -3,13 +3,21 @@ Timezone-related classes and functions.
""" """
import functools import functools
import sys
import warnings
try:
import zoneinfo
except ImportError:
from backports import zoneinfo
from contextlib import ContextDecorator from contextlib import ContextDecorator
from datetime import datetime, timedelta, timezone, tzinfo from datetime import datetime, timedelta, timezone, tzinfo
import pytz
from asgiref.local import Local from asgiref.local import Local
from django.conf import settings from django.conf import settings
from django.utils.deprecation import RemovedInDjango50Warning
__all__ = [ __all__ = [
'utc', 'get_fixed_timezone', 'utc', 'get_fixed_timezone',
@ -20,14 +28,11 @@ __all__ = [
'is_aware', 'is_naive', 'make_aware', 'make_naive', '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) utc = timezone.utc
# In releases prior to 2018.4, pytz.UTC was not a subclass of BaseTzInfo
if not isinstance(pytz.UTC, pytz._FixedOffset):
_PYTZ_BASE_CLASSES = _PYTZ_BASE_CLASSES + (type(pytz.UTC),)
def get_fixed_timezone(offset): def get_fixed_timezone(offset):
@ -49,7 +54,10 @@ def get_default_timezone():
This is the time zone defined by settings.TIME_ZONE. 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 # This function exists for consistency with get_current_timezone_name
@ -94,7 +102,11 @@ 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):
_active.value = pytz.timezone(timezone) if settings.USE_DEPRECATED_PYTZ:
import pytz
_active.value = pytz.timezone(timezone)
else:
_active.value = zoneinfo.ZoneInfo(timezone)
else: else:
raise ValueError("Invalid timezone: %r" % timezone) raise ValueError("Invalid timezone: %r" % timezone)
@ -229,8 +241,17 @@ def is_naive(value):
return value.utcoffset() is None 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.""" """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: if timezone is None:
timezone = get_current_timezone() timezone = get_current_timezone()
if _is_pytz_zone(timezone): if _is_pytz_zone(timezone):
@ -255,13 +276,45 @@ 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): def _is_pytz_zone(tz):
"""Checks if a zone is a pytz zone.""" """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) 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): if _is_pytz_zone(tz):
import pytz
try: try:
tz.utcoffset(dt) tz.utcoffset(dt)
except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError): except (pytz.AmbiguousTimeError, pytz.NonExistentTimeError):

View File

@ -36,6 +36,24 @@ details on these changes.
* The ``USE_L10N`` setting will be removed. * 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: .. _deprecation-removed-in-4.1:
4.1 4.1

View File

@ -242,7 +242,8 @@ Takes an ``expression`` representing a ``DateField``, ``DateTimeField``,
of the date referenced by ``lookup_name`` as an ``IntegerField``. of the date referenced by ``lookup_name`` as an ``IntegerField``.
Django usually uses the databases' extract function, so you may use any Django usually uses the databases' extract function, so you may use any
``lookup_name`` that your database supports. A ``tzinfo`` subclass, usually ``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 Given the datetime ``2015-06-15 23:30:01.000321+00:00``, the built-in
``lookup_name``\s return: ``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 the Melbourne timezone (UTC +10:00), which changes the day, weekday, and hour
values that are returned:: values that are returned::
>>> import pytz >>> import zoneinfo
>>> melb = pytz.timezone('Australia/Melbourne') # UTC+10:00 >>> melb = zoneinfo.ZoneInfo('Australia/Melbourne') # UTC+10:00
>>> with timezone.override(melb): >>> with timezone.override(melb):
... Experiment.objects.annotate( ... Experiment.objects.annotate(
... day=ExtractDay('start_datetime'), ... day=ExtractDay('start_datetime'),
@ -466,8 +467,8 @@ values that are returned::
Explicitly passing the timezone to the ``Extract`` function behaves in the same Explicitly passing the timezone to the ``Extract`` function behaves in the same
way, and takes priority over an active timezone:: way, and takes priority over an active timezone::
>>> import pytz >>> import zoneinfo
>>> melb = pytz.timezone('Australia/Melbourne') >>> melb = zoneinfo.ZoneInfo('Australia/Melbourne')
>>> Experiment.objects.annotate( >>> Experiment.objects.annotate(
... day=ExtractDay('start_datetime', tzinfo=melb), ... day=ExtractDay('start_datetime', tzinfo=melb),
... weekday=ExtractWeekDay('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 ``TimeField()``, or ``DateField()``. It returns a datetime, date, or time
depending on ``output_field``, with fields up to ``kind`` set to their minimum 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`` value. If ``output_field`` is omitted, it will default to the ``output_field``
of ``expression``. A ``tzinfo`` subclass, usually provided by ``pytz``, can be of ``expression``. A ``tzinfo`` subclass, usually provided by :mod:`zoneinfo`,
passed to truncate a value in a specific timezone. can be passed to truncate a value in a specific timezone.
The ``is_dst`` parameter indicates whether or not ``pytz`` should interpret .. deprecated:: 4.0
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 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 Given the datetime ``2015-06-15 14:30:50.000321+00:00``, the built-in ``kind``\s
return: return:
@ -607,6 +612,10 @@ Usage example::
.. attribute:: kind = 'quarter' .. 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 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 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 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 2014-01-01 1
2015-01-01 2 2015-01-01 2
>>> import pytz >>> import zoneinfo
>>> melb = pytz.timezone('Australia/Melbourne') >>> melb = zoneinfo.ZoneInfo('Australia/Melbourne')
>>> experiments_per_month = Experiment.objects.annotate( >>> experiments_per_month = Experiment.objects.annotate(
... month=TruncMonth('start_datetime', tzinfo=melb)).values('month').annotate( ... month=TruncMonth('start_datetime', tzinfo=melb)).values('month').annotate(
... experiments=Count('id')) ... experiments=Count('id'))
@ -691,6 +700,10 @@ truncate function. It's also registered as a transform on ``DateTimeField`` as
.. attribute:: kind = 'second' .. 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 These are logically equivalent to ``Trunc('datetime_field', kind)``. They
truncate all parts of the date up to ``kind`` and allow grouping or filtering 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 datetimes with less precision. ``expression`` must have an ``output_field`` of
@ -704,10 +717,10 @@ Usage example::
... TruncDate, TruncDay, TruncHour, TruncMinute, TruncSecond, ... TruncDate, TruncDay, TruncHour, TruncMinute, TruncSecond,
... ) ... )
>>> from django.utils import timezone >>> from django.utils import timezone
>>> import pytz >>> import zoneinfo
>>> start1 = datetime(2014, 6, 15, 14, 30, 50, 321, tzinfo=timezone.utc) >>> start1 = datetime(2014, 6, 15, 14, 30, 50, 321, tzinfo=timezone.utc)
>>> Experiment.objects.create(start_datetime=start1, start_date=start1.date()) >>> Experiment.objects.create(start_datetime=start1, start_date=start1.date())
>>> melb = pytz.timezone('Australia/Melbourne') >>> melb = zoneinfo.ZoneInfo('Australia/Melbourne')
>>> Experiment.objects.annotate( >>> Experiment.objects.annotate(
... date=TruncDate('start_datetime'), ... date=TruncDate('start_datetime'),
... day=TruncDay('start_datetime', tzinfo=melb), ... day=TruncDay('start_datetime', tzinfo=melb),
@ -716,10 +729,10 @@ Usage example::
... second=TruncSecond('start_datetime'), ... second=TruncSecond('start_datetime'),
... ).values('date', 'day', 'hour', 'minute', 'second').get() ... ).values('date', 'day', 'hour', 'minute', 'second').get()
{'date': datetime.date(2014, 6, 15), {'date': datetime.date(2014, 6, 15),
'day': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=<DstTzInfo 'Australia/Melbourne' AEST+10:00:00 STD>), 'day': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=zoneinfo.ZoneInfo('Australia/Melbourne')),
'hour': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=<DstTzInfo 'Australia/Melbourne' AEST+10:00:00 STD>), 'hour': datetime.datetime(2014, 6, 16, 0, 0, tzinfo=zoneinfo.ZoneInfo('Australia/Melbourne')),
'minute': 'minute': datetime.datetime(2014, 6, 15, 14, 30, tzinfo=<UTC>), 'minute': 'minute': datetime.datetime(2014, 6, 15, 14, 30, tzinfo=zoneinfo.ZoneInfo('UTC')),
'second': datetime.datetime(2014, 6, 15, 14, 30, 50, tzinfo=<UTC>) 'second': datetime.datetime(2014, 6, 15, 14, 30, 50, tzinfo=zoneinfo.ZoneInfo('UTC'))
} }
``TimeField`` truncation ``TimeField`` truncation
@ -740,6 +753,10 @@ Usage example::
.. attribute:: kind = 'second' .. 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 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 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 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 14:00:00 2
17:00:00 1 17:00:00 1
>>> import pytz >>> import zoneinfo
>>> melb = pytz.timezone('Australia/Melbourne') >>> melb = zoneinfo.ZoneInfo('Australia/Melbourne')
>>> experiments_per_hour = Experiment.objects.annotate( >>> experiments_per_hour = Experiment.objects.annotate(
... hour=TruncHour('start_datetime', tzinfo=melb), ... hour=TruncHour('start_datetime', tzinfo=melb),
... ).values('hour').annotate(experiments=Count('id')) ... ).values('hour').annotate(experiments=Count('id'))

View File

@ -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``), ambiguous datetimes in daylight saving time. By default (when ``is_dst=None``),
``pytz`` raises an exception for such datetimes. ``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: .. _database-time-zone-definitions:
.. note:: .. 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 As a consequence, your database must be able to interpret the value of
``tzinfo.tzname(None)``. This translates into the following requirements: ``tzinfo.tzname(None)``. This translates into the following requirements:
- SQLite: no requirements. Conversions are performed in Python with pytz_ - SQLite: no requirements. Conversions are performed in Python.
(installed when you install Django).
- PostgreSQL: no requirements (see `Time Zones`_). - PostgreSQL: no requirements (see `Time Zones`_).
- Oracle: no requirements (see `Choosing a Time Zone File`_). - Oracle: no requirements (see `Choosing a Time Zone File`_).
- MySQL: load the time zone tables with `mysql_tzinfo_to_sql`_. - 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 .. _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/ .. _Choosing a Time Zone File: https://docs.oracle.com/en/database/oracle/
oracle-database/18/nlspg/datetime-data-types-and-time-zone-support.html oracle-database/18/nlspg/datetime-data-types-and-time-zone-support.html

View File

@ -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 otherwise, you should leave this option unset. It's best to store datetimes
in UTC because it avoids ambiguous or nonexistent datetimes during daylight in UTC because it avoids ambiguous or nonexistent datetimes during daylight
saving time changes. Also, receiving datetimes in UTC keeps datetime saving time changes. Also, receiving datetimes in UTC keeps datetime
arithmetic simple — there's no need for the ``normalize()`` method provided arithmetic simple — there's no need to consider potential offset changes
by pytz. over a DST transition.
* If you're connecting to a third-party database that stores datetimes in a * 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 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. as ``date_trunc``, because their results depend on the time zone.
However, this has a downside: receiving all datetimes in local time makes However, this has a downside: receiving all datetimes in local time makes
datetime arithmetic more tricky — you must call the ``normalize()`` method datetime arithmetic more tricky — you must account for possible offset
provided by pytz after each operation. changes over DST transitions.
Consider converting to local time explicitly with ``AT TIME ZONE`` in raw SQL Consider converting to local time explicitly with ``AT TIME ZONE`` in raw SQL
queries instead of setting the ``TIME_ZONE`` option. 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 .. _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 .. setting:: USE_I18N
``USE_I18N`` ``USE_I18N``

View File

@ -941,24 +941,30 @@ appropriate entities.
:class:`~datetime.datetime`. If ``timezone`` is set to ``None``, it :class:`~datetime.datetime`. If ``timezone`` is set to ``None``, it
defaults to the :ref:`current time zone <default-current-time-zone>`. defaults to the :ref:`current time zone <default-current-time-zone>`.
When using ``pytz``, the ``pytz.AmbiguousTimeError`` exception is raised if .. deprecated:: 4.0
you try to make ``value`` aware during a DST transition where the same time
occurs twice (when reverting from DST). Setting ``is_dst`` to ``True`` or
``False`` will avoid the exception by choosing if the time is
pre-transition or post-transition respectively.
When using ``pytz``, the ``pytz.NonExistentTimeError`` exception is raised When using ``pytz``, the ``pytz.AmbiguousTimeError`` exception is
if you try to make ``value`` aware during a DST transition such that the raised if you try to make ``value`` aware during a DST transition where
time never occurred. For example, if the 2:00 hour is skipped during a DST the same time occurs twice (when reverting from DST). Setting
transition, trying to make 2:30 aware in that time zone will raise an ``is_dst`` to ``True`` or ``False`` will avoid the exception by
exception. To avoid that you can use ``is_dst`` to specify how choosing if the time is pre-transition or post-transition respectively.
``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 When using ``pytz``, the ``pytz.NonExistentTimeError`` exception is
implementations. 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) .. function:: make_naive(value, timezone=None)

View File

@ -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 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 Functional unique constraints
----------------------------- -----------------------------
@ -595,11 +635,37 @@ Miscellaneous
* The default value of the ``USE_L10N`` setting is changed to ``True``. See the * The default value of the ``USE_L10N`` setting is changed to ``True``. See the
:ref:`Localization section <use_l10n_deprecation>` above for more details. :ref:`Localization section <use_l10n_deprecation>` above for more details.
* As part of the :ref:`move to zoneinfo <whats-new-4.0>`,
:attr:`django.utils.timezone.utc` is changed to alias
:attr:`datetime.timezone.utc`.
.. _deprecated-features-4.0: .. _deprecated-features-4.0:
Features deprecated in 4.0 Features deprecated in 4.0
========================== ==========================
Use of ``pytz`` time zones
--------------------------
As part of the :ref:`move to zoneinfo <whats-new-4.0>`, 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 Time zone support
----------------- -----------------

View File

@ -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 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, 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. you're likely to encounter errors twice a year, when the transitions happen.
(The pytz_ documentation discusses `these issues`_ in greater detail.) This This probably doesn't matter for your blog, but it's a problem if you over bill
probably doesn't matter for your blog, but it's a problem if you over-bill or or under bill your customers by one hour, twice a year, every year. The
under-bill your customers by one hour, twice a year, every year. The solution solution to this problem is to use UTC in the code and use local time only when
to this problem is to use UTC in the code and use local time only when
interacting with end users. interacting with end users.
Time zone support is disabled by default. To enable it, set :setting:`USE_TZ = Time zone support is disabled by default. To enable it, set :setting:`USE_TZ =
@ -32,15 +31,20 @@ True <USE_TZ>` in your settings file.
In Django 5.0, time zone support will be enabled by default. 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 Time zone support uses :mod:`zoneinfo`, which is part of the Python standard
Django; Django also supports the use of other time zone implementations like library from Python 3.9. The ``backports.zoneinfo`` package is automatically
:mod:`zoneinfo` by passing :class:`~datetime.tzinfo` objects directly to installed alongside Django if you are using Python 3.8.
functions in :mod:`django.utils.timezone`.
.. versionchanged:: 3.2 .. versionchanged:: 3.2
Support for non-``pytz`` timezone implementations was added. 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:: .. note::
The default :file:`settings.py` file created by :djadmin:`django-admin 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, Dealing with aware datetime objects isn't always intuitive. For instance,
the ``tzinfo`` argument of the standard datetime constructor doesn't work 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 reliably for time zones with DST. Using UTC is generally safe; if you're
using other time zones, you should review the `pytz`_ documentation using other time zones, you should review the :mod:`zoneinfo`
carefully. documentation carefully.
.. note:: .. note::
@ -113,8 +117,10 @@ receives one, it attempts to make it aware by interpreting it in the
:ref:`default time zone <default-current-time-zone>` and raises a warning. :ref:`default time zone <default-current-time-zone>` and raises a warning.
Unfortunately, during DST transitions, some datetimes don't exist or are Unfortunately, during DST transitions, some datetimes don't exist or are
ambiguous. In such situations, pytz_ raises an exception. That's why you should ambiguous. That's why you should always create aware datetime objects when time
always create aware datetime objects when time zone support is enabled. zone support is enabled. (See the :mod:`Using ZoneInfo section of the zoneinfo
docs <zoneinfo>` 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 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 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 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 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 the time zone of their primary audience or UTC.
list of time zones per country, that you can use to pre-select the most likely :func:`zoneinfo.available_timezones` provides a set of available timezones that
choices. 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 Here's an example that stores the current timezone in the session. (It skips
error handling entirely for the sake of simplicity.) error handling entirely for the sake of simplicity.)
Add the following middleware to :setting:`MIDDLEWARE`:: Add the following middleware to :setting:`MIDDLEWARE`::
import pytz import zoneinfo
from django.utils import timezone from django.utils import timezone
@ -183,7 +189,7 @@ Add the following middleware to :setting:`MIDDLEWARE`::
def __call__(self, request): def __call__(self, request):
tzname = request.session.get('django_timezone') tzname = request.session.get('django_timezone')
if tzname: if tzname:
timezone.activate(pytz.timezone(tzname)) timezone.activate(zoneinfo.ZoneInfo(tzname))
else: else:
timezone.deactivate() timezone.deactivate()
return self.get_response(request) return self.get_response(request)
@ -192,12 +198,19 @@ Create a view that can set the current timezone::
from django.shortcuts import redirect, render 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): def set_timezone(request):
if request.method == 'POST': if request.method == 'POST':
request.session['django_timezone'] = request.POST['timezone'] request.session['django_timezone'] = request.POST['timezone']
return redirect('/') return redirect('/')
else: 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: 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 %} {% csrf_token %}
<label for="timezone">Time zone:</label> <label for="timezone">Time zone:</label>
<select name="timezone"> <select name="timezone">
{% for tz in timezones %} {% for city, tz in timezones %}
<option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ tz }}</option> <option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ city }}</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="submit" value="Set"> <input type="submit" value="Set">
@ -225,9 +238,8 @@ When you enable time zone support, Django interprets datetimes entered in
forms in the :ref:`current time zone <default-current-time-zone>` and returns forms in the :ref:`current time zone <default-current-time-zone>` and returns
aware datetime objects in ``cleaned_data``. aware datetime objects in ``cleaned_data``.
If the current time zone raises an exception for datetimes that don't exist or Converted datetimes that don't exist or are ambiguous because they fall in a
are ambiguous because they fall in a DST transition (the timezones provided by DST transition will be reported as invalid values.
pytz_ do this), such datetimes will be reported as invalid values.
.. _time-zones-in-templates: .. _time-zones-in-templates:
@ -583,20 +595,20 @@ Troubleshooting
None of this is true in a time zone aware environment:: None of this is true in a time zone aware environment::
>>> import datetime >>> import datetime
>>> import pytz >>> import zoneinfo
>>> paris_tz = pytz.timezone("Europe/Paris") >>> paris_tz = zoneinfo.ZoneInfo("Europe/Paris")
>>> new_york_tz = pytz.timezone("America/New_York") >>> new_york_tz = zoneinfo.ZoneInfo("America/New_York")
>>> paris = paris_tz.localize(datetime.datetime(2012, 3, 3, 1, 30)) >>> paris = datetime.datetime(2012, 3, 3, 1, 30, tzinfo=paris_tz)
# This is the correct way to convert between time zones with pytz. # This is the correct way to convert between time zones.
>>> new_york = new_york_tz.normalize(paris.astimezone(new_york_tz)) >>> new_york = paris.astimezone(new_york_tz)
>>> paris == new_york, paris.date() == new_york.date() >>> paris == new_york, paris.date() == new_york.date()
(True, False) (True, False)
>>> paris - new_york, paris.date() - new_york.date() >>> paris - new_york, paris.date() - new_york.date()
(datetime.timedelta(0), datetime.timedelta(1)) (datetime.timedelta(0), datetime.timedelta(1))
>>> paris >>> paris
datetime.datetime(2012, 3, 3, 1, 30, tzinfo=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>) datetime.datetime(2012, 3, 3, 1, 30, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris'))
>>> new_york >>> new_york
datetime.datetime(2012, 3, 2, 19, 30, tzinfo=<DstTzInfo 'America/New_York' EST-1 day, 19:00:00 STD>) 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 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 the time zone in which it is represented. But the real problem is more
@ -621,14 +633,13 @@ Troubleshooting
will be the current timezone:: will be the current timezone::
>>> from django.utils import 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 # 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. # you would obtain the current time zone in the general case.
>>> current_tz = timezone.get_current_timezone() >>> current_tz = timezone.get_current_timezone()
# Again, this is the correct way to convert between time zones with pytz. >>> local = paris.astimezone(current_tz)
>>> local = current_tz.normalize(paris.astimezone(current_tz))
>>> local >>> local
datetime.datetime(2012, 3, 3, 8, 30, tzinfo=<DstTzInfo 'Asia/Singapore' SGT+8:00:00 STD>) datetime.datetime(2012, 3, 3, 8, 30, tzinfo=zoneinfo.ZoneInfo(key='Asia/Singapore'))
>>> local.date() >>> local.date()
datetime.date(2012, 3, 3) datetime.date(2012, 3, 3)
@ -645,18 +656,14 @@ Usage
``"Europe/Helsinki"`` **time zone. How do I turn that into an aware ``"Europe/Helsinki"`` **time zone. How do I turn that into an aware
datetime?** 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 >>> from django.utils.dateparse import parse_datetime
>>> naive = parse_datetime("2012-02-21 10:28:45") >>> naive = parse_datetime("2012-02-21 10:28:45")
>>> import pytz >>> naive.replace(tzinfo=zoneinfo.ZoneInfo("Europe/Helsinki"))
>>> pytz.timezone("Europe/Helsinki").localize(naive, is_dst=None) datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=zoneinfo.ZoneInfo(key='Europe/Helsinki'))
datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=<DstTzInfo 'Europe/Helsinki' EET+2:00:00 STD>)
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.
#. **How can I obtain the local time in the current time zone?** #. **How can I obtain the local time in the current time zone?**
@ -677,19 +684,14 @@ Usage
>>> from django.utils import timezone >>> from django.utils import timezone
>>> timezone.localtime(timezone.now()) >>> timezone.localtime(timezone.now())
datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>) 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"``. In this example, the current time zone is ``"Europe/Paris"``.
#. **How can I see all available time zones?** #. **How can I see all available time zones?**
pytz_ provides helpers_, including a list of current time zones and a list :func:`zoneinfo.available_timezones` provides the set of all valid keys for
of all available time zones -- some of which are only of historical IANA time zones available to your system. See the docs for usage
interest. :mod:`zoneinfo` also provides similar functionality via considerations.
:func:`zoneinfo.available_timezones`.
.. _pytz: http://pytz.sourceforge.net/ .. _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

View File

@ -38,8 +38,9 @@ include_package_data = true
zip_safe = false zip_safe = false
install_requires = install_requires =
asgiref >= 3.3.2 asgiref >= 3.3.2
pytz backports.zoneinfo; python_version<"3.9"
sqlparse >= 0.2.2 sqlparse >= 0.2.2
tzdata; sys_platform == 'win32'
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =

View File

@ -5,15 +5,15 @@ import unittest
from unittest import mock from unittest import mock
from urllib.parse import parse_qsl, urljoin, urlparse from urllib.parse import parse_qsl, urljoin, urlparse
import pytz
try: try:
import zoneinfo import zoneinfo
except ImportError: except ImportError:
try: from backports import zoneinfo
from backports import zoneinfo
except ImportError: try:
zoneinfo = None 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
@ -73,10 +73,10 @@ MULTIPART_ENCTYPE = 'enctype="multipart/form-data"'
def make_aware_datetimes(dt, iana_key): 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 pytz.timezone(iana_key).localize(dt, is_dst=None) yield dt.replace(tzinfo=zoneinfo.ZoneInfo(iana_key))
if zoneinfo is not None: if pytz is not None:
yield dt.replace(tzinfo=zoneinfo.ZoneInfo(iana_key)) yield pytz.timezone(iana_key).localize(dt, is_dst=None)
class AdminFieldExtractionMixin: class AdminFieldExtractionMixin:

View File

@ -4,7 +4,10 @@ import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from importlib import import_module from importlib import import_module
import pytz try:
import zoneinfo
except ImportError:
from backports import zoneinfo
from django import forms from django import forms
from django.conf import settings from django.conf import settings
@ -967,8 +970,8 @@ class DateTimePickerShortcutsSeleniumTests(AdminWidgetSeleniumTestCase):
error_margin = timedelta(seconds=10) error_margin = timedelta(seconds=10)
# If we are neighbouring a DST, we add an hour of error margin. # If we are neighbouring a DST, we add an hour of error margin.
tz = pytz.timezone('America/Chicago') tz = zoneinfo.ZoneInfo('America/Chicago')
utc_now = datetime.now(pytz.utc) utc_now = datetime.now(zoneinfo.ZoneInfo('UTC'))
tz_yesterday = (utc_now - timedelta(days=1)).astimezone(tz).tzname() tz_yesterday = (utc_now - timedelta(days=1)).astimezone(tz).tzname()
tz_tomorrow = (utc_now + timedelta(days=1)).astimezone(tz).tzname() tz_tomorrow = (utc_now + timedelta(days=1)).astimezone(tz).tzname()
if tz_yesterday != tz_tomorrow: if tz_yesterday != tz_tomorrow:

View File

@ -1,9 +1,14 @@
import datetime 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 import timezone
from django.utils.deprecation import RemovedInDjango50Warning
from .models import Article, Category, Comment from .models import Article, Category, Comment
@ -91,7 +96,9 @@ 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)
@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): def test_datetimes_ambiguous_and_invalid_times(self):
sao = pytz.timezone('America/Sao_Paulo') sao = pytz.timezone('America/Sao_Paulo')
utc = pytz.UTC utc = pytz.UTC

View File

@ -1,14 +1,15 @@
import unittest
from datetime import datetime, timedelta, timezone as datetime_timezone from datetime import datetime, timedelta, timezone as datetime_timezone
import pytz
try: try:
import zoneinfo import zoneinfo
except ImportError: except ImportError:
try: from backports import zoneinfo
from backports import zoneinfo
except ImportError: try:
zoneinfo = None import pytz
except ImportError:
pytz = None
from django.conf import settings from django.conf import settings
from django.db.models import ( from django.db.models import (
@ -23,15 +24,24 @@ from django.db.models.functions import (
TruncYear, TruncYear,
) )
from django.test import ( from django.test import (
TestCase, override_settings, skipIfDBFeature, skipUnlessDBFeature, TestCase, ignore_warnings, override_settings, skipIfDBFeature,
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
ZONE_CONSTRUCTORS = (pytz.timezone,) HAS_PYTZ = pytz is not None
if zoneinfo is not None: if not HAS_PYTZ:
ZONE_CONSTRUCTORS += (zoneinfo.ZoneInfo,) 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):
@ -98,8 +108,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 10) start_datetime = datetime(2015, 6, 15, 14, 10)
end_datetime = datetime(2016, 6, 15, 14, 10) end_datetime = datetime(2016, 6, 15, 14, 10)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
@ -135,8 +145,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 10) start_datetime = datetime(2015, 6, 15, 14, 10)
end_datetime = datetime(2016, 6, 15, 14, 10) end_datetime = datetime(2016, 6, 15, 14, 10)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
@ -158,8 +168,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 10) start_datetime = datetime(2015, 6, 15, 14, 10)
end_datetime = datetime(2016, 6, 15, 14, 10) end_datetime = datetime(2016, 6, 15, 14, 10)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
@ -181,8 +191,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
@ -280,8 +290,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -319,8 +329,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -339,8 +349,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -359,14 +369,14 @@ class DateFunctionTests(TestCase):
def test_extract_iso_year_func_boundaries(self): def test_extract_iso_year_func_boundaries(self):
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: 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_52_day_2014 = datetime(2014, 12, 27, 13, 0) # Sunday
week_1_day_2014_2015 = datetime(2014, 12, 31, 13, 0) # Wednesday week_1_day_2014_2015 = datetime(2014, 12, 31, 13, 0) # Wednesday
week_53_day_2015 = datetime(2015, 12, 31, 13, 0) # Thursday week_53_day_2015 = datetime(2015, 12, 31, 13, 0) # Thursday
if settings.USE_TZ: if settings.USE_TZ:
week_1_day_2014_2015 = timezone.make_aware(week_1_day_2014_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, is_dst=False) week_52_day_2014 = timezone.make_aware(week_52_day_2014)
week_53_day_2015 = timezone.make_aware(week_53_day_2015, is_dst=False) 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] 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_2014 = self.create_model(week_52_day_2014, end_datetime)
obj_1_iso_2015 = self.create_model(week_1_day_2014_2015, 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) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -417,8 +427,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -437,8 +447,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -458,8 +468,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 8, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 8, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -477,13 +487,13 @@ class DateFunctionTests(TestCase):
def test_extract_quarter_func_boundaries(self): def test_extract_quarter_func_boundaries(self):
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: 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) last_quarter_2014 = datetime(2014, 12, 31, 13, 0)
first_quarter_2015 = datetime(2015, 1, 1, 13, 0) first_quarter_2015 = datetime(2015, 1, 1, 13, 0)
if settings.USE_TZ: if settings.USE_TZ:
last_quarter_2014 = timezone.make_aware(last_quarter_2014, is_dst=False) last_quarter_2014 = timezone.make_aware(last_quarter_2014)
first_quarter_2015 = timezone.make_aware(first_quarter_2015, is_dst=False) first_quarter_2015 = timezone.make_aware(first_quarter_2015)
dates = [last_quarter_2014, first_quarter_2015] dates = [last_quarter_2014, first_quarter_2015]
self.create_model(last_quarter_2014, end_datetime) self.create_model(last_quarter_2014, end_datetime)
self.create_model(first_quarter_2015, end_datetime) self.create_model(first_quarter_2015, end_datetime)
@ -498,15 +508,15 @@ class DateFunctionTests(TestCase):
def test_extract_week_func_boundaries(self): def test_extract_week_func_boundaries(self):
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: 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_52_day_2014 = datetime(2014, 12, 27, 13, 0) # Sunday
week_1_day_2014_2015 = datetime(2014, 12, 31, 13, 0) # Wednesday week_1_day_2014_2015 = datetime(2014, 12, 31, 13, 0) # Wednesday
week_53_day_2015 = datetime(2015, 12, 31, 13, 0) # Thursday week_53_day_2015 = datetime(2015, 12, 31, 13, 0) # Thursday
if settings.USE_TZ: if settings.USE_TZ:
week_1_day_2014_2015 = timezone.make_aware(week_1_day_2014_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, is_dst=False) week_52_day_2014 = timezone.make_aware(week_52_day_2014)
week_53_day_2015 = timezone.make_aware(week_53_day_2015, is_dst=False) 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] days = [week_52_day_2014, week_1_day_2014_2015, week_53_day_2015]
self.create_model(week_53_day_2015, end_datetime) 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) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -551,8 +561,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -586,8 +596,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -606,8 +616,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -626,8 +636,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -646,8 +656,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
@ -752,8 +762,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'year') end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'year')
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -786,10 +796,10 @@ class DateFunctionTests(TestCase):
last_quarter_2015 = truncate_to(datetime(2015, 12, 31, 14, 10, 50, 123), 'quarter') 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') first_quarter_2016 = truncate_to(datetime(2016, 1, 1, 14, 10, 50, 123), 'quarter')
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) end_datetime = timezone.make_aware(end_datetime)
last_quarter_2015 = timezone.make_aware(last_quarter_2015, is_dst=False) last_quarter_2015 = timezone.make_aware(last_quarter_2015)
first_quarter_2016 = timezone.make_aware(first_quarter_2016, is_dst=False) 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=start_datetime, end_datetime=end_datetime)
self.create_model(start_datetime=end_datetime, end_datetime=start_datetime) self.create_model(start_datetime=end_datetime, end_datetime=start_datetime)
self.create_model(start_datetime=last_quarter_2015, end_datetime=end_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) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'month') end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'month')
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -857,8 +867,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'week') end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'week')
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -881,8 +891,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -909,8 +919,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -937,8 +947,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 26) # 0 microseconds. start_datetime = datetime(2015, 6, 15, 14, 30, 26) # 0 microseconds.
end_datetime = datetime(2015, 6, 15, 14, 30, 26, 321) end_datetime = datetime(2015, 6, 15, 14, 30, 26, 321)
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) end_datetime = timezone.make_aware(end_datetime)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
self.assertIs( self.assertIs(
DTModel.objects.filter( DTModel.objects.filter(
@ -962,8 +972,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'day') end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'day')
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -986,8 +996,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'hour') end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'hour')
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -1018,8 +1028,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'minute') end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'minute')
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -1050,8 +1060,8 @@ class DateFunctionTests(TestCase):
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'second') end_datetime = truncate_to(datetime(2016, 6, 15, 14, 10, 50, 123), 'second')
if settings.USE_TZ: if settings.USE_TZ:
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)
self.assertQuerysetEqual( self.assertQuerysetEqual(
@ -1085,9 +1095,9 @@ class DateFunctionTests(TestCase):
fan_since_2 = datetime(2015, 2, 3, 15, 0, 0) fan_since_2 = datetime(2015, 2, 3, 15, 0, 0)
fan_since_3 = datetime(2017, 2, 3, 15, 0, 0) fan_since_3 = datetime(2017, 2, 3, 15, 0, 0)
if settings.USE_TZ: if settings.USE_TZ:
fan_since_1 = timezone.make_aware(fan_since_1, is_dst=False) fan_since_1 = timezone.make_aware(fan_since_1)
fan_since_2 = timezone.make_aware(fan_since_2, is_dst=False) fan_since_2 = timezone.make_aware(fan_since_2)
fan_since_3 = timezone.make_aware(fan_since_3, is_dst=False) 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='Tom', fan_since=fan_since_1)
Fan.objects.create(author=author_1, name='Emma', fan_since=fan_since_2) 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) 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_2 = datetime(2001, 3, 5)
datetime_3 = datetime(2002, 1, 3) datetime_3 = datetime(2002, 1, 3)
if settings.USE_TZ: if settings.USE_TZ:
datetime_1 = timezone.make_aware(datetime_1, is_dst=False) datetime_1 = timezone.make_aware(datetime_1)
datetime_2 = timezone.make_aware(datetime_2, is_dst=False) datetime_2 = timezone.make_aware(datetime_2)
datetime_3 = timezone.make_aware(datetime_3, is_dst=False) datetime_3 = timezone.make_aware(datetime_3)
obj_1 = self.create_model(datetime_1, datetime_3) obj_1 = self.create_model(datetime_1, datetime_3)
obj_2 = self.create_model(datetime_2, datetime_1) obj_2 = self.create_model(datetime_2, datetime_1)
obj_3 = self.create_model(datetime_3, datetime_2) obj_3 = self.create_model(datetime_3, datetime_2)
@ -1144,8 +1154,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
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)
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) end_datetime = timezone.make_aware(end_datetime)
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))
@ -1203,8 +1213,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
def test_extract_func_explicit_timezone_priority(self): def test_extract_func_explicit_timezone_priority(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)
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) end_datetime = timezone.make_aware(end_datetime)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
for melb in self.get_timezones('Australia/Melbourne'): for melb in self.get_timezones('Australia/Melbourne'):
@ -1233,8 +1243,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
def test_trunc_timezone_applied_before_truncation(self): def test_trunc_timezone_applied_before_truncation(self):
start_datetime = datetime(2016, 1, 1, 1, 30, 50, 321) start_datetime = datetime(2016, 1, 1, 1, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) end_datetime = timezone.make_aware(end_datetime)
self.create_model(start_datetime, end_datetime) self.create_model(start_datetime, end_datetime)
for melb, pacific in zip( for melb, pacific in zip(
@ -1263,6 +1273,8 @@ 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): def test_trunc_ambiguous_and_invalid_times(self):
sao = pytz.timezone('America/Sao_Paulo') sao = pytz.timezone('America/Sao_Paulo')
utc = timezone.utc utc = timezone.utc
@ -1294,8 +1306,8 @@ class DateFunctionWithTimeZoneTests(DateFunctionTests):
""" """
start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321) start_datetime = datetime(2015, 6, 15, 14, 30, 50, 321)
end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123) end_datetime = datetime(2016, 6, 15, 14, 10, 50, 123)
start_datetime = timezone.make_aware(start_datetime, is_dst=False) start_datetime = timezone.make_aware(start_datetime)
end_datetime = timezone.make_aware(end_datetime, is_dst=False) 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)

View File

@ -10,6 +10,16 @@ import sys
import uuid import uuid
from unittest import mock 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.more_operations
import custom_migration_operations.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): def test_serialize_fields(self):
self.assertSerializedFieldEqual(models.CharField(max_length=255)) self.assertSerializedFieldEqual(models.CharField(max_length=255))
self.assertSerializedResultEqual( self.assertSerializedResultEqual(

View File

@ -4,7 +4,10 @@ import unittest
from types import ModuleType, SimpleNamespace from types import ModuleType, SimpleNamespace
from unittest import mock 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.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest from django.http import HttpRequest
from django.test import ( from django.test import (
@ -348,6 +351,21 @@ class SettingsTests(SimpleTestCase):
finally: finally:
del sys.modules['fake_settings_module'] 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): class TestComplexSettingOverride(SimpleTestCase):
def setUp(self): def setUp(self):

View File

@ -5,15 +5,15 @@ from contextlib import contextmanager
from unittest import SkipTest, skipIf from unittest import SkipTest, skipIf
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
import pytz
try: try:
import zoneinfo import zoneinfo
except ImportError: except ImportError:
try: from backports import zoneinfo
from backports import zoneinfo
except ImportError: try:
zoneinfo = None 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
@ -61,9 +61,9 @@ UTC = 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 = (pytz.timezone,) ZONE_CONSTRUCTORS = (zoneinfo.ZoneInfo,)
if zoneinfo is not None: if pytz is not None:
ZONE_CONSTRUCTORS += (zoneinfo.ZoneInfo,) ZONE_CONSTRUCTORS += (pytz.timezone,)
def get_timezones(key): 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__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
# shouldn't be converted. # shouldn't be converted.
@ -921,7 +938,7 @@ class TemplateTests(SimpleTestCase):
tpl = Template("{% load tz %}{{ dt|timezone:tz }}") tpl = Template("{% load tz %}{{ dt|timezone:tz }}")
ctx = Context({ ctx = Context({
'dt': datetime.datetime(2011, 9, 1, 13, 20, 30), '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") 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") self.assertEqual(tpl.render(ctx), "2011-09-01T12:20:30+02:00")
@ignore_warnings(category=RemovedInDjango50Warning)
def test_timezone_templatetag_invalid_argument(self): def test_timezone_templatetag_invalid_argument(self):
with self.assertRaises(TemplateSyntaxError): with self.assertRaises(TemplateSyntaxError):
Template("{% load tz %}{% timezone %}{% endtimezone %}").render() 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'})) 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):

View File

@ -14,7 +14,10 @@ from pathlib import Path
from subprocess import CompletedProcess from subprocess import CompletedProcess
from unittest import mock, skip, skipIf from unittest import mock, skip, skipIf
import pytz try:
import zoneinfo
except ImportError:
from backports import zoneinfo
import django.__main__ import django.__main__
from django.apps.registry import Apps from django.apps.registry import Apps
@ -247,7 +250,7 @@ class TestChildArguments(SimpleTestCase):
class TestUtilities(SimpleTestCase): class TestUtilities(SimpleTestCase):
def test_is_django_module(self): def test_is_django_module(self):
for module, expected in ( for module, expected in (
(pytz, False), (zoneinfo, False),
(sys, False), (sys, False),
(autoreload, True) (autoreload, True)
): ):
@ -256,7 +259,7 @@ class TestUtilities(SimpleTestCase):
def test_is_django_path(self): def test_is_django_path(self):
for module, expected in ( for module, expected in (
(pytz.__file__, False), (zoneinfo.__file__, False),
(contextlib.__file__, False), (contextlib.__file__, False),
(autoreload.__file__, True) (autoreload.__file__, True)
): ):

View File

@ -2,41 +2,58 @@ import datetime
import unittest import unittest
from unittest import mock from unittest import mock
import pytz try:
import pytz
except ImportError:
pytz = None
try: try:
import zoneinfo import zoneinfo
except ImportError: except ImportError:
try: from backports import zoneinfo
from backports import zoneinfo
except ImportError:
zoneinfo = None
from django.test import SimpleTestCase, override_settings from django.test import SimpleTestCase, ignore_warnings, override_settings
from django.utils import timezone 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 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_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: needs_pytz = unittest.skip('Test requires pytz')
PARIS_ZI = None
PARIS_IMPLS = (CET,)
needs_zoneinfo = unittest.skip("Test requires zoneinfo")
else: else:
PARIS_ZI = zoneinfo.ZoneInfo('Europe/Paris') CET = pytz.timezone('Europe/Paris')
PARIS_IMPLS = (CET, PARIS_ZI) PARIS_IMPLS = (PARIS_ZI, CET)
def needs_zoneinfo(f): def needs_pytz(f):
return 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):
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()))
@ -173,13 +190,14 @@ class TimezoneTests(SimpleTestCase):
timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), tz), timezone.make_aware(datetime.datetime(2011, 9, 1, 12, 20, 30), tz),
datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=CEST)) datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=CEST))
with self.assertRaises(ValueError): if HAS_PYTZ:
timezone.make_aware(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET)
if HAS_ZONEINFO:
with self.assertRaises(ValueError): 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): def test_make_naive_pytz(self):
self.assertEqual( self.assertEqual(
timezone.make_naive(CET.localize(datetime.datetime(2011, 9, 1, 12, 20, 30)), CET), 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'): 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) timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30), CET)
@needs_zoneinfo
def test_make_naive_zoneinfo(self): def test_make_naive_zoneinfo(self):
self.assertEqual( self.assertEqual(
timezone.make_naive(datetime.datetime(2011, 9, 1, 12, 20, 30, tzinfo=PARIS_ZI), PARIS_ZI), 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) datetime.datetime(2011, 9, 1, 12, 20, 30, fold=1)
) )
@needs_pytz
@ignore_warnings(category=RemovedInDjango50Warning)
def test_make_aware_pytz_ambiguous(self): def test_make_aware_pytz_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)
@ -217,7 +236,6 @@ class TimezoneTests(SimpleTestCase):
self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1)) self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2)) self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
@needs_zoneinfo
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)
@ -232,6 +250,8 @@ 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): def test_make_aware_pytz_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)
@ -245,7 +265,6 @@ class TimezoneTests(SimpleTestCase):
self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1)) self.assertEqual(std.tzinfo.utcoffset(std), datetime.timedelta(hours=1))
self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2)) self.assertEqual(dst.tzinfo.utcoffset(dst), datetime.timedelta(hours=2))
@needs_zoneinfo
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)
@ -260,6 +279,15 @@ 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))
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): def test_get_timezone_name(self):
""" """
The _get_timezone_name() helper must return the offset for fixed offset 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, fixed offset with and without `name`.
(datetime.timezone(datetime.timedelta(hours=10)), 'UTC+10:00'), (datetime.timezone(datetime.timedelta(hours=10)), 'UTC+10:00'),
(datetime.timezone(datetime.timedelta(hours=10), name='Etc/GMT-10'), 'Etc/GMT-10'), (datetime.timezone(datetime.timedelta(hours=10), name='Etc/GMT-10'), 'Etc/GMT-10'),
# pytz, named and fixed offset. # zoneinfo, named and fixed offset.
(pytz.timezone('Europe/Madrid'), 'Europe/Madrid'), (zoneinfo.ZoneInfo('Europe/Madrid'), 'Europe/Madrid'),
(pytz.timezone('Etc/GMT-10'), '+10'), (zoneinfo.ZoneInfo('Etc/GMT-10'), '+10'),
] ]
if HAS_ZONEINFO: if HAS_PYTZ:
tests += [ tests += [
# zoneinfo, named and fixed offset. # pytz, named and fixed offset.
(zoneinfo.ZoneInfo('Europe/Madrid'), 'Europe/Madrid'), (pytz.timezone('Europe/Madrid'), 'Europe/Madrid'),
(zoneinfo.ZoneInfo('Etc/GMT-10'), '+10'), (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):
@ -288,10 +316,6 @@ class TimezoneTests(SimpleTestCase):
def test_get_default_timezone(self): def test_get_default_timezone(self):
self.assertEqual(timezone.get_default_timezone_name(), 'America/Chicago') 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): def test_fixedoffset_timedelta(self):
delta = datetime.timedelta(hours=1) delta = datetime.timedelta(hours=1)
self.assertEqual(timezone.get_fixed_timezone(delta).utcoffset(None), delta) self.assertEqual(timezone.get_fixed_timezone(delta).utcoffset(None), delta)