diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 09d56eed6..4f82e4a10 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -29,6 +29,7 @@ from django.utils.encoding import is_protected_type, smart_str from django.utils.formats import localize_input, sanitize_separators from django.utils.ipv6 import clean_ipv6_address from django.utils.translation import gettext_lazy as _ +from pytz.exceptions import InvalidTimeError from rest_framework import ISO_8601 from rest_framework.exceptions import ErrorDetail, ValidationError @@ -1161,12 +1162,15 @@ class DateTimeField(Field): return value.astimezone(field_timezone) except OverflowError: self.fail('overflow') - 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): + try: + 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) - return dt elif (field_timezone is None) and timezone.is_aware(value): return timezone.make_naive(value, datetime.timezone.utc) return value diff --git a/setup.py b/setup.py index 6afd5e05e..533ffa97f 100755 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ setup( author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) packages=find_packages(exclude=['tests*']), include_package_data=True, - install_requires=["django>=3.0", 'backports.zoneinfo;python_version<"3.9"'], + install_requires=["django>=3.0", "pytz", 'backports.zoneinfo;python_version<"3.9"'], python_requires=">=3.6", zip_safe=False, classifiers=[ diff --git a/tests/test_fields.py b/tests/test_fields.py index 189819c68..bcf388441 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -9,6 +9,7 @@ from enum import auto from unittest.mock import patch import pytest +import pytz from django.core.exceptions import ValidationError as DjangoValidationError from django.db.models import IntegerChoices, TextChoices from django.http import QueryDict @@ -1589,6 +1590,31 @@ class TestCustomTimezoneForDateTimeField(TestCase): assert rendered_date == rendered_date_in_timezone +@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 + 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 MockTimezone(pytz.BaseTzInfo): + @staticmethod + def localize(value, is_dst): + raise pytz.InvalidTimeError() + + def __str__(self): + return 'America/New_York' + + field = serializers.DateTimeField(default_timezone=MockTimezone()) + + @patch('rest_framework.utils.timezone.datetime_ambiguous', return_value=True) class TestNaiveDayLightSavingTimeTimeZoneDateTimeField(FieldValues): """