Skip to content

Use ZoneInfo as primary source of timezone data #8924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
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


Expand Down Expand Up @@ -1154,7 +1155,12 @@ def enforce_timezone(self, value):
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):
Expand Down
25 changes: 25 additions & 0 deletions rest_framework/utils/timezone.py
Original file line number Diff line number Diff line change
@@ -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 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def get_version(package):
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=[
Expand Down
51 changes: 39 additions & 12 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
import uuid
from decimal import ROUND_DOWN, ROUND_UP, Decimal
from unittest.mock import patch

import pytest
import pytz
Expand All @@ -21,6 +22,11 @@
)
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.
Expand Down Expand Up @@ -651,15 +657,15 @@ 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.
"""
for input_value, expected_output in get_items(self.valid_inputs):
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.
"""
Expand All @@ -669,7 +675,7 @@ def test_invalid_inputs(self):
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))
Expand Down Expand Up @@ -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 = {
Expand All @@ -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):
"""
Expand All @@ -1551,18 +1557,17 @@ def test_current_timezone(self):
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)
Expand All @@ -1572,7 +1577,8 @@ def test_should_render_date_time_in_default_timezone(self):
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
Expand All @@ -1596,6 +1602,27 @@ def __str__(self):
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`.
Expand Down
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