mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 02:53:58 +03:00
Use ZoneInfo as primary source of timezone data (#8924)
* Use ZoneInfo as primary source of timezone data * Update tests/test_fields.py --------- Co-authored-by: Asif Saif Uddin <auvipy@gmail.com>
This commit is contained in:
parent
4842ad1b6a
commit
62abf6ac1f
|
@ -35,6 +35,7 @@ from rest_framework.exceptions import ErrorDetail, ValidationError
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.utils import html, humanize_datetime, json, representation
|
from rest_framework.utils import html, humanize_datetime, json, representation
|
||||||
from rest_framework.utils.formatting import lazy_format
|
from rest_framework.utils.formatting import lazy_format
|
||||||
|
from rest_framework.utils.timezone import valid_datetime
|
||||||
from rest_framework.validators import ProhibitSurrogateCharactersValidator
|
from rest_framework.validators import ProhibitSurrogateCharactersValidator
|
||||||
|
|
||||||
|
|
||||||
|
@ -1154,7 +1155,12 @@ class DateTimeField(Field):
|
||||||
except OverflowError:
|
except OverflowError:
|
||||||
self.fail('overflow')
|
self.fail('overflow')
|
||||||
try:
|
try:
|
||||||
return timezone.make_aware(value, field_timezone)
|
dt = timezone.make_aware(value, field_timezone)
|
||||||
|
# When the resulting datetime is a ZoneInfo instance, it won't necessarily
|
||||||
|
# throw given an invalid datetime, so we need to specifically check.
|
||||||
|
if not valid_datetime(dt):
|
||||||
|
self.fail('make_aware', timezone=field_timezone)
|
||||||
|
return dt
|
||||||
except InvalidTimeError:
|
except InvalidTimeError:
|
||||||
self.fail('make_aware', timezone=field_timezone)
|
self.fail('make_aware', timezone=field_timezone)
|
||||||
elif (field_timezone is None) and timezone.is_aware(value):
|
elif (field_timezone is None) and timezone.is_aware(value):
|
||||||
|
|
25
rest_framework/utils/timezone.py
Normal file
25
rest_framework/utils/timezone.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from datetime import datetime, timezone, tzinfo
|
||||||
|
|
||||||
|
|
||||||
|
def datetime_exists(dt):
|
||||||
|
"""Check if a datetime exists. Taken from: https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html"""
|
||||||
|
# There are no non-existent times in UTC, and comparisons between
|
||||||
|
# aware time zones always compare absolute times; if a datetime is
|
||||||
|
# not equal to the same datetime represented in UTC, it is imaginary.
|
||||||
|
return dt.astimezone(timezone.utc) == dt
|
||||||
|
|
||||||
|
|
||||||
|
def datetime_ambiguous(dt: datetime):
|
||||||
|
"""Check whether a datetime is ambiguous. Taken from: https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html"""
|
||||||
|
# If a datetime exists and its UTC offset changes in response to
|
||||||
|
# changing `fold`, it is ambiguous in the zone specified.
|
||||||
|
return datetime_exists(dt) and (
|
||||||
|
dt.replace(fold=not dt.fold).utcoffset() != dt.utcoffset()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def valid_datetime(dt):
|
||||||
|
"""Returns True if the datetime is not ambiguous or imaginary, False otherwise."""
|
||||||
|
if isinstance(dt.tzinfo, tzinfo) and not datetime_ambiguous(dt):
|
||||||
|
return True
|
||||||
|
return False
|
2
setup.py
2
setup.py
|
@ -82,7 +82,7 @@ setup(
|
||||||
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
|
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
|
||||||
packages=find_packages(exclude=['tests*']),
|
packages=find_packages(exclude=['tests*']),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=["django>=3.0", "pytz"],
|
install_requires=["django>=3.0", "pytz", 'backports.zoneinfo;python_version<"3.9"'],
|
||||||
python_requires=">=3.6",
|
python_requires=">=3.6",
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
|
|
|
@ -5,6 +5,7 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
from decimal import ROUND_DOWN, ROUND_UP, Decimal
|
from decimal import ROUND_DOWN, ROUND_UP, Decimal
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytz
|
import pytz
|
||||||
|
@ -21,6 +22,11 @@ from rest_framework.fields import (
|
||||||
)
|
)
|
||||||
from tests.models import UUIDForeignKeyTarget
|
from tests.models import UUIDForeignKeyTarget
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
else:
|
||||||
|
from backports.zoneinfo import ZoneInfo
|
||||||
|
|
||||||
utc = datetime.timezone.utc
|
utc = datetime.timezone.utc
|
||||||
|
|
||||||
# Tests for helper functions.
|
# Tests for helper functions.
|
||||||
|
@ -651,7 +657,7 @@ class FieldValues:
|
||||||
"""
|
"""
|
||||||
Base class for testing valid and invalid input values.
|
Base class for testing valid and invalid input values.
|
||||||
"""
|
"""
|
||||||
def test_valid_inputs(self):
|
def test_valid_inputs(self, *args):
|
||||||
"""
|
"""
|
||||||
Ensure that valid values return the expected validated data.
|
Ensure that valid values return the expected validated data.
|
||||||
"""
|
"""
|
||||||
|
@ -659,7 +665,7 @@ class FieldValues:
|
||||||
assert self.field.run_validation(input_value) == expected_output, \
|
assert self.field.run_validation(input_value) == expected_output, \
|
||||||
'input value: {}'.format(repr(input_value))
|
'input value: {}'.format(repr(input_value))
|
||||||
|
|
||||||
def test_invalid_inputs(self):
|
def test_invalid_inputs(self, *args):
|
||||||
"""
|
"""
|
||||||
Ensure that invalid values raise the expected validation error.
|
Ensure that invalid values raise the expected validation error.
|
||||||
"""
|
"""
|
||||||
|
@ -669,7 +675,7 @@ class FieldValues:
|
||||||
assert exc_info.value.detail == expected_failure, \
|
assert exc_info.value.detail == expected_failure, \
|
||||||
'input value: {}'.format(repr(input_value))
|
'input value: {}'.format(repr(input_value))
|
||||||
|
|
||||||
def test_outputs(self):
|
def test_outputs(self, *args):
|
||||||
for output_value, expected_output in get_items(self.outputs):
|
for output_value, expected_output in get_items(self.outputs):
|
||||||
assert self.field.to_representation(output_value) == expected_output, \
|
assert self.field.to_representation(output_value) == expected_output, \
|
||||||
'output value: {}'.format(repr(output_value))
|
'output value: {}'.format(repr(output_value))
|
||||||
|
@ -1505,12 +1511,12 @@ class TestTZWithDateTimeField(FieldValues):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_class(cls):
|
def setup_class(cls):
|
||||||
# use class setup method, as class-level attribute will still be evaluated even if test is skipped
|
# use class setup method, as class-level attribute will still be evaluated even if test is skipped
|
||||||
kolkata = pytz.timezone('Asia/Kolkata')
|
kolkata = ZoneInfo('Asia/Kolkata')
|
||||||
|
|
||||||
cls.valid_inputs = {
|
cls.valid_inputs = {
|
||||||
'2016-12-19T10:00:00': kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
|
'2016-12-19T10:00:00': datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata),
|
||||||
'2016-12-19T10:00:00+05:30': kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
|
'2016-12-19T10:00:00+05:30': datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata),
|
||||||
datetime.datetime(2016, 12, 19, 10): kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
|
datetime.datetime(2016, 12, 19, 10): datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata),
|
||||||
}
|
}
|
||||||
cls.invalid_inputs = {}
|
cls.invalid_inputs = {}
|
||||||
cls.outputs = {
|
cls.outputs = {
|
||||||
|
@ -1529,7 +1535,7 @@ class TestDefaultTZDateTimeField(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_class(cls):
|
def setup_class(cls):
|
||||||
cls.field = serializers.DateTimeField()
|
cls.field = serializers.DateTimeField()
|
||||||
cls.kolkata = pytz.timezone('Asia/Kolkata')
|
cls.kolkata = ZoneInfo('Asia/Kolkata')
|
||||||
|
|
||||||
def assertUTC(self, tzinfo):
|
def assertUTC(self, tzinfo):
|
||||||
"""
|
"""
|
||||||
|
@ -1551,18 +1557,17 @@ class TestDefaultTZDateTimeField(TestCase):
|
||||||
self.assertUTC(self.field.default_timezone())
|
self.assertUTC(self.field.default_timezone())
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(pytz is None, reason='pytz not installed')
|
|
||||||
@override_settings(TIME_ZONE='UTC', USE_TZ=True)
|
@override_settings(TIME_ZONE='UTC', USE_TZ=True)
|
||||||
class TestCustomTimezoneForDateTimeField(TestCase):
|
class TestCustomTimezoneForDateTimeField(TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_class(cls):
|
def setup_class(cls):
|
||||||
cls.kolkata = pytz.timezone('Asia/Kolkata')
|
cls.kolkata = ZoneInfo('Asia/Kolkata')
|
||||||
cls.date_format = '%d/%m/%Y %H:%M'
|
cls.date_format = '%d/%m/%Y %H:%M'
|
||||||
|
|
||||||
def test_should_render_date_time_in_default_timezone(self):
|
def test_should_render_date_time_in_default_timezone(self):
|
||||||
field = serializers.DateTimeField(default_timezone=self.kolkata, format=self.date_format)
|
field = serializers.DateTimeField(default_timezone=self.kolkata, format=self.date_format)
|
||||||
dt = datetime.datetime(2018, 2, 8, 14, 15, 16, tzinfo=pytz.utc)
|
dt = datetime.datetime(2018, 2, 8, 14, 15, 16, tzinfo=ZoneInfo("UTC"))
|
||||||
|
|
||||||
with override(self.kolkata):
|
with override(self.kolkata):
|
||||||
rendered_date = field.to_representation(dt)
|
rendered_date = field.to_representation(dt)
|
||||||
|
@ -1572,7 +1577,8 @@ class TestCustomTimezoneForDateTimeField(TestCase):
|
||||||
assert rendered_date == rendered_date_in_timezone
|
assert rendered_date == rendered_date_in_timezone
|
||||||
|
|
||||||
|
|
||||||
class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
@pytest.mark.skipif(pytz is None, reason="As Django 4.0 has deprecated pytz, this test should eventually be able to get removed.")
|
||||||
|
class TestPytzNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
||||||
"""
|
"""
|
||||||
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
|
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
|
||||||
Timezone America/New_York has DST shift from 2017-03-12T02:00:00 to 2017-03-12T03:00:00 and
|
Timezone America/New_York has DST shift from 2017-03-12T02:00:00 to 2017-03-12T03:00:00 and
|
||||||
|
@ -1596,6 +1602,27 @@ class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
||||||
field = serializers.DateTimeField(default_timezone=MockTimezone())
|
field = serializers.DateTimeField(default_timezone=MockTimezone())
|
||||||
|
|
||||||
|
|
||||||
|
@patch('rest_framework.utils.timezone.datetime_ambiguous', return_value=True)
|
||||||
|
class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
|
||||||
|
"""
|
||||||
|
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
|
||||||
|
Timezone America/New_York has DST shift from 2017-03-12T02:00:00 to 2017-03-12T03:00:00 and
|
||||||
|
from 2017-11-05T02:00:00 to 2017-11-05T01:00:00 in 2017.
|
||||||
|
"""
|
||||||
|
valid_inputs = {}
|
||||||
|
invalid_inputs = {
|
||||||
|
'2017-03-12T02:30:00': ['Invalid datetime for the timezone "America/New_York".'],
|
||||||
|
'2017-11-05T01:30:00': ['Invalid datetime for the timezone "America/New_York".']
|
||||||
|
}
|
||||||
|
outputs = {}
|
||||||
|
|
||||||
|
class MockZoneInfoTimezone(datetime.tzinfo):
|
||||||
|
def __str__(self):
|
||||||
|
return 'America/New_York'
|
||||||
|
|
||||||
|
field = serializers.DateTimeField(default_timezone=MockZoneInfoTimezone())
|
||||||
|
|
||||||
|
|
||||||
class TestTimeField(FieldValues):
|
class TestTimeField(FieldValues):
|
||||||
"""
|
"""
|
||||||
Valid and invalid values for `TimeField`.
|
Valid and invalid values for `TimeField`.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user