From e387afcdd49197ee7950633ccb29da82a091a909 Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 09:39:40 +0200 Subject: [PATCH] Upstream myusuf3/delorean state of the touched files before 1d2926e754 --- delorean/dates.py | 774 ++++++++++++++++++++++++ tests/delorean_tests.py | 1246 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 2020 insertions(+) create mode 100644 delorean/dates.py create mode 100644 tests/delorean_tests.py diff --git a/delorean/dates.py b/delorean/dates.py new file mode 100644 index 0000000..4dc00ba --- /dev/null +++ b/delorean/dates.py @@ -0,0 +1,774 @@ +import sys +from datetime import datetime, timedelta, timezone +from functools import partial, update_wrapper +from operator import index + +import humanize +from babel.dates import format_datetime +from dateutil.relativedelta import relativedelta +from tzlocal import get_localzone + +from .exceptions import DeloreanInvalidDatetime, DeloreanInvalidTimezone +from .timezones import timezone as get_timezone +from .timezones import utc + + +def get_total_second(td): + """ + This method takes a timedelta and return the number of seconds it + represents with the resolution of 10**6 + """ + return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 1e6) / 1e6 + + +def is_datetime_naive(dt): + """ + This method returns true if the datetime is naive else returns false + """ + if dt.tzinfo is None: + return True + else: + return False + + +def is_datetime_instance(dt): + if dt is None: + return + if not isinstance(dt, datetime): + raise ValueError("Please provide a datetime instance to Delorean") + + +def _move_datetime(dt, direction, delta): + """ + Move datetime given delta by given direction + """ + if direction == "next": + dt = dt + delta + elif direction == "last": + dt = dt - delta + else: + pass + # raise some delorean error here + return dt + + +def move_datetime_day(dt, direction, num_shifts): + delta = relativedelta(days=+num_shifts) + return _move_datetime(dt, direction, delta) + + +def move_datetime_namedday(dt, direction, unit): + TOTAL_DAYS = 7 + days = { + "monday": 1, + "tuesday": 2, + "wednesday": 3, + "thursday": 4, + "friday": 5, + "saturday": 6, + "sunday": 7, + } + + current_day = days[dt.strftime("%A").lower()] + target_day = days[unit.lower()] + + if direction == "next": + if current_day < target_day: + delta_days = target_day - current_day + else: + delta_days = (target_day - current_day) + TOTAL_DAYS + elif direction == "last": + + if current_day <= target_day: + delta_days = (current_day - target_day) + TOTAL_DAYS + else: + delta_days = current_day - target_day + + delta = relativedelta(days=+delta_days) + return _move_datetime(dt, direction, delta) + + +def move_datetime_month(dt, direction, num_shifts): + """ + Move datetime 1 month in the chosen direction. + unit is a no-op, to keep the API the same as the day case + """ + delta = relativedelta(months=+num_shifts) + return _move_datetime(dt, direction, delta) + + +def move_datetime_week(dt, direction, num_shifts): + """ + Move datetime 1 week in the chosen direction. + unit is a no-op, to keep the API the same as the day case + """ + delta = relativedelta(weeks=+num_shifts) + return _move_datetime(dt, direction, delta) + + +def move_datetime_year(dt, direction, num_shifts): + """ + Move datetime 1 year in the chosen direction. + unit is a no-op, to keep the API the same as the day case + """ + delta = relativedelta(years=+num_shifts) + return _move_datetime(dt, direction, delta) + + +def move_datetime_hour(dt, direction, num_shifts): + delta = relativedelta(hours=+num_shifts) + return _move_datetime(dt, direction, delta) + + +def move_datetime_minute(dt, direction, num_shifts): + delta = relativedelta(minutes=+num_shifts) + return _move_datetime(dt, direction, delta) + + +def move_datetime_second(dt, direction, num_shifts): + delta = relativedelta(seconds=+num_shifts) + return _move_datetime(dt, direction, delta) + + +def datetime_timezone(tz): + """ + This method given a timezone returns a localized datetime object. + """ + utc_datetime_naive = datetime.now(timezone.utc).replace(tzinfo=None) + # return a localized datetime to UTC + utc_localized_datetime = localize(utc_datetime_naive, "UTC") + # normalize the datetime to given timezone + normalized_datetime = normalize(utc_localized_datetime, tz) + return normalized_datetime + + +def localize(dt, tz): + """ + Given a naive datetime object this method will return a localized + datetime object + """ + tz = get_timezone(tz) + + # Attaching a zone to an already-aware datetime would move the instant it + # represents rather than describe it, so refuse rather than guess. + if dt.tzinfo is not None: + raise DeloreanInvalidDatetime("localize requires a naive datetime") + + # A pytz zone from a caller still on 1.x carries no offset until localized; + # attaching one directly would silently yield that zone's LMT offset. + if hasattr(tz, "localize"): + return tz.localize(dt) + + return dt.replace(tzinfo=tz) + + +def normalize(dt, tz): + """ + Given a object with a timezone return a datetime object + normalized to the proper timezone. + + This means take the give localized datetime and returns the + datetime normalized to match the specified timezone. + """ + tz = get_timezone(tz) + + if dt.tzinfo is None: + raise DeloreanInvalidDatetime("normalize requires an aware datetime") + + return dt.astimezone(tz) + + +class Delorean(object): + """ + The class `Delorean ` object. This method accepts naive + datetime objects, with a string timezone. + """ + + _VALID_SHIFT_DIRECTIONS = ("last", "next") + _VALID_SHIFT_UNITS = ( + "second", + "minute", + "hour", + "day", + "week", + "month", + "year", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ) + + def __init__(self, datetime=None, timezone=None): + # maybe set timezone on the way in here. if here set it if not + # use UTC + is_datetime_instance(datetime) + + if datetime: + if is_datetime_naive(datetime): + if timezone: + self._dt = localize(datetime, timezone) + self._tzinfo = self._dt.tzinfo + else: + # TODO(mlew, 2015-08-09): + # Should we really throw an error here, or should this + # default to UTC?) + raise DeloreanInvalidTimezone("Provide a valid timezone") + else: + self._tzinfo = datetime.tzinfo + self._dt = datetime + else: + if timezone: + self._tzinfo = get_timezone(timezone) + self._dt = datetime_timezone(self._tzinfo) + self._tzinfo = self._dt.tzinfo + else: + self._tzinfo = utc + self._dt = datetime_timezone(utc) + + def __repr__(self): + dt = self.datetime.replace(tzinfo=None) + # Both zoneinfo zones and fixed offsets render as something timezone() + # accepts, so this stays round-trippable through eval(). + return "Delorean(datetime=%r, timezone=%r)" % (dt, str(self.timezone)) + + def __eq__(self, other): + if isinstance(other, Delorean): + return self.epoch == other.epoch + return False + + def __lt__(self, other): + return self.epoch < other.epoch + + def __gt__(self, other): + return self.epoch > other.epoch + + def __ge__(self, other): + return self.epoch >= other.epoch + + def __le__(self, other): + return self.epoch <= other.epoch + + def __ne__(self, other): + return not self == other + + def __add__(self, other): + if not isinstance(other, timedelta): + raise TypeError("Delorean objects can only be added with timedelta objects") + dt = self._dt + other + return Delorean(datetime=dt, timezone=self.timezone) + + def __sub__(self, other): + if isinstance(other, timedelta): + dt = self._dt - other + return Delorean(datetime=dt, timezone=self.timezone) + elif isinstance(other, Delorean): + return self._dt - other._dt + else: + raise TypeError( + "Delorean objects can only be subtracted with timedelta or other Delorean objects" + ) + + def __getattr__(self, name): + """ + Implement __getattr__ to call `shift_date` function when function + called does not exist + """ + func_parts = name.split("_") + # is the func we are trying to call the right length? + if len(func_parts) != 2: + raise AttributeError + + # is the function we are trying to call valid? + if ( + func_parts[0] not in self._VALID_SHIFT_DIRECTIONS + or func_parts[1] not in self._VALID_SHIFT_UNITS + ): + raise AttributeError + + # dispatch our function + func = partial(self._shift_date, func_parts[0], func_parts[1]) + # update our partial with self.shift_date attributes + update_wrapper(func, self._shift_date) + return func + + def _shift_date(self, direction, unit, *args): + """ + Shift datetime in `direction` in _VALID_SHIFT_DIRECTIONS and by some + unit in _VALID_SHIFTS and shift that amount by some multiple, + defined by by args[0] if it exists + """ + this_module = sys.modules[__name__] + + num_shifts = 1 + if len(args) > 0: + if unit == "second": + num_shifts = args[0] + else: + try: + num_shifts = index(args[0]) + except TypeError: + raise TypeError(f"{unit} shifts require an integer") from None + + if unit in [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ]: + shift_func = move_datetime_namedday + dt = shift_func(self._dt, direction, unit) + if num_shifts > 1: + for n in range(num_shifts - 1): + dt = shift_func(dt, direction, unit) + else: + shift_func = getattr(this_module, "move_datetime_%s" % unit) + dt = shift_func(self._dt, direction, num_shifts) + + return Delorean(datetime=dt.replace(tzinfo=None), timezone=self.timezone) + + @property + def timezone(self): + """ + Returns the tzinfo object associated with the Delorean object: a + `zoneinfo.ZoneInfo` for a named zone, or a `datetime.timezone` for a + fixed offset. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1), timezone='UTC') + >>> d.timezone + zoneinfo.ZoneInfo(key='UTC') + + .. versionchanged:: 2.0 + Timezones are standard library objects. Earlier versions returned + `pytz` objects, so comparisons against `pytz.utc` and friends need + updating to :func:`delorean.timezone` or `zoneinfo.ZoneInfo`. + + """ + return self._tzinfo + + def truncate(self, s): + """ + Truncate the delorean object to the nearest s + (second, minute, hour, day, month, year) + + This is a destructive method, modifies the internal datetime + object associated with the Delorean object. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1, 12, 10), timezone='US/Pacific') + >>> d.truncate('hour') + Delorean(datetime=datetime.datetime(2015, 1, 1, 12, 0), timezone='US/Pacific') + + """ + if s == "second": + self._dt = self._dt.replace(microsecond=0) + elif s == "minute": + self._dt = self._dt.replace(second=0, microsecond=0) + elif s == "hour": + self._dt = self._dt.replace(minute=0, second=0, microsecond=0) + elif s == "day": + self._dt = self._dt.replace(hour=0, minute=0, second=0, microsecond=0) + elif s == "month": + self._dt = self._dt.replace( + day=1, hour=0, minute=0, second=0, microsecond=0 + ) + elif s == "year": + self._dt = self._dt.replace( + month=1, day=1, hour=0, minute=0, second=0, microsecond=0 + ) + else: + raise ValueError("Invalid truncation level") + + return self + + @property + def naive(self): + """ + Returns a naive datetime object associated with the Delorean + object, this method simply converts the localize datetime to UTC + and removes the tzinfo that is associated with it modifying the Delorean object. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1), timezone='US/Pacific') + >>> d.naive + datetime.datetime(2015, 1, 1, 8, 0) + + """ + self.shift("UTC") + return self._dt.replace(tzinfo=None) + + @classmethod + def now(cls, timezone=None): + if timezone: + return cls(timezone=timezone) + else: + return cls(timezone=get_localzone()) + + @classmethod + def utcnow(cls): + return cls() + + @property + def midnight(self): + """ + Returns a new `Delorean` object for midnight of the day associated + with this object. This object is left unchanged. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1, 12), timezone='UTC') + >>> d.midnight + Delorean(datetime=datetime.datetime(2015, 1, 1, 0, 0), timezone='UTC') + + """ + return Delorean( + datetime=self._dt.replace(hour=0, minute=0, second=0, microsecond=0), + timezone=self.timezone, + ) + + @property + def start_of_day(self): + """ + Returns a new `Delorean` object for the start of the day associated + with this object. This object is left unchanged. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1, 12), timezone='UTC') + >>> d.start_of_day + Delorean(datetime=datetime.datetime(2015, 1, 1, 0, 0), timezone='UTC') + + """ + return self.midnight + + @property + def end_of_day(self): + """ + Returns a new `Delorean` object for the end of the day associated + with this object. This object is left unchanged. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1, 12), timezone='UTC') + >>> d.end_of_day + Delorean(datetime=datetime.datetime(2015, 1, 1, 23, 59, 59, 999999), timezone='UTC') + + """ + return Delorean( + datetime=self._dt.replace( + hour=23, minute=59, second=59, microsecond=999999 + ), + timezone=self.timezone, + ) + + @property + def start_of_month(self): + """ + Returns a new `Delorean` object for the first moment of the month + associated with this object. This object is left unchanged. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 5, 15, 14, 30), timezone='UTC') + >>> d.start_of_month + Delorean(datetime=datetime.datetime(2015, 5, 1, 0, 0), timezone='UTC') + + """ + return Delorean( + datetime=self._dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0), + timezone=self.timezone, + ) + + @property + def end_of_month(self): + """ + Returns a new `Delorean` object for the last moment of the month + associated with this object, accounting for month length and leap + years. This object is left unchanged. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 2, 15, 14, 30), timezone='UTC') + >>> d.end_of_month + Delorean(datetime=datetime.datetime(2015, 2, 28, 23, 59, 59, 999999), timezone='UTC') + + """ + end = self._dt.replace( + hour=23, minute=59, second=59, microsecond=999999 + ) + relativedelta(day=31) + return Delorean(datetime=end, timezone=self.timezone) + + @property + def start_of_year(self): + """ + Returns a new `Delorean` object for the first moment of the year + associated with this object. This object is left unchanged. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 5, 15, 14, 30), timezone='UTC') + >>> d.start_of_year + Delorean(datetime=datetime.datetime(2015, 1, 1, 0, 0), timezone='UTC') + + """ + return Delorean( + datetime=self._dt.replace( + month=1, day=1, hour=0, minute=0, second=0, microsecond=0 + ), + timezone=self.timezone, + ) + + @property + def end_of_year(self): + """ + Returns a new `Delorean` object for the last moment of the year + associated with this object. This object is left unchanged. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 5, 15, 14, 30), timezone='UTC') + >>> d.end_of_year + Delorean(datetime=datetime.datetime(2015, 12, 31, 23, 59, 59, 999999), timezone='UTC') + + """ + return Delorean( + datetime=self._dt.replace( + month=12, day=31, hour=23, minute=59, second=59, microsecond=999999 + ), + timezone=self.timezone, + ) + + def shift(self, timezone): + """ + Shifts the timezone from the current timezone to the specified timezone associated with the Delorean object, + modifying the Delorean object and returning the modified object. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1), timezone='US/Pacific') + >>> d.shift('UTC') + Delorean(datetime=datetime.datetime(2015, 1, 1, 8, 0), timezone='UTC') + + """ + try: + self._tzinfo = get_timezone(timezone) + except DeloreanInvalidTimezone: + raise DeloreanInvalidTimezone("Provide a valid timezone") from None + self._dt = self._dt.astimezone(self._tzinfo) + self._tzinfo = self._dt.tzinfo + return self + + @property + def timestamp(self): + """ + Returns the total seconds since the Unix epoch associated with + the Delorean object. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1), timezone='US/Pacific') + >>> d.timestamp + 1420099200.0 + + """ + epoch_sec = datetime.fromtimestamp(0, timezone.utc) + now_sec = self._dt.astimezone(utc) + delta_sec = now_sec - epoch_sec + return get_total_second(delta_sec) + + @property + def epoch(self): + """ + Alias of :attr:`timestamp`, kept for backwards compatibility. + + The value is seconds since the Unix epoch, so `timestamp` is the + more accurate name; the epoch itself is the fixed point the count + starts from. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1), timezone='US/Pacific') + >>> d.epoch + 1420099200.0 + + """ + return self.timestamp + + @property + def date(self): + """ + Returns the actual date object associated with + the Delorean object. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1, 12, 15), timezone='US/Pacific') + >>> d.date + datetime.date(2015, 1, 1) + + """ + return self._dt.date() + + @property + def datetime(self): + """ + Returns the actual datetime object associated with + the Delorean object. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1, 12, 15), timezone='UTC') + >>> d.datetime + datetime.datetime(2015, 1, 1, 12, 15, tzinfo=zoneinfo.ZoneInfo(key='UTC')) + """ + return self._dt + + def replace(self, **kwargs): + """ + Returns a new Delorean object after applying replace on the + existing datetime object. + + .. testsetup:: + + from datetime import datetime + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1, 12, 15), timezone='UTC') + >>> d.replace(hour=8) + Delorean(datetime=datetime.datetime(2015, 1, 1, 8, 15), timezone='UTC') + """ + + return Delorean(datetime=self._dt.replace(**kwargs), timezone=self.timezone) + + def humanize(self): + """ + Humanize relative to now: + + .. testsetup:: + + from datetime import timedelta + from delorean import Delorean + + .. doctest:: + + >>> past = Delorean.utcnow() - timedelta(hours=1) + >>> past.humanize() + 'an hour ago' + + """ + now = self.now(self.timezone) + + return humanize.naturaltime(now - self) + + def format_datetime(self, format="medium", locale="en_US"): + r""" + Return a date string formatted to the given pattern. + + Note that CLDR separates the time from the AM/PM marker with a narrow + no-break space (``\u202f``), not a plain space. + + .. testsetup:: + + from delorean import Delorean + + .. doctest:: + + >>> d = Delorean(datetime(2015, 1, 1, 12, 30), timezone='US/Pacific') + >>> d.format_datetime(locale='en_US') + 'Jan 1, 2015, 12:30:00\u202fPM' + + >>> d.format_datetime(format='long', locale='de_DE') + '1. Januar 2015, 12:30:00 -0800' + + :param format: one of "full", "long", "medium", "short", or a custom datetime pattern + :param locale: a locale identifier + + """ + return format_datetime(self._dt, format=format, locale=locale) diff --git a/tests/delorean_tests.py b/tests/delorean_tests.py new file mode 100644 index 0000000..5ab5061 --- /dev/null +++ b/tests/delorean_tests.py @@ -0,0 +1,1246 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Testing for Delorean +""" + +import unittest +from copy import deepcopy +from datetime import date, datetime, timedelta, timezone, tzinfo +from unittest import mock +from zoneinfo import ZoneInfo + +from dateutil.parser import UnknownTimezoneWarning +from dateutil.tz import tzlocal, tzoffset + +import delorean + + +def naive_utcnow(): + return datetime.now(timezone.utc).replace(tzinfo=None) + + +class GenericUTC(tzinfo): + """GenericUTC""" + + ZERO = timedelta(0) + + def utcoffset(self, dt): + return self.ZERO + + def tzname(self, dt): + return "GenericUTC" + + def dst(self, dt): + return self.ZERO + + +UTC = "UTC" +generic_utc = GenericUTC() +est = naive_utcnow().replace(tzinfo=ZoneInfo("US/Eastern")).tzinfo + + +class DeloreanTests(unittest.TestCase): + def setUp(self): + self.naive_dt = datetime(2013, 1, 3, 4, 31, 14, 148540) + self.do = delorean.Delorean(datetime=self.naive_dt, timezone="UTC") + + def test_date_failure(self): + dt = date(2013, 5, 6) + self.assertRaises(ValueError, delorean.Delorean, dt) + + def test_initialize_from_datetime_naive(self): + self.assertRaises( + delorean.DeloreanInvalidTimezone, delorean.Delorean, datetime=self.naive_dt + ) + + def test_initialize_with_tzinfo_generic(self): + aware_dt_generic = datetime(2013, 1, 3, 4, 31, 14, 148540, tzinfo=generic_utc) + do = delorean.Delorean(datetime=aware_dt_generic) + self.assertTrue(isinstance(do, delorean.Delorean)) + + def test_initialize_with_tzinfo_zoneinfo(self): + aware_dt_zoneinfo = datetime(2013, 1, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + do = delorean.Delorean(datetime=aware_dt_zoneinfo) + self.assertTrue(isinstance(do, delorean.Delorean)) + + def test_initialize_with_invalid_datetime(self): + self.assertRaises(ValueError, delorean.Delorean, "2015-01-01") + + def test_initialize_with_naive_datetime_with_zoneinfo_timezone(self): + dt = datetime(2015, 1, 1) + do = delorean.Delorean(dt, timezone=delorean.utc) + + dt = dt.replace(tzinfo=timezone.utc) + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, delorean.utc) + + def test_initialize_with_naive_datetime_with_dateutil_timezone(self): + dt = datetime(2015, 1, 1) + tz = tzoffset(None, -22500) + do = delorean.Delorean(dt, timezone=tz) + + utcoffset = tz.utcoffset(None) + total_seconds = ( + utcoffset.microseconds + + (utcoffset.seconds + utcoffset.days * 24 * 3600) * 10**6 + ) / 10**6 + tz = timezone(timedelta(minutes=total_seconds / 60)) + dt = dt.replace(tzinfo=tz) + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, tz) + + def test_initialize_with_naive_datetime_with_string_timezone(self): + dt = datetime(2015, 1, 1) + tz_str = "US/Pacific" + tz = ZoneInfo(tz_str) + do = delorean.Delorean(dt, timezone=tz_str) + tz = datetime(2015, 1, 1).replace(tzinfo=tz).tzinfo + + dt = dt.replace(tzinfo=tz) + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, tz) + + def test_initialize_with_naive_datetime_without_timezone(self): + self.assertRaises( + delorean.DeloreanInvalidTimezone, delorean.Delorean, datetime(2015, 1, 1) + ) + + def test_initialize_with_datetime_with_timezone(self): + dt = datetime(2015, 1, 1).replace(tzinfo=timezone.utc) + do = delorean.Delorean(dt, timezone=ZoneInfo("US/Pacific")) + + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, timezone.utc) + + def test_initialize_with_datetime_without_timezone(self): + dt = datetime(2015, 1, 1).replace(tzinfo=timezone.utc) + do = delorean.Delorean(dt) + + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, timezone.utc) + + @mock.patch("delorean.dates.datetime_timezone") + def test_initialize_without_datetime_with_zoneinfo_timezone( + self, mock_datetime_timezone + ): + tz = ZoneInfo("US/Pacific") + dt = datetime(2015, 1, 1).replace(tzinfo=tz) + tz = dt.tzinfo + mock_datetime_timezone.return_value = dt + + do = delorean.Delorean(timezone=tz) + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, tz) + + @mock.patch("delorean.dates.datetime_timezone") + def test_initialize_without_datetime_with_dateutil_timezone( + self, mock_datetime_timezone + ): + tz = tzoffset(None, -22500) + dt = datetime(2015, 1, 1, tzinfo=tz) + + utcoffset = tz.utcoffset(None) + total_seconds = ( + utcoffset.microseconds + + (utcoffset.seconds + utcoffset.days * 24 * 3600) * 10**6 + ) / 10**6 + + tz = timezone(timedelta(minutes=total_seconds / 60)) + dt = dt.astimezone(tz) + mock_datetime_timezone.return_value = dt + + do = delorean.Delorean(timezone=tz) + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, tz) + + @mock.patch("delorean.dates.datetime_timezone") + def test_initialize_without_datetime_with_string_timezone( + self, mock_datetime_timezone + ): + tz_str = "US/Pacific" + tz = ZoneInfo(tz_str) + dt = datetime(2015, 1, 1).replace(tzinfo=tz) + tz = dt.tzinfo + mock_datetime_timezone.return_value = dt + + do = delorean.Delorean(timezone=tz_str) + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, tz) + + @mock.patch("delorean.dates.datetime_timezone") + def test_initialize_without_datetime_without_timezone(self, mock_datetime_timezone): + dt = datetime(2015, 1, 1).replace(tzinfo=timezone.utc) + mock_datetime_timezone.return_value = dt + + do = delorean.Delorean() + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, delorean.utc) + + def test_truncation_hour(self): + self.do.truncate("hour") + self.assertEqual(self.do.naive, datetime(2013, 1, 3, 4, 0)) + + def test_midnight(self): + do = self.do.midnight + self.assertIsInstance(do, delorean.Delorean) + self.assertEqual( + do.datetime, datetime(2013, 1, 3, 0, 0, 0, tzinfo=delorean.utc) + ) + + def test_start_of_day(self): + do = self.do.start_of_day + self.assertIsInstance(do, delorean.Delorean) + self.assertEqual( + do.datetime, datetime(2013, 1, 3, 0, 0, 0, 0, tzinfo=delorean.utc) + ) + + def test_end_of_day(self): + do = self.do.end_of_day + self.assertIsInstance(do, delorean.Delorean) + self.assertEqual( + do.datetime, datetime(2013, 1, 3, 23, 59, 59, 999999, tzinfo=delorean.utc) + ) + + def test_day_boundaries_are_chainable(self): + do = self.do.last_month().end_of_day + self.assertIsInstance(do, delorean.Delorean) + self.assertEqual( + do.datetime, datetime(2012, 12, 3, 23, 59, 59, 999999, tzinfo=delorean.utc) + ) + # Still a Delorean, so the chain can continue. + self.assertIsInstance(do.shift("US/Eastern"), delorean.Delorean) + + def test_start_of_month(self): + do = delorean.Delorean(datetime(2015, 5, 15, 14, 30), timezone="UTC") + self.assertEqual( + do.start_of_month.datetime, datetime(2015, 5, 1, tzinfo=delorean.utc) + ) + + def test_end_of_month(self): + do = delorean.Delorean(datetime(2015, 5, 15, 14, 30), timezone="UTC") + self.assertEqual( + do.end_of_month.datetime, + datetime(2015, 5, 31, 23, 59, 59, 999999, tzinfo=delorean.utc), + ) + + def test_end_of_month_short_month(self): + do = delorean.Delorean(datetime(2015, 2, 15), timezone="UTC") + self.assertEqual(do.end_of_month.datetime.day, 28) + + def test_end_of_month_leap_year(self): + do = delorean.Delorean(datetime(2024, 2, 15), timezone="UTC") + self.assertEqual(do.end_of_month.datetime.day, 29) + + def test_start_of_year(self): + do = delorean.Delorean(datetime(2015, 5, 15, 14, 30), timezone="UTC") + self.assertEqual( + do.start_of_year.datetime, datetime(2015, 1, 1, tzinfo=delorean.utc) + ) + + def test_end_of_year(self): + do = delorean.Delorean(datetime(2015, 5, 15, 14, 30), timezone="UTC") + self.assertEqual( + do.end_of_year.datetime, + datetime(2015, 12, 31, 23, 59, 59, 999999, tzinfo=delorean.utc), + ) + + def test_last_day_of_previous_month(self): + # The use case from issue #85. + do = delorean.Delorean(datetime(2013, 5, 15, 14, 30), timezone="US/Eastern") + end = do.last_month().end_of_month + self.assertIsInstance(end, delorean.Delorean) + self.assertEqual(end.datetime.month, 4) + self.assertEqual(end.datetime.day, 30) + + def test_month_and_year_boundaries_do_not_mutate(self): + before = self.do.datetime + self.do.start_of_month + self.do.end_of_month + self.do.start_of_year + self.do.end_of_year + self.assertEqual(self.do.datetime, before) + + def test_day_boundaries_do_not_mutate(self): + before = self.do.datetime + self.do.midnight + self.do.start_of_day + self.do.end_of_day + self.assertEqual(self.do.datetime, before) + + def test_truncation_second(self): + self.do.truncate("second") + self.assertEqual(self.do.naive, datetime(2013, 1, 3, 4, 31, 14, 0)) + + def test_truncation_minute(self): + self.do.truncate("minute") + self.assertEqual(self.do.naive, datetime(2013, 1, 3, 4, 31, 0, 0)) + + def test_truncation_day(self): + self.do.truncate("day") + self.assertEqual(self.do.naive, datetime(2013, 1, 3, 0, 0, 0, 0)) + + def test_truncation_month(self): + self.do.truncate("month") + self.assertEqual(self.do.naive, datetime(2013, 1, 1, 0, 0, 0, 0)) + + def test_truncation_year(self): + self.do.truncate("year") + self.assertEqual(self.do.naive, datetime(2013, 1, 1, 0, 0, 0, 0)) + + def test_date(self): + self.assertEqual(self.do.date, date(2013, 1, 3)) + + def test_datetime(self): + self.assertEqual(self.do.naive, datetime(2013, 1, 3, 4, 31, 14, 148540)) + + def test_naive(self): + dt1 = delorean.Delorean() + dt_naive = dt1.naive + self.assertEqual(dt_naive.tzinfo, None) + + def test_naive_timezone(self): + dt1 = delorean.Delorean(timezone="US/Eastern").truncate("minute").naive + dt2 = delorean.Delorean().truncate("minute").naive + self.assertEqual(dt2, dt1) + self.assertEqual(dt1.tzinfo, None) + + def test_localize(self): + dt = datetime.today() + dt = delorean.localize(dt, "UTC") + self.assertEqual(dt.tzinfo, delorean.utc) + + def test_failure_truncation(self): + self.assertRaises(ValueError, self.do.truncate, "century") + + def test_normalize(self): + dt1 = delorean.Delorean() + dt2 = delorean.Delorean(timezone="US/Eastern") + dt1.truncate("minute") + dt2.truncate("minute") + dt_normalized = delorean.normalize(dt1.datetime, "US/Eastern") + self.assertEqual(dt2.datetime, dt_normalized) + + def test_normalize_failure(self): + naive_datetime = datetime.today() + self.assertRaises(ValueError, delorean.normalize, naive_datetime, "US/Eastern") + + def test_localize_failure(self): + dt1 = delorean.localize(naive_utcnow(), "UTC") + self.assertRaises(ValueError, delorean.localize, dt1, "UTC") + + def test_timezone(self): + do_timezone = delorean.Delorean().timezone + self.assertEqual(delorean.utc, do_timezone) + + def test_datetime_timezone_default(self): + do = delorean.Delorean() + do.truncate("minute") + dt1 = delorean.datetime_timezone(UTC) + self.assertEqual(dt1.replace(second=0, microsecond=0), do.datetime) + + def test_datetime_timezone(self): + do = delorean.Delorean(timezone="US/Eastern") + do.truncate("minute") + dt1 = delorean.datetime_timezone(tz="US/Eastern") + self.assertEqual(dt1.replace(second=0, microsecond=0), do.datetime) + + def test_parse(self): + with self.assertWarns(UnknownTimezoneWarning): + do = delorean.parse("Thu Sep 25 10:36:28 BRST 2003") + dt1 = datetime(2003, 9, 25, 10, 36, 28).replace(tzinfo=timezone.utc) + self.assertEqual(do.datetime, dt1) + + def test_parse_iso(self): + do = delorean.parse("2017-05-02T05:38:49Z") + dt1 = datetime(2017, 5, 2, 5, 38, 49).replace(tzinfo=timezone.utc) + self.assertEqual(do.datetime, dt1) + + def test_parse_default(self): + do = delorean.parse("2016-07-01T11:00:00+02:00", dayfirst=False) + dt1 = delorean.Delorean( + datetime=datetime(2016, 7, 1, 11, 0), + timezone=timezone(timedelta(minutes=120)), + ) + self.assertEqual(do.datetime, dt1.datetime) + + def test_parse_with_invalid_datetime_string(self): + self.assertRaises(ValueError, delorean.parse, "asd") + + def test_parse_timezone_less_string_assumes_utc_by_default(self): + do = delorean.parse("2015-02-04T16:33:21.247513") + + self.assertEqual( + do.datetime, + datetime(2015, 2, 4, 16, 33, 21, 247513, tzinfo=delorean.utc), + ) + + def test_parse_timezone_less_string_can_require_assumption(self): + with self.assertRaisesRegex( + delorean.DeloreanInvalidDatetime, "assume_timezone" + ): + delorean.parse( + "2015-02-04T16:33:21.247513", + assume_timezone=None, + ) + + def test_parse_with_assumed_utc_timezone(self): + do = delorean.parse("2015-02-04T16:33:21.247513", assume_timezone="UTC") + + self.assertIsInstance(do, delorean.Delorean) + self.assertEqual( + do.datetime, + datetime(2015, 2, 4, 16, 33, 21, 247513, tzinfo=delorean.utc), + ) + + def test_parse_with_assumed_non_utc_timezone(self): + do = delorean.parse( + "2015-02-04T16:33:21.247513", + assume_timezone="America/Toronto", + ) + expected = datetime(2015, 2, 4, 16, 33, 21, 247513).replace( + tzinfo=ZoneInfo("America/Toronto") + ) + + self.assertIsInstance(do, delorean.Delorean) + self.assertEqual(do.datetime, expected) + + def test_parse_with_assumed_timezone_object(self): + timezone = ZoneInfo("America/Toronto") + do = delorean.parse( + "2015-02-04T16:33:21.247513", + assume_timezone=timezone, + ) + expected = datetime(2015, 2, 4, 16, 33, 21, 247513).replace(tzinfo=timezone) + + self.assertEqual(do.datetime, expected) + + def test_parse_does_not_apply_assumed_timezone_to_aware_string(self): + do = delorean.parse( + "2015-02-04T16:33:21.247513-05:00", + assume_timezone="US/Pacific", + ) + + self.assertEqual(do.timezone, timezone(timedelta(minutes=-300))) + + def test_parse_timezone_override_takes_precedence_over_assumption(self): + do = delorean.parse( + "2015-02-04T16:33:21.247513-05:00", + timezone="US/Pacific", + assume_timezone="UTC", + ) + expected = datetime(2015, 2, 4, 16, 33, 21, 247513).replace( + tzinfo=ZoneInfo("US/Pacific") + ) + + self.assertEqual(do.datetime, expected) + + def test_parse_timezone_override_applies_to_timezone_less_string(self): + do = delorean.parse( + "2015-02-04T16:33:21.247513", + timezone="US/Pacific", + assume_timezone=None, + ) + expected = datetime(2015, 2, 4, 16, 33, 21, 247513).replace( + tzinfo=ZoneInfo("US/Pacific") + ) + + self.assertEqual(do.datetime, expected) + + def test_parse_does_not_apply_assumed_timezone_to_utc_designator(self): + do = delorean.parse("2013-09-30T15:34:00.000Z", assume_timezone="US/Pacific") + + self.assertEqual( + do.datetime, datetime(2013, 9, 30, 15, 34).replace(tzinfo=timezone.utc) + ) + + def test_parse_with_assumed_zoneinfo_timezone(self): + tz = ZoneInfo("America/Toronto") + do = delorean.parse("2015-02-04T16:33:21.247513", assume_timezone=tz) + + self.assertEqual( + do.datetime, + datetime(2015, 2, 4, 16, 33, 21, 247513, tzinfo=tz), + ) + + def test_parse_with_assumed_timezone_resolves_nonexistent_local_time(self): + do = delorean.parse("2013-03-10T02:30:00", assume_timezone="US/Eastern") + + self.assertEqual(do.datetime.tzname(), "EST") + + def test_parse_with_assumed_timezone_takes_the_first_ambiguous_occurrence(self): + do = delorean.parse("2013-11-03T01:30:00", assume_timezone="US/Eastern") + + self.assertEqual(do.datetime.tzname(), "EDT") + + def test_parse_strict_error_is_a_value_error(self): + with self.assertRaises(ValueError): + delorean.parse("2015-02-04T16:33:21.247513", assume_timezone=None) + + def test_parse_with_utc_year_fill(self): + do = delorean.parse("Thu Sep 25 10:36:28") + dt1 = datetime(date.today().year, 9, 25, 10, 36, 28, tzinfo=delorean.utc) + self.assertEqual(do.datetime, dt1) + + def test_parse_with_assumed_timezone_year_fill(self): + do = delorean.parse("Thu Sep 25 10:36:28", assume_timezone="UTC") + dt1 = datetime(date.today().year, 9, 25, 10, 36, 28, tzinfo=delorean.utc) + self.assertEqual(do.datetime, dt1) + self.assertEqual(do.timezone.tzname(None), "UTC") + + def test_parse_with_fixed_offset_timezone(self): + tz = timezone(timedelta(minutes=-480)) + dt = datetime(2015, 1, 1).replace(tzinfo=tz) + dt_str = dt.strftime("%Y-%m-%d %H:%M:%S %z") + + do = delorean.parse(dt_str) + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, tz) + + @mock.patch("delorean.interface.get_localzone") + def test_parse_with_tzlocal_timezone(self, mock_get_local_zone): + tz = ZoneInfo("US/Eastern") + mock_get_local_zone.return_value = tz + dt = datetime(2015, 1, 1, tzinfo=tzlocal()) + dt_str = dt.strftime("%Y-%m-%d %H:%M:%S %Z") + dt = dt.replace(tzinfo=None) + dt = dt.replace(tzinfo=tz) + tz = dt.tzinfo + + do = delorean.parse(dt_str) + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, tz) + + @mock.patch("delorean.interface.get_localzone") + def test_parse_with_tzutc_timezone(self, mock_get_localzone): + mock_get_localzone.return_value = delorean.utc + dt = datetime(2015, 1, 1).replace(tzinfo=timezone.utc) + dt_str = dt.strftime("%Y-%m-%d %H:%M:%S %Z") + + do = delorean.parse(dt_str) + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, delorean.utc) + + def test_parse_with_timezone_parameter(self): + tz = ZoneInfo("US/Pacific") + dt = datetime(2015, 1, 1).replace(tzinfo=tz) + tz = dt.tzinfo + dt_str = dt.strftime("%Y-%m-%d %H:%M:%S") + + do = delorean.parse(dt_str, timezone="US/Pacific") + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, tz) + + def test_parse_with_overriding_timezone_parameter(self): + tz = ZoneInfo("US/Pacific") + dt = datetime(2015, 1, 1).replace(tzinfo=tz) + tz = dt.tzinfo + dt_str = dt.strftime("%Y-%m-%d %H:%M:%S -0500") + + do = delorean.parse(dt_str, timezone="US/Pacific") + self.assertEqual(do.datetime, dt) + self.assertEqual(do.timezone, tz) + + def test_move_namedday(self): + dt_next = datetime(2013, 1, 4, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_next_2 = datetime(2013, 1, 11, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2012, 12, 28, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last_2 = datetime(2012, 12, 21, 4, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = self.do.next_friday() + d_obj_next_2 = self.do.next_friday(2) + d_obj_last = self.do.last_friday() + d_obj_last_2 = self.do.last_friday(2) + + self.assertEqual(dt_next, d_obj_next.datetime) + self.assertEqual(dt_last, d_obj_last.datetime) + self.assertEqual(dt_next_2, d_obj_next_2.datetime) + self.assertEqual(dt_last_2, d_obj_last_2.datetime) + + def test_move_namedday_function(self): + dt_next = datetime(2013, 1, 4, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2012, 12, 28, 4, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = delorean.move_datetime_namedday(self.do.datetime, "next", "friday") + d_obj_last = delorean.move_datetime_namedday(self.do.datetime, "last", "friday") + + self.assertEqual(dt_next, d_obj_next) + self.assertEqual(dt_last, d_obj_last) + + def test_move_week(self): + dt_next = datetime(2013, 1, 10, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_next_2 = datetime(2013, 1, 17, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2012, 12, 27, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last_2 = datetime(2012, 12, 20, 4, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = self.do.next_week() + d_obj_next_2 = self.do.next_week(2) + d_obj_last = self.do.last_week() + d_obj_last_2 = self.do.last_week(2) + + self.assertEqual(dt_next, d_obj_next.datetime) + self.assertEqual(dt_last, d_obj_last.datetime) + self.assertEqual(dt_next_2, d_obj_next_2.datetime) + self.assertEqual(dt_last_2, d_obj_last_2.datetime) + + def test_move_week_function(self): + dt_next = datetime(2013, 1, 10, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2012, 12, 27, 4, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = delorean.move_datetime_week(self.do.datetime, "next", 1) + d_obj_last = delorean.move_datetime_week(self.do.datetime, "last", 1) + + self.assertEqual(dt_next, d_obj_next) + self.assertEqual(dt_last, d_obj_last) + + def test_move_month(self): + dt_next = datetime(2013, 2, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_next_2 = datetime(2013, 3, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2012, 12, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last_2 = datetime(2012, 11, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = self.do.next_month() + d_obj_next_2 = self.do.next_month(2) + d_obj_last = self.do.last_month() + d_obj_last_2 = self.do.last_month(2) + + self.assertEqual(dt_next, d_obj_next.datetime) + self.assertEqual(dt_last, d_obj_last.datetime) + self.assertEqual(dt_next_2, d_obj_next_2.datetime) + self.assertEqual(dt_last_2, d_obj_last_2.datetime) + + def test_move_month_function(self): + dt_next = datetime(2013, 2, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2012, 12, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = delorean.move_datetime_month(self.do.datetime, "next", 1) + d_obj_last = delorean.move_datetime_month(self.do.datetime, "last", 1) + + self.assertEqual(dt_next, d_obj_next) + self.assertEqual(dt_last, d_obj_last) + + def test_move_day_function(self): + dt_next = datetime(2013, 1, 4, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2013, 1, 2, 4, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = delorean.move_datetime_day(self.do.datetime, "next", 1) + d_obj_last = delorean.move_datetime_day(self.do.datetime, "last", 1) + + self.assertEqual(dt_next, d_obj_next) + self.assertEqual(dt_last, d_obj_last) + + def test_move_year(self): + dt_next = datetime(2014, 1, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_next_2 = datetime(2015, 1, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2012, 1, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last_2 = datetime(2011, 1, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = self.do.next_year() + d_obj_next_2 = self.do.next_year(2) + d_obj_last = self.do.last_year() + d_obj_last_2 = self.do.last_year(2) + + self.assertEqual(dt_next, d_obj_next.datetime) + self.assertEqual(dt_last, d_obj_last.datetime) + self.assertEqual(dt_next_2, d_obj_next_2.datetime) + self.assertEqual(dt_last_2, d_obj_last_2.datetime) + + def test_move_year_function(self): + dt_next = datetime(2014, 1, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2012, 1, 3, 4, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = delorean.move_datetime_year(self.do.datetime, "next", 1) + d_obj_last = delorean.move_datetime_year(self.do.datetime, "last", 1) + + self.assertEqual(dt_next, d_obj_next) + self.assertEqual(dt_last, d_obj_last) + + def test_move_hour(self): + dt_next = datetime(2013, 1, 3, 5, 31, 14, 148540, tzinfo=delorean.utc) + dt_next_2 = datetime(2013, 1, 3, 6, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2013, 1, 3, 3, 31, 14, 148540, tzinfo=delorean.utc) + dt_last_2 = datetime(2013, 1, 3, 2, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = self.do.next_hour() + d_obj_next_2 = self.do.next_hour(2) + d_obj_last = self.do.last_hour() + d_obj_last_2 = self.do.last_hour(2) + + self.assertEqual(dt_next, d_obj_next.datetime) + self.assertEqual(dt_last, d_obj_last.datetime) + self.assertEqual(dt_next_2, d_obj_next_2.datetime) + self.assertEqual(dt_last_2, d_obj_last_2.datetime) + + def test_move_hour_function(self): + dt_next = datetime(2013, 1, 3, 5, 31, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2013, 1, 3, 3, 31, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = delorean.move_datetime_hour(self.do.datetime, "next", 1) + d_obj_last = delorean.move_datetime_hour(self.do.datetime, "last", 1) + + self.assertEqual(dt_next, d_obj_next) + self.assertEqual(dt_last, d_obj_last) + + def test_move_minute(self): + dt_next = datetime(2013, 1, 3, 4, 32, 14, 148540, tzinfo=delorean.utc) + dt_next_2 = datetime(2013, 1, 3, 4, 33, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2013, 1, 3, 4, 30, 14, 148540, tzinfo=delorean.utc) + dt_last_2 = datetime(2013, 1, 3, 4, 29, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = self.do.next_minute() + d_obj_next_2 = self.do.next_minute(2) + d_obj_last = self.do.last_minute() + d_obj_last_2 = self.do.last_minute(2) + + self.assertEqual(dt_next, d_obj_next.datetime) + self.assertEqual(dt_last, d_obj_last.datetime) + self.assertEqual(dt_next_2, d_obj_next_2.datetime) + self.assertEqual(dt_last_2, d_obj_last_2.datetime) + + def test_move_minute_function(self): + dt_next = datetime(2013, 1, 3, 4, 32, 14, 148540, tzinfo=delorean.utc) + dt_last = datetime(2013, 1, 3, 4, 30, 14, 148540, tzinfo=delorean.utc) + + d_obj_next = delorean.move_datetime_minute(self.do.datetime, "next", 1) + d_obj_last = delorean.move_datetime_minute(self.do.datetime, "last", 1) + + self.assertEqual(dt_next, d_obj_next) + self.assertEqual(dt_last, d_obj_last) + + def test_move_second(self): + dt_next = datetime(2013, 1, 3, 4, 31, 15, 148540, tzinfo=delorean.utc) + dt_next_2 = datetime(2013, 1, 3, 4, 31, 16, 148540, tzinfo=delorean.utc) + dt_last = datetime(2013, 1, 3, 4, 31, 13, 148540, tzinfo=delorean.utc) + dt_last_2 = datetime(2013, 1, 3, 4, 31, 12, 148540, tzinfo=delorean.utc) + + d_obj_next = self.do.next_second() + d_obj_next_2 = self.do.next_second(2) + d_obj_last = self.do.last_second() + d_obj_last_2 = self.do.last_second(2) + + self.assertEqual(dt_next, d_obj_next.datetime) + self.assertEqual(dt_last, d_obj_last.datetime) + self.assertEqual(dt_next_2, d_obj_next_2.datetime) + self.assertEqual(dt_last_2, d_obj_last_2.datetime) + + def test_move_fractional_seconds(self): + expected_datetimes = { + ("next_second", 0.5): datetime( + 2013, 1, 3, 4, 31, 14, 648540, tzinfo=delorean.utc + ), + ("next_second", 1.5): datetime( + 2013, 1, 3, 4, 31, 15, 648540, tzinfo=delorean.utc + ), + ("last_second", 0.5): datetime( + 2013, 1, 3, 4, 31, 13, 648540, tzinfo=delorean.utc + ), + ("last_second", 1.5): datetime( + 2013, 1, 3, 4, 31, 12, 648540, tzinfo=delorean.utc + ), + } + + for (method_name, seconds), expected in expected_datetimes.items(): + with self.subTest(method_name=method_name, seconds=seconds): + shifted = getattr(self.do, method_name)(seconds) + self.assertEqual(expected, shifted.datetime) + + def test_fractional_shifts_are_rejected_for_non_second_units(self): + non_second_units = set(self.do._VALID_SHIFT_UNITS) - {"second"} + + for direction in self.do._VALID_SHIFT_DIRECTIONS: + for unit in non_second_units: + with self.subTest(direction=direction, unit=unit): + shift = getattr(self.do, f"{direction}_{unit}") + with self.assertRaisesRegex(TypeError, "integer"): + shift(0.5) + + def test_move_second_function(self): + dt_next = datetime(2013, 1, 3, 4, 31, 15, 148540, tzinfo=delorean.utc) + dt_last = datetime(2013, 1, 3, 4, 31, 13, 148540, tzinfo=delorean.utc) + + d_obj_next = delorean.move_datetime_second(self.do.datetime, "next", 1) + d_obj_last = delorean.move_datetime_second(self.do.datetime, "last", 1) + + self.assertEqual(dt_next, d_obj_next) + self.assertEqual(dt_last, d_obj_last) + + def test_range_count(self): + """ + tests the range method with count used + """ + count = list(delorean.stops(delorean.DAILY, count=5)) + self.assertEqual(len(count), 5) + + def test_range_with_start(self): + dates1 = [] + for do in delorean.stops(delorean.DAILY, count=5, start=naive_utcnow()): + do.truncate("minute") + dates1.append(do) + do = delorean.Delorean().truncate("minute") + dates2 = [] + for x in range(5): + dates2.append(do.next_day(x)) + self.assertEqual(dates1, dates2) + + def test_range_with_start_and_stop(self): + dates1 = [] + tomorrow = naive_utcnow() + timedelta(days=1) + for do in delorean.stops(delorean.DAILY, start=naive_utcnow(), stop=tomorrow): + do.truncate("minute") + dates1.append(do) + do = delorean.Delorean().truncate("minute") + dates2 = [] + for x in range(2): + dates2.append(do.next_day(x)) + self.assertEqual(dates1, dates2) + + def test_range_with_interval(self): + dates1 = [] + for do in delorean.stops( + delorean.DAILY, interval=2, count=3, start=naive_utcnow() + ): + do.truncate("minute") + dates1.append(do) + do = delorean.Delorean().truncate("minute") + dates2 = [] + for x in range(6): + if (x % 2) == 0: + dates2.append(do.next_day(x)) + self.assertEqual(dates1, dates2) + + def test_delorean_with_datetime(self): + dt = naive_utcnow() + d = delorean.Delorean(datetime=dt, timezone=UTC) + dt = dt.replace(tzinfo=timezone.utc) + self.assertEqual(dt, d._dt) + self.assertEqual(delorean.utc, d.timezone) + + def test_delorean_with_timezone(self): + dt = naive_utcnow() + d = delorean.Delorean(datetime=dt, timezone=UTC) + d = d.shift("US/Eastern") + dt = dt.replace(tzinfo=timezone.utc).astimezone(est) + dt = dt.astimezone(est) + self.assertEqual(dt, d.datetime) + self.assertEqual(est, d.timezone) + + def test_delorean_with_only_timezone(self): + dt = naive_utcnow() + dt = dt.replace(tzinfo=timezone.utc) + dt = dt.astimezone(est) + dt = dt.replace(second=0, microsecond=0) + d = delorean.Delorean(timezone="US/Eastern") + d.truncate("minute") + self.assertEqual(est, d.timezone) + self.assertEqual(dt, d._dt) + + def test_shift_failure(self): + self.assertRaises( + delorean.DeloreanInvalidTimezone, self.do.shift, "US/Westerrn" + ) + + def test_invalid_shift_unit_raises(self): + self.assertRaises(AttributeError, getattr, self.do, "next_bogus") + + def test_invalid_shift_direction_raises(self): + self.assertRaises(AttributeError, getattr, self.do, "sideways_day") + + def test_invalid_shift_name_is_not_reported_by_hasattr(self): + self.assertFalse(hasattr(self.do, "next_bogus")) + + def test_shift_across_dst_keeps_wall_clock(self): + eastern = ZoneInfo("US/Eastern") + do = delorean.Delorean(datetime(2013, 3, 9, 7, 0).replace(tzinfo=eastern)) + + shifted = do.next_day().datetime + self.assertEqual(shifted.strftime("%Y-%m-%d %H:%M"), "2013-03-10 07:00") + self.assertEqual(shifted.tzname(), "EDT") + + def test_shift_into_nonexistent_local_time_resolves_to_standard(self): + # Eastern clocks jump 02:00 EST -> 03:00 EDT on 2013-03-10, so 02:30 + # has no local reading at all that day. + eastern = ZoneInfo("US/Eastern") + do = delorean.Delorean(datetime(2013, 3, 9, 2, 30).replace(tzinfo=eastern)) + + shifted = do.next_day().datetime + self.assertEqual(shifted.strftime("%Y-%m-%d %H:%M"), "2013-03-10 02:30") + self.assertEqual(shifted.tzname(), "EST") + self.assertEqual(shifted.astimezone(delorean.utc).strftime("%H:%M"), "07:30") + + def test_shift_into_ambiguous_local_time_takes_the_first_occurrence(self): + # 01:30 happens twice on 2013-11-03, first as EDT and then as EST. + eastern = ZoneInfo("US/Eastern") + do = delorean.Delorean(datetime(2013, 11, 2, 1, 30).replace(tzinfo=eastern)) + + shifted = do.next_day().datetime + self.assertEqual(shifted.strftime("%Y-%m-%d %H:%M"), "2013-11-03 01:30") + self.assertEqual(shifted.tzname(), "EDT") + + def test_localize_nonexistent_local_time_resolves_to_standard(self): + dt = delorean.localize(datetime(2013, 3, 10, 2, 30), "US/Eastern") + self.assertEqual(dt.tzname(), "EST") + + def test_localize_ambiguous_local_time_takes_the_first_occurrence(self): + dt = delorean.localize(datetime(2013, 11, 3, 1, 30), "US/Eastern") + self.assertEqual(dt.tzname(), "EDT") + + def test_localize_with_zoneinfo_timezone(self): + tz = ZoneInfo("US/Eastern") + dt = delorean.localize(datetime(2013, 1, 1, 12), tz) + + self.assertEqual(dt, datetime(2013, 1, 1, 12, tzinfo=tz)) + + def test_normalize_with_zoneinfo_timezone(self): + tz = ZoneInfo("US/Eastern") + dt = delorean.normalize( + datetime(2013, 1, 1, 12).replace(tzinfo=timezone.utc), tz + ) + + self.assertEqual(dt, datetime(2013, 1, 1, 7, tzinfo=tz)) + + def test_now_with_zoneinfo_local_timezone(self): + # tzlocal 5.x hands back a zoneinfo zone rather than a pytz one. + tz = ZoneInfo("America/Toronto") + with mock.patch("delorean.interface.get_localzone", return_value=tz): + do = delorean.now() + + self.assertEqual(do.timezone, tz) + + def test_datetime_localization(self): + dt1 = self.do.datetime + dt2 = delorean.Delorean(dt1).datetime + self.assertEqual(dt1, dt2) + + def test_localize_datetime(self): + dt = naive_utcnow() + tz = ZoneInfo("US/Pacific") + dt = dt.replace(tzinfo=tz) + d = delorean.Delorean(dt) + d2 = d.shift("US/Pacific") + + self.assertEqual(str(d.timezone), "US/Pacific") + self.assertEqual(d.datetime, dt) + self.assertEqual(d.datetime, d2.datetime) + + def test_lt(self): + dt1 = self.do + dt2 = delorean.Delorean() + self.assertTrue(dt1 < dt2) + + def test_gt(self): + dt1 = self.do + dt2 = delorean.Delorean() + self.assertTrue(dt2 > dt1) + + def test_ge(self): + dt = naive_utcnow() + dt1 = delorean.Delorean(dt, timezone="UTC") + dt2 = delorean.Delorean(dt, timezone="UTC") + dt3 = self.do + self.assertTrue(dt2 >= dt1) + self.assertTrue(dt1 >= dt3) + + def test_le(self): + dt = naive_utcnow() + dt1 = delorean.Delorean(dt, timezone="UTC") + dt2 = delorean.Delorean(dt, timezone="UTC") + dt3 = self.do + self.assertTrue(dt2 <= dt1) + self.assertTrue(dt3 <= dt2) + + def test_timestamp(self): + self.assertEqual(self.do.timestamp, 1357187474.148540) + + def test_timestamp_matches_stdlib(self): + self.assertEqual(self.do.timestamp, self.do.datetime.timestamp()) + + def test_from_timestamp(self): + do = delorean.from_timestamp(1357187474.148540) + self.assertEqual(self.do, do) + + def test_from_timestamp_round_trips(self): + self.assertEqual(delorean.from_timestamp(self.do.timestamp), self.do) + + def test_epoch_is_alias_of_timestamp(self): + self.assertEqual(self.do.epoch, self.do.timestamp) + + def test_epoch_creation_is_alias_of_from_timestamp(self): + self.assertEqual( + delorean.epoch(1357187474.148540), + delorean.from_timestamp(1357187474.148540), + ) + + def test_not_equal(self): + d = delorean.Delorean() + self.assertNotEqual(d, None) + + def test_equal(self): + d1 = delorean.Delorean() + d2 = deepcopy(d1) + self.assertEqual(d1, d2) + self.assertFalse(d1 != d2, "Overloaded __ne__ is not correct") + + d1 = delorean.Delorean(datetime(2015, 1, 1), timezone="US/Pacific") + d2 = delorean.Delorean(datetime(2015, 1, 1, 8), timezone="UTC") + self.assertEqual(d1, d2) + + def test_repr_string_timezone(self): + import datetime + + from delorean import Delorean + + d1 = Delorean(datetime.datetime(2015, 1, 1), timezone="US/Pacific") + d2 = eval(repr(d1)) + + self.assertEqual(d1, d2) + + d3 = Delorean(d1.datetime, timezone="UTC") + d4 = eval(repr(d3)) + + self.assertEqual(d1, d4) + + def test_repr_named_timezone(self): + import datetime + + from delorean import Delorean + + d1 = Delorean(datetime.datetime(2015, 1, 1), timezone="US/Pacific") + d2 = eval(repr(d1)) + + self.assertEqual(d1, d2) + self.assertEqual(d1.datetime, d2.datetime) + self.assertEqual(d1.timezone, d2.timezone) + + def test_repr_fixed_offset_timezone(self): + import datetime + + # Resolved by the eval(repr(...)) below, so static analysis sees no use. + from delorean import Delorean # noqa: F401 + + tz = ZoneInfo("US/Pacific") + dt = datetime.datetime(2015, 1, 1).replace(tzinfo=tz) + dt_str = dt.strftime("%Y-%m-%d %H:%M:%S %z") + + d1 = delorean.parse(dt_str) + d2 = eval(repr(d1)) + + self.assertEqual(d1, d2) + self.assertEqual(d1.datetime, d2.datetime) + self.assertEqual(d1.timezone, d2.timezone) + + def test_repr_zoneinfo_timezone(self): + d = delorean.Delorean( + datetime(2015, 1, 1), timezone=ZoneInfo("America/Toronto") + ) + + self.assertEqual( + repr(d), + "Delorean(datetime=datetime.datetime(2015, 1, 1, 0, 0), " + "timezone='America/Toronto')", + ) + + def test_timezone_delorean_to_datetime_to_delorean_utc(self): + d1 = delorean.Delorean() + d2 = delorean.Delorean(d1.datetime) + + # these deloreans should be the same + self.assertEqual(d1.next_day(1), d2.next_day(1)) + self.assertEqual(d2.last_week(), d2.last_week()) + self.assertEqual(d1.timezone, d2.timezone) + self.assertEqual(d1, d2) + + def test_timezone_delorean_to_datetime_to_delorean_non_utc(self): + """Test if when you create Delorean object from Delorean's datetime + it still behaves the same + """ + d1 = delorean.Delorean(timezone="America/Chicago") + d2 = delorean.Delorean(d1.datetime) + + # these deloreans should be the same + self.assertEqual(d1.next_day(1), d2.next_day(1)) + self.assertEqual(d2.last_week(), d2.last_week()) + self.assertEqual(d1.timezone, d2.timezone) + self.assertEqual(d1, d2) + + def test_stops_bymonth(self): + """Test if create stops, checks bymonth, bymonthday, count + and start parameters work properly + """ + days = list( + delorean.interface.stops( + delorean.MONTHLY, + bymonth=(1, 4, 7, 10), + bymonthday=15, + count=4, + start=datetime(datetime.now().year, 1, 1), + ) + ) + year = datetime.now().year + day = 15 + dt1 = datetime(year, 1, day) + dt4 = datetime(year, 4, day) + dt7 = datetime(year, 7, day) + dt10 = datetime(year, 10, day) + + self.assertTrue(len(days) == 4) + dl1 = delorean.Delorean(datetime=dt1, timezone="UTC") + self.assertEqual(days[0], dl1) + + dl4 = delorean.Delorean(datetime=dt4, timezone="UTC") + self.assertEqual(days[1], dl4) + + dl7 = delorean.Delorean(datetime=dt7, timezone="UTC") + self.assertEqual(days[2], dl7) + + dl10 = delorean.Delorean(datetime=dt10, timezone="UTC") + self.assertEqual(days[3], dl10) + + def test_timedelta_arithmetic(self): + hour = timedelta(hours=1) + d = delorean.parse("2014/06/02 10:00:00 -0700") + hour_before = delorean.parse("2014/06/02 09:00:00 -0700") + hour_after = delorean.parse("2014/06/02 11:00:00 -0700") + self.assertEqual(d + hour, hour_after) + self.assertEqual(d - hour, hour_before) + self.assertEqual(hour_after - d, hour) + + @mock.patch("delorean.Delorean.now") + def test_humanize_past(self, mock_now): + mock_now.return_value = delorean.Delorean( + datetime(2015, 1, 2), timezone="US/Pacific" + ) + + do = delorean.Delorean(datetime(2015, 1, 1), timezone="US/Pacific") + self.assertEqual(do.humanize(), "a day ago") + + @mock.patch("delorean.Delorean.now") + def test_humanize_future(self, mock_now): + mock_now.return_value = delorean.Delorean( + datetime(2014, 12, 31), timezone="US/Pacific" + ) + + do = delorean.Delorean(datetime(2015, 1, 1), timezone="US/Pacific") + self.assertEqual(do.humanize(), "a day from now") + + def test_replace(self): + do = delorean.Delorean(datetime(2015, 1, 1), timezone="US/Pacific") + dt = do.datetime + + # Check if parts are okay + self.assertEqual(do.replace(hour=8).datetime.hour, 8) + self.assertEqual(do.replace(minute=23).datetime.minute, 23) + self.assertEqual(do.replace(second=45).datetime.second, 45) + + # Asserts on datetimes + self.assertEqual(do.replace(hour=8).datetime, dt.replace(hour=8)) + self.assertEqual(do.replace(minute=38).datetime, dt.replace(minute=38)) + self.assertEqual(do.replace(second=48).datetime, dt.replace(second=48)) + + # Test that the timezone does not change + self.assertEqual(do.replace(second=45).timezone, do.timezone) + + # Test the datetime + eight_hours_late = dt + timedelta(hours=8) + self.assertEqual(do.replace(hour=8).datetime, eight_hours_late) + + +class StdlibTimezoneTests(unittest.TestCase): + """The 2.0 timezone surface: standard library types, no pytz anywhere.""" + + def test_utc_constant_is_exported(self): + self.assertEqual(delorean.utc, ZoneInfo("UTC")) + + def test_timezone_helper_returns_a_named_zone(self): + self.assertEqual(delorean.timezone("US/Eastern"), ZoneInfo("US/Eastern")) + + def test_timezone_helper_understands_an_offset_string(self): + self.assertEqual( + delorean.timezone("UTC-08:00").utcoffset(None), timedelta(hours=-8) + ) + + def test_timezone_helper_understands_a_positive_offset_string(self): + self.assertEqual( + delorean.timezone("UTC+05:30").utcoffset(None), + timedelta(hours=5, minutes=30), + ) + + def test_timezone_helper_passes_a_tzinfo_through(self): + tz = ZoneInfo("US/Eastern") + + self.assertIs(delorean.timezone(tz), tz) + + def test_timezone_helper_rejects_an_unknown_name(self): + with self.assertRaises(delorean.DeloreanInvalidTimezone): + delorean.timezone("Not/AZone") + + def test_error_base_class_is_exported(self): + self.assertTrue(issubclass(delorean.DeloreanError, ValueError)) + + def test_named_zone_is_a_zoneinfo(self): + do = delorean.Delorean(datetime(2013, 1, 15, 12), timezone="US/Eastern") + + self.assertIsInstance(do.timezone, ZoneInfo) + + def test_parsed_offset_is_a_stdlib_timezone(self): + do = delorean.parse("2015-01-01 00:01:02 -0800") + + self.assertIsInstance(do.timezone, timezone) + + def test_no_third_party_timezone_types_escape(self): + values = [ + delorean.Delorean(datetime(2013, 1, 15, 12), timezone="US/Eastern"), + delorean.parse("2015-01-01 00:01:02 -0800"), + delorean.parse("2013-09-30T15:34:00.000Z"), + delorean.utcnow(), + ] + + for do in values: + with self.subTest(value=repr(do)): + self.assertIn(type(do.timezone).__module__, ("zoneinfo", "datetime")) + + def test_constructor_accepts_an_offset_string(self): + do = delorean.Delorean(datetime(2015, 1, 1), timezone="UTC-08:00") + + self.assertEqual(do.datetime.utcoffset(), timedelta(hours=-8)) + + def test_repr_prints_an_offset_as_a_string(self): + do = delorean.parse("2015-01-01 00:01:02 -0800") + + self.assertIn("timezone='UTC-08:00'", repr(do)) + + def test_repr_prints_a_named_zone_as_its_key(self): + do = delorean.Delorean(datetime(2013, 1, 15, 12), timezone="US/Eastern") + + self.assertIn("timezone='US/Eastern'", repr(do)) + + def test_unknown_zone_in_constructor_raises_delorean_error(self): + with self.assertRaises(delorean.DeloreanInvalidTimezone): + delorean.Delorean(datetime(2013, 1, 15, 12), timezone="Not/AZone") + + def test_unknown_assumed_zone_in_parse_raises_delorean_error(self): + with self.assertRaises(delorean.DeloreanInvalidTimezone): + delorean.parse("2015-02-04T16:33:21", assume_timezone="Not/AZone") + + def test_ambiguous_local_time_takes_the_first_occurrence(self): + # zoneinfo's fold=0 default: 01:30 resolves to the daylight side. + do = delorean.Delorean(datetime(2013, 11, 3, 1, 30), timezone="US/Eastern") + + self.assertEqual(do.datetime.tzname(), "EDT") + self.assertEqual( + do.datetime.astimezone(timezone.utc), + datetime(2013, 11, 3, 5, 30, tzinfo=timezone.utc), + ) + + def test_nonexistent_local_time_still_takes_standard_time(self): + do = delorean.Delorean(datetime(2013, 3, 10, 2, 30), timezone="US/Eastern") + + self.assertEqual(do.datetime.tzname(), "EST") + self.assertEqual( + do.datetime.astimezone(timezone.utc), + datetime(2013, 3, 10, 7, 30, tzinfo=timezone.utc), + ) + + +if __name__ == "__main__": + unittest.main()