Skip to content

Commit 62abf6a

Browse files
max-muotoauvipy
andauthored
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>
1 parent 4842ad1 commit 62abf6a

File tree

4 files changed

+72
-14
lines changed

4 files changed

+72
-14
lines changed

rest_framework/fields.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from rest_framework.settings import api_settings
3636
from rest_framework.utils import html, humanize_datetime, json, representation
3737
from rest_framework.utils.formatting import lazy_format
38+
from rest_framework.utils.timezone import valid_datetime
3839
from rest_framework.validators import ProhibitSurrogateCharactersValidator
3940

4041

@@ -1154,7 +1155,12 @@ def enforce_timezone(self, value):
11541155
except OverflowError:
11551156
self.fail('overflow')
11561157
try:
1157-
return timezone.make_aware(value, field_timezone)
1158+
dt = timezone.make_aware(value, field_timezone)
1159+
# When the resulting datetime is a ZoneInfo instance, it won't necessarily
1160+
# throw given an invalid datetime, so we need to specifically check.
1161+
if not valid_datetime(dt):
1162+
self.fail('make_aware', timezone=field_timezone)
1163+
return dt
11581164
except InvalidTimeError:
11591165
self.fail('make_aware', timezone=field_timezone)
11601166
elif (field_timezone is None) and timezone.is_aware(value):

rest_framework/utils/timezone.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from datetime import datetime, timezone, tzinfo
2+
3+
4+
def datetime_exists(dt):
5+
"""Check if a datetime exists. Taken from: https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html"""
6+
# There are no non-existent times in UTC, and comparisons between
7+
# aware time zones always compare absolute times; if a datetime is
8+
# not equal to the same datetime represented in UTC, it is imaginary.
9+
return dt.astimezone(timezone.utc) == dt
10+
11+
12+
def datetime_ambiguous(dt: datetime):
13+
"""Check whether a datetime is ambiguous. Taken from: https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html"""
14+
# If a datetime exists and its UTC offset changes in response to
15+
# changing `fold`, it is ambiguous in the zone specified.
16+
return datetime_exists(dt) and (
17+
dt.replace(fold=not dt.fold).utcoffset() != dt.utcoffset()
18+
)
19+
20+
21+
def valid_datetime(dt):
22+
"""Returns True if the datetime is not ambiguous or imaginary, False otherwise."""
23+
if isinstance(dt.tzinfo, tzinfo) and not datetime_ambiguous(dt):
24+
return True
25+
return False

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def get_version(package):
8282
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
8383
packages=find_packages(exclude=['tests*']),
8484
include_package_data=True,
85-
install_requires=["django>=3.0", "pytz"],
85+
install_requires=["django>=3.0", "pytz", 'backports.zoneinfo;python_version<"3.9"'],
8686
python_requires=">=3.6",
8787
zip_safe=False,
8888
classifiers=[

tests/test_fields.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import uuid
77
from decimal import ROUND_DOWN, ROUND_UP, Decimal
8+
from unittest.mock import patch
89

910
import pytest
1011
import pytz
@@ -21,6 +22,11 @@
2122
)
2223
from tests.models import UUIDForeignKeyTarget
2324

25+
if sys.version_info >= (3, 9):
26+
from zoneinfo import ZoneInfo
27+
else:
28+
from backports.zoneinfo import ZoneInfo
29+
2430
utc = datetime.timezone.utc
2531

2632
# Tests for helper functions.
@@ -651,15 +657,15 @@ class FieldValues:
651657
"""
652658
Base class for testing valid and invalid input values.
653659
"""
654-
def test_valid_inputs(self):
660+
def test_valid_inputs(self, *args):
655661
"""
656662
Ensure that valid values return the expected validated data.
657663
"""
658664
for input_value, expected_output in get_items(self.valid_inputs):
659665
assert self.field.run_validation(input_value) == expected_output, \
660666
'input value: {}'.format(repr(input_value))
661667

662-
def test_invalid_inputs(self):
668+
def test_invalid_inputs(self, *args):
663669
"""
664670
Ensure that invalid values raise the expected validation error.
665671
"""
@@ -669,7 +675,7 @@ def test_invalid_inputs(self):
669675
assert exc_info.value.detail == expected_failure, \
670676
'input value: {}'.format(repr(input_value))
671677

