From fe58daa51e8a8176a1eeda033c99e07858f16b95 Mon Sep 17 00:00:00 2001 From: Jose Tomas Robles Hahn Date: Wed, 6 Jan 2021 14:37:28 -0300 Subject: [PATCH] libs.tz_utils: Add checks to validate_dt_tz The `zone` attribute is not defined in the abstract base class `datetime.tzinfo`. We need to check that it is there before using it in the function `validate_dt_tz` to prevent unexpected exceptions when dealing with Python Standard Library time zones that are instances of class `datetime.timezone`. --- cl_sii/libs/tz_utils.py | 7 ++++++ tests/test_libs_tz_utils.py | 47 ++++++++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/cl_sii/libs/tz_utils.py b/cl_sii/libs/tz_utils.py index 74a98208..3ba84e82 100644 --- a/cl_sii/libs/tz_utils.py +++ b/cl_sii/libs/tz_utils.py @@ -166,5 +166,12 @@ def validate_dt_tz(value: datetime, tz: PytzTimezone) -> None: """ if not dt_is_aware(value): raise ValueError("Value must be a timezone-aware datetime object.") + + # The 'zone' attribute is not defined in the abstract base class 'datetime.tzinfo'. We need to + # check that it is there before using it below to prevent unexpected exceptions when dealing + # with Python Standard Library time zones that are instances of class 'datetime.timezone'. + assert hasattr(value.tzinfo, 'zone'), f"Object {value.tzinfo!r} must have 'zone' attribute." + assert hasattr(tz, 'zone'), f"Object {tz!r} must have 'zone' attribute." + if value.tzinfo.zone != tz.zone: # type: ignore raise ValueError(f"Timezone of datetime value must be '{tz.zone!s}'.", value) diff --git a/tests/test_libs_tz_utils.py b/tests/test_libs_tz_utils.py index a685c5a5..4a203af0 100644 --- a/tests/test_libs_tz_utils.py +++ b/tests/test_libs_tz_utils.py @@ -1,9 +1,11 @@ +import datetime +import re import unittest from cl_sii.libs.tz_utils import ( # noqa: F401 convert_naive_dt_to_tz_aware, convert_tz_aware_dt_to_naive, dt_is_aware, dt_is_naive, get_now_tz_aware, validate_dt_tz, - PytzTimezone, TZ_UTC, + PytzTimezone, _TZ_CL_SANTIAGO, TZ_UTC, ) @@ -37,3 +39,46 @@ def test_dt_is_naive(self) -> None: def test_validate_dt_tz(self) -> None: # TODO: implement for 'validate_dt_tz' pass + + def test_validate_dt_tz_tzinfo_zone_attribute_check(self) -> None: + # Time zone: UTC. Source: Pytz: + tzinfo_utc_pytz = TZ_UTC + dt_with_tzinfo_utc_pytz = convert_naive_dt_to_tz_aware( + datetime.datetime(2021, 1, 6, 15, 21), + tzinfo_utc_pytz, + ) + + # Time zone: UTC. Source: Python Standard Library: + tzinfo_utc_stdlib = datetime.timezone.utc + dt_with_tzinfo_utc_stdlib = datetime.datetime.fromisoformat('2021-01-06T15:04+00:00') + + # Time zone: Not UTC. Source: Pytz: + tzinfo_not_utc_pytz = _TZ_CL_SANTIAGO + dt_with_tzinfo_not_utc_pytz = convert_naive_dt_to_tz_aware( + datetime.datetime(2021, 1, 6, 15, 21), + tzinfo_not_utc_pytz, + ) + + # Time zone: Not UTC. Source: Python Standard Library: + tzinfo_not_utc_stdlib = datetime.timezone(datetime.timedelta(days=-1, seconds=75600)) + dt_with_tzinfo_not_utc_stdlib = datetime.datetime.fromisoformat('2021-01-06T15:04-03:00') + + # Test datetimes with UTC time zone: + expected_error_message = re.compile( + r"^Object datetime.timezone.utc must have 'zone' attribute.$" + ) + with self.assertRaisesRegex(AssertionError, expected_error_message): + validate_dt_tz(dt_with_tzinfo_utc_pytz, tzinfo_utc_stdlib) + with self.assertRaisesRegex(AssertionError, expected_error_message): + validate_dt_tz(dt_with_tzinfo_utc_stdlib, tzinfo_utc_pytz) + + # Test datetimes with non-UTC time zone: + expected_error_message = re.compile( + r"^Object" + r" datetime.timezone\(datetime.timedelta\(days=-1, seconds=75600\)\)" + r" must have 'zone' attribute.$" + ) + with self.assertRaisesRegex(AssertionError, expected_error_message): + validate_dt_tz(dt_with_tzinfo_not_utc_pytz, tzinfo_not_utc_stdlib) # type: ignore + with self.assertRaisesRegex(AssertionError, expected_error_message): + validate_dt_tz(dt_with_tzinfo_not_utc_stdlib, tzinfo_not_utc_pytz)