mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 01:26:53 +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.utils import html, humanize_datetime, json, representation
|
||||
from rest_framework.utils.formatting import lazy_format
|
||||
from rest_framework.utils.timezone import valid_datetime
|
||||
from rest_framework.validators import ProhibitSurrogateCharactersValidator
|
||||
|
||||
|
||||
|
@ -1154,7 +1155,12 @@ class DateTimeField(Field):
|
|||
except OverflowError:
|
||||
self.fail('overflow')
|
||||
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:
|
||||
self.fail('make_aware', timezone=field_timezone)
|
||||
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 (*)
|
||||
packages=find_packages(exclude=['tests*']),
|
||||
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",
|
||||
zip_safe=False,
|
||||
classifiers=[
|
||||
|
|
|
@ -5,6 +5,7 @@ import re
|
|||
import sys
|
||||
import uuid
|
||||
from decimal import ROUND_DOWN, ROUND_UP, Decimal
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import pytz
|
||||
|
@ -21,6 +22,11 @@ from rest_framework.fields import (
|
|||
)
|
||||
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
|
||||
|
||||
# Tests for helper functions.
|
||||
|
@ -651,7 +657,7 @@ class FieldValues:
|
|||
"""
|
||||
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.
|
||||
"""
|
||||
|
@ -659,7 +665,7 @@ class FieldValues:
|
|||
assert self.field.run_validation(input_value) == expected_output, \
|
||||
'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.
|
||||
"""
|
||||
|
@ -669,7 +675,7 @@ class FieldValues:
|
|||
assert exc_info.value.detail == expected_failure, \
|
||||
'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):
|
||||
assert self.field.to_representation(output_value) == expected_output, \
|
||||
'output value: {}'.format(repr(output_value))
|
||||
|
@ -1505,12 +1511,12 @@ class TestTZWithDateTimeField(FieldValues):
|
|||
@classmethod
|
||||
def setup_class(cls):
|
||||
# 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 = {
|
||||
'2016-12-19T10:00:00': kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
|
||||
'2016-12-19T10:00:00+05:30': kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
|
||||
datetime.datetime(2016, 12, 19, 10): 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': datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata),
|
||||
datetime.datetime(2016, 12, 19, 10): datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata),
|
||||
}
|
||||
cls.invalid_inputs = {}
|
||||
cls.outputs = {
|
||||
|
@ -1529,7 +1535,7 @@ class TestDefaultTZDateTimeField(TestCase):
|
|||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.field = serializers.DateTimeField()
|
||||
cls.kolkata = pytz.timezone('Asia/Kolkata')
|
||||
cls.kolkata = ZoneInfo('Asia/Kolkata')
|
||||
|
||||
def assertUTC(self, tzinfo):
|
||||
"""
|
||||
|
@ -1551,18 +1557,17 @@ class TestDefaultTZDateTimeField(TestCase):
|
|||
self.assertUTC(self.field.default_timezone())
|
||||
|
||||
|
||||
@pytest.mark.skipif(pytz is None, reason='pytz not installed')
|
||||
@override_settings(TIME_ZONE='UTC', USE_TZ=True)
|
||||
class TestCustomTimezoneForDateTimeField(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.kolkata = pytz.timezone('Asia/Kolkata')
|
||||
cls.kolkata = ZoneInfo('Asia/Kolkata')
|
||||
cls.date_format = '%d/%m/%Y %H:%M'
|
||||
|
||||
def test_should_render_date_time_in_default_timezone(self):
|
||||
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):
|
||||
rendered_date = field.to_representation(dt)
|
||||
|
@ -1572,7 +1577,8 @@ class TestCustomTimezoneForDateTimeField(TestCase):
|
|||
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.
|
||||
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())
|
||||
|
||||
|
||||
@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):
|
||||
"""
|
||||
Valid and invalid values for `TimeField`.
|
||||
|
|
Loading…
Reference in New Issue
Block a user