From 6a2497b12957e190d236273de4fe5e101b9bd891 Mon Sep 17 00:00:00 2001 From: Ritvik Nag Date: Sat, 18 Jan 2025 20:10:51 -0500 Subject: [PATCH] add tests --- dataclass_wizard/v1/models.py | 51 +++++++++++---------------- dataclass_wizard/v1/models.pyi | 4 ++- tests/unit/v1/test_loaders.py | 64 ++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 31 deletions(-) diff --git a/dataclass_wizard/v1/models.py b/dataclass_wizard/v1/models.py index 69d6cd9..d3d6ee7 100644 --- a/dataclass_wizard/v1/models.py +++ b/dataclass_wizard/v1/models.py @@ -15,7 +15,7 @@ from ..utils.typing_compat import get_origin_v2 -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from ..bases import META @@ -201,7 +201,7 @@ def _wrap_inner(self, extras, def __str__(self): return getattr(self, '_wrapped', '') - def __repr__(self): + def __repr__(self): # pragma: no cover items = ', '.join([f'{v}={getattr(self, v)!r}' for v in self.__slots__ if not v.startswith('_')]) @@ -229,39 +229,30 @@ class PatternBase: 'tz_info', '_repr') - def __init__(self, base, patterns=None, tzname=None): + def __init__(self, base, patterns=None, tz_info=None): self.base = base if patterns is not None: self.patterns = patterns - if tzname is not None: - self.tz_info = ZoneInfo(tzname) + if tz_info is not None: + self.tz_info = tz_info - def with_tz(self, tz_info: tzinfo) -> Self: + def with_tz(self, tz_info: tzinfo) -> Self: # pragma: no cover self.tz_info = tz_info return self def __getitem__(self, patterns): - if (tz_info := getattr(self, 'tz_info', None)) is not None: - if tz_info is ...: # expect time zone as first argument - tz_info = patterns[0] - if isinstance(tz_info, str): - tz_info = ZoneInfo(tz_info) - - pb = PatternBase( - self.base, - patterns[1:], - ) - else: - pb = PatternBase( - self.base, - (patterns,) if patterns.__class__ is str else patterns, - ) - - return pb.with_tz(tz_info) + if (tz_info := getattr(self, 'tz_info', None)) is ...: + # expect time zone as first argument + tz_info, *patterns = patterns + if isinstance(tz_info, str): + tz_info = ZoneInfo(tz_info) + else: + patterns = (patterns, ) if patterns.__class__ is str else patterns return PatternBase( self.base, - (patterns, ) if patterns.__class__ is str else patterns, + patterns, + tz_info, ) __call__ = __getitem__ @@ -437,9 +428,9 @@ def __repr__(self): Pattern = PatternBase(...) -UTCPattern = PatternBase(...).with_tz(UTC) +UTCPattern = PatternBase(..., tz_info=UTC) -AwarePattern = PatternBase(...).with_tz(...) +AwarePattern = PatternBase(..., tz_info=...) # noinspection PyTypeChecker DatePattern = PatternBase(date) @@ -451,16 +442,16 @@ def __repr__(self): DateTimePattern = PatternBase(datetime) # noinspection PyTypeChecker -UTCTimePattern = PatternBase(time).with_tz(UTC) +UTCTimePattern = PatternBase(time, tz_info=UTC) # noinspection PyTypeChecker -UTCDateTimePattern = PatternBase(datetime).with_tz(UTC) +UTCDateTimePattern = PatternBase(datetime, tz_info=UTC) # noinspection PyTypeChecker -AwareTimePattern = PatternBase(time).with_tz(...) +AwareTimePattern = PatternBase(time, tz_info=...) # noinspection PyTypeChecker -AwareDateTimePattern = PatternBase(datetime).with_tz(...) +AwareDateTimePattern = PatternBase(datetime, tz_info=...) # Instances of Field are only ever created from within this module, # and only from the field() function, although Field instances are diff --git a/dataclass_wizard/v1/models.pyi b/dataclass_wizard/v1/models.pyi index 4960ef4..ad56936 100644 --- a/dataclass_wizard/v1/models.pyi +++ b/dataclass_wizard/v1/models.pyi @@ -86,7 +86,9 @@ class PatternBase: tz_info: tzinfo | Ellipsis - def __init__(self, base, patterns=None, tzname=None): ... + def __init__(self, base: type[DT], + patterns: tuple[str, ...] = None, + tz_info: tzinfo | Ellipsis | None = None): ... def with_tz(self, tz_info: tzinfo | Ellipsis) -> Self: ... diff --git a/tests/unit/v1/test_loaders.py b/tests/unit/v1/test_loaders.py index cee6c11..f8a7f39 100644 --- a/tests/unit/v1/test_loaders.py +++ b/tests/unit/v1/test_loaders.py @@ -17,6 +17,7 @@ List, Optional, Union, Tuple, Dict, NamedTuple, DefaultDict, Set, FrozenSet, Annotated, Literal, Sequence, MutableSequence, Collection ) +from zoneinfo import ZoneInfo import pytest @@ -770,6 +771,69 @@ class MyClass: log.debug('Error details: %r', e.value) +def test_aware_and_utc_date_times_with_custom_pattern(): + """ + Time and datetime objects with a custom date string + format, where the objects are timezone-aware or in UTC. + """ + class MyTime(time, metaclass=create_strict_eq): + def print_hour(self): + print(self.hour) + + @dataclass + class Example(JSONPyWizard): + class _(JSONPyWizard.Meta): + v1 = True + + my_dt1: Annotated[AwareDateTimePattern['Asia/Tokyo', '%m-%Y-%H:%M-%Z'], Alias('key')] + my_dt2: UTCDateTimePattern['%Y-%m-%d %H'] + my_time1: UTCTimePattern['%H:%M:%S'] + my_time2: Annotated[list[MyTime], AwarePattern['US/Hawaii', '%H:%M-%Z']] + + d = {'key': '10-2020-15:30-UTC', + 'my_dt2': '2010-5-7 8', + 'my_time1': '17:10:05', + 'my_time2': ['21:45-UTC']} + ex = Example.from_dict(d) + + # noinspection PyTypeChecker + expected = Example( + my_dt1=datetime(2020, 10, 1, 15, 30, tzinfo=ZoneInfo('Asia/Tokyo')), + my_dt2=datetime(2010, 5, 7, 8, 0, tzinfo=ZoneInfo('UTC')), + my_time1=time(17, 10, 5, tzinfo=ZoneInfo('UTC')), + my_time2=[ + MyTime(21, 45, tzinfo=ZoneInfo('US/Hawaii')), + ]) + + assert ex == expected + + assert ex.to_dict() == { + 'key': '2020-10-01T15:30:00+09:00', + 'my_dt2': '2010-05-07T08:00:00Z', + 'my_time1': '17:10:05Z', + 'my_time2': ['21:45:00']} + + ex = Example.from_dict(ex.to_dict()) + ex = Example.from_dict(ex.to_dict()) + + assert ex == expected + + # De-serializing using `timestamp()` + + d = {'key': expected.my_dt1.timestamp(), + 'my_dt2': int(expected.my_dt2.timestamp()), + 'my_time1': '17:10:05', + 'my_time2': ['21:45-UTC']} + + assert Example.from_dict(d) == expected + + # ParseError: `time` doesn't have `fromtimestamp()`, + # so an integer input should raise an error. + d['my_time1'] = 123 + with pytest.raises(ParseError): + _ = Example.from_dict(d) + + def test_tag_field_is_used_in_load_process(): """ Confirm that the `_TAG` field is used when de-serializing to a dataclass