672-
def test_outputs(self):
678+
def test_outputs(self, *args):
673679
for output_value, expected_output in get_items(self.outputs):
674680
assert self.field.to_representation(output_value) == expected_output, \
675681
'output value: {}'.format(repr(output_value))
@@ -1505,12 +1511,12 @@ class TestTZWithDateTimeField(FieldValues):
15051511
@classmethod
15061512
def setup_class(cls):
15071513
# use class setup method, as class-level attribute will still be evaluated even if test is skipped
1508-
kolkata = pytz.timezone('Asia/Kolkata')
1514+
kolkata = ZoneInfo('Asia/Kolkata')
15091515

15101516
cls.valid_inputs = {
1511-
'2016-12-19T10:00:00': kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
1512-
'2016-12-19T10:00:00+05:30': kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
1513-
datetime.datetime(2016, 12, 19, 10): kolkata.localize(datetime.datetime(2016, 12, 19, 10)),
1517+
'2016-12-19T10:00:00': datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata),
1518+
'2016-12-19T10:00:00+05:30': datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata),
1519+
datetime.datetime(2016, 12, 19, 10): datetime.datetime(2016, 12, 19, 10, tzinfo=kolkata),
15141520
}
15151521
cls.invalid_inputs = {}
15161522
cls.outputs = {
@@ -1529,7 +1535,7 @@ class TestDefaultTZDateTimeField(TestCase):
15291535
@classmethod
15301536
def setup_class(cls):
15311537
cls.field = serializers.DateTimeField()
1532-
cls.kolkata = pytz.timezone('Asia/Kolkata')
1538+
cls.kolkata = ZoneInfo('Asia/Kolkata')
15331539

15341540
def assertUTC(self, tzinfo):
15351541
"""
@@ -1551,18 +1557,17 @@ def test_current_timezone(self):
15511557
self.assertUTC(self.field.default_timezone())
15521558

15531559

1554-
@pytest.mark.skipif(pytz is None, reason='pytz not installed')
15551560
@override_settings(TIME_ZONE='UTC', USE_TZ=True)
15561561
class TestCustomTimezoneForDateTimeField(TestCase):
15571562

15581563
@classmethod
15591564
def setup_class(cls):
1560-
cls.kolkata = pytz.timezone('Asia/Kolkata')
1565+
cls.kolkata = ZoneInfo('Asia/Kolkata')
15611566
cls.date_format = '%d/%m/%Y %H:%M'
15621567

15631568
def test_should_render_date_time_in_default_timezone(self):
15641569
field = serializers.DateTimeField(default_timezone=self.kolkata, format=self.date_format)
1565-
dt = datetime.datetime(2018, 2, 8, 14, 15, 16, tzinfo=pytz.utc)
1570+
dt = datetime.datetime(2018, 2, 8, 14, 15, 16, tzinfo=ZoneInfo("UTC"))
15661571

15671572
with override(self.kolkata):
15681573
rendered_date = field.to_representation(dt)
@@ -1572,7 +1577,8 @@ def test_should_render_date_time_in_default_timezone(self):
15721577
assert rendered_date == rendered_date_in_timezone
15731578

15741579

1575-
class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
1580+
@pytest.mark.skipif(pytz is None, reason="As Django 4.0 has deprecated pytz, this test should eventually be able to get removed.")
1581+
class TestPytzNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
15761582
"""
15771583
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
15781584
Timezone America/New_York has DST shift from 2017-03-12T02:00:00 to 2017-03-12T03:00:00 and
@@ -1596,6 +1602,27 @@ def __str__(self):
15961602
field = serializers.DateTimeField(default_timezone=MockTimezone())
15971603

15981604

1605+
@patch('rest_framework.utils.timezone.datetime_ambiguous', return_value=True)
1606+
class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues):
1607+
"""
1608+
Invalid values for `DateTimeField` with datetime in DST shift (non-existing or ambiguous) and timezone with DST.
1609+
Timezone America/New_York has DST shift from 2017-03-12T02:00:00 to 2017-03-12T03:00:00 and
1610+
from 2017-11-05T02:00:00 to 2017-11-05T01:00:00 in 2017.
1611+
"""
1612+
valid_inputs = {}
1613+
invalid_inputs = {
1614+
'2017-03-12T02:30:00': ['Invalid datetime for the timezone "America/New_York".'],
1615+
'2017-11-05T01:30:00': ['Invalid datetime for the timezone "America/New_York".']
1616+
}
1617+
outputs = {}
1618+
1619+
class MockZoneInfoTimezone(datetime.tzinfo):
1620+
def __str__(self):
1621+
return 'America/New_York'
1622+
1623+
field = serializers.DateTimeField(default_timezone=MockZoneInfoTimezone())
1624+
1625+
15991626
class TestTimeField(FieldValues):
16001627
"""
16011628
Valid and invalid values for `TimeField`.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy