diff --git a/.gitignore b/.gitignore index d022b15ff..138aa1f78 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ doc/source/configspec.rst .mypy_cache/ .env .venv +.venv* env/ venv/ .hypothesis/ diff --git a/khal/cli.py b/khal/cli.py index 51ad78b92..e4704906e 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -400,13 +400,22 @@ def printcalendars(ctx, include_calendar, exclude_calendar): @cli.command() @click.pass_context -def printformats(ctx): +@click.option( + '--now', + help=('Print the current date and time in the local timezone instead.'), + is_flag=True, +) +def printformats(ctx, now: bool): '''Print a date in all formats. Print the date 2013-12-21 21:45 in all configured date(time) formats to check if these locale settings are configured to ones liking.''' time = dt.datetime(2013, 12, 21, 21, 45) + if now: + import pytz + time = dt.datetime.utcnow() + time = pytz.UTC.localize(time).astimezone(ctx.obj['conf']['locale']['local_timezone']) try: for strftime_format in [ 'longdatetimeformat', 'datetimeformat', 'longdateformat', diff --git a/khal/controllers.py b/khal/controllers.py index abe36e9d7..fb5004404 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -27,7 +27,7 @@ import textwrap from collections import OrderedDict, defaultdict from shutil import get_terminal_size -from typing import Callable, List, Optional +from typing import Callable, List, Optional, Tuple import pytz from click import confirm, echo, prompt, style @@ -62,7 +62,7 @@ def format_day(day: dt.date, format_string: str, locale, attributes=None): attributes["date"] = day.strftime(locale['dateformat']) attributes["date-long"] = day.strftime(locale['longdateformat']) - attributes["name"] = parse_datetime.construct_daynames(day) + attributes["name"] = parse_datetime.construct_daynames(day, timezone=locale['local_timezone']) colors = {"reset": style("", reset=True), "bold": style("", bold=True, reset=False)} for c in ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]: @@ -148,7 +148,7 @@ def start_end_from_daterange( locale: LocaleConfiguration, default_timedelta_date: dt.timedelta=dt.timedelta(days=1), default_timedelta_datetime: dt.timedelta=dt.timedelta(hours=1), -): +) -> Tuple[dt.datetime, dt.datetime]: """ convert a string description of a daterange into start and end datetime @@ -158,7 +158,8 @@ def start_end_from_daterange( :param locale: locale settings """ if not daterange: - start = dt.datetime(*dt.date.today().timetuple()[:3]) + today = dt.datetime.now(locale['local_timezone']).date() + start = dt.datetime.combine(today, dt.time.min) end = start + default_timedelta_date else: start, end, allday = parse_datetime.guessrangefstr( @@ -203,8 +204,11 @@ def get_events_between( env = {} assert start assert end - start_local = locale['local_timezone'].localize(start) - end_local = locale['local_timezone'].localize(end) + assert start.tzinfo is not None + assert end.tzinfo is not None + + start_local = start + end_local = end start = start_local.replace(tzinfo=None) end = end_local.replace(tzinfo=None) @@ -272,6 +276,8 @@ def khal_list( default_timedelta_datetime=conf['default']['timedelta'], ) logger.debug(f'Getting all events between {start} and {end}') + assert start.tzinfo is not None + assert end.tzinfo is not None elif datepoint is not None: if not datepoint: @@ -294,18 +300,23 @@ def khal_list( bold=True, ) logger.debug(f'Getting all events between {start} and {end}') + assert start.tzinfo is not None + assert end.tzinfo is not None + else: + raise ValueError('Something has gone wrong') event_column: List[str] = [] once = set() if once else None if env is None: env = {} - original_start = conf['locale']['local_timezone'].localize(start) + original_start = start while start < end: if start.date() == end.date(): day_end = end else: day_end = dt.datetime.combine(start.date(), dt.time.max) + day_end = day_end.replace(tzinfo=start.tzinfo) current_events = get_events_between( collection, locale=conf['locale'], formatter=formatter, start=start, end=day_end, notstarted=notstarted, original_start=original_start, @@ -319,6 +330,7 @@ def khal_list( event_column.append(format_day(start.date(), day_format, conf['locale'])) event_column.extend(current_events) start = dt.datetime(*start.date().timetuple()[:3]) + dt.timedelta(days=1) + start = start.replace(tzinfo=original_start.tzinfo) return event_column @@ -555,7 +567,7 @@ def edit_event(event, collection, locale, allow_quit=False, width=80): until = prompt('until (or "None")', until) if until == 'None': until = None - rrule = parse_datetime.rrulefstr(freq, until, locale, event.start.tzinfo) + rrule = parse_datetime.rrulefstr(freq, until, locale) event.update_rrule(rrule) edited = True elif choice == "alarm": diff --git a/khal/icalendar.py b/khal/icalendar.py index 39e6eda8f..e543330c4 100644 --- a/khal/icalendar.py +++ b/khal/icalendar.py @@ -116,8 +116,8 @@ def new_vevent(locale, if not allday and timezone is not None: assert isinstance(dtstart, dt.datetime) assert isinstance(dtend, dt.datetime) - dtstart = timezone.localize(dtstart) - dtend = timezone.localize(dtend) + assert dtstart.tzinfo is not None + assert dtend.tzinfo is not None event = icalendar.Event() event.add('dtstart', dtstart) @@ -136,7 +136,7 @@ def new_vevent(locale, if url: event.add('url', icalendar.vUri(url)) if repeat and repeat != "none": - rrule = rrulefstr(repeat, until, locale, getattr(dtstart, 'tzinfo', None)) + rrule = rrulefstr(repeat, until, locale) event.add('rrule', rrule) if alarms: for alarm in str2alarm(alarms, description or ''): diff --git a/khal/parse_datetime.py b/khal/parse_datetime.py index c7d92f2ff..cd7a117b0 100644 --- a/khal/parse_datetime.py +++ b/khal/parse_datetime.py @@ -38,7 +38,7 @@ logger = logging.getLogger('khal') -def timefstr(dtime_list: List[str], timeformat: str) -> dt.datetime: +def timefstr(dtime_list: List[str], timeformat: str, timezone: dt.tzinfo) -> dt.datetime: """converts the first item of a list (a time as a string) to a datetimeobject where the date is today and the time is given by a string @@ -48,8 +48,8 @@ def timefstr(dtime_list: List[str], timeformat: str) -> dt.datetime: raise ValueError() datetime_start = dt.datetime.strptime(dtime_list[0], timeformat) time_start = dt.time(*datetime_start.timetuple()[3:5]) - day_start = dt.date.today() - dtstart = dt.datetime.combine(day_start, time_start) + today = dt.datetime.now(timezone).date() + dtstart = dt.datetime.combine(today, time_start).replace(tzinfo=timezone) dtime_list.pop(0) return dtstart @@ -60,10 +60,11 @@ def datetimefstr( default_day: Optional[dt.date]=None, infer_year: bool=True, in_future: bool=True, + timezone: dt.tzinfo=pytz.UTC, ) -> dt.datetime: """converts a datetime (as one or several string elements of a list) to a datetimeobject, if infer_year is True, use the `default_day`'s year as - the year of the return datetimeobject, + the year of the returned datetimeobject, removes "used" elements of list @@ -73,7 +74,7 @@ def datetimefstr( dateformat = '%d.%m. %H:%M' """ # if now() is called as default param, mocking with freezegun won't work - now = dt.datetime.now() + now = dt.datetime.now(timezone) if default_day is None: default_day = now.date() parts = dateformat.count(' ') + 1 @@ -92,13 +93,17 @@ def datetimefstr( if infer_year: dtstart = dt.datetime(*(default_day.timetuple()[:1] + dtstart_struct[1:5])) + dtstart = dtstart.astimezone(timezone) if in_future and dtstart < now: dtstart = dtstart.replace(year=dtstart.year + 1) if dtstart.date() < default_day: dtstart = dtstart.replace(year=default_day.year + 1) + assert dtstart.tzinfo is not None return dtstart else: - return dt.datetime(*dtstart_struct[:5]) + rdt = dt.datetime(*dtstart_struct[:5]).replace(tzinfo=timezone) + assert rdt.tzinfo is not None + return rdt def weekdaypstr(dayname: str) -> int: @@ -125,26 +130,28 @@ def weekdaypstr(dayname: str) -> int: raise ValueError('invalid weekday name `%s`' % dayname) -def construct_daynames(date_: dt.date) -> str: +def construct_daynames(date_: dt.date, timezone: dt.tzinfo) -> str: """converts datetime.date into a string description either `Today`, `Tomorrow` or name of weekday. """ - if date_ == dt.date.today(): + today = dt.datetime.now(timezone).date() + if date_ == today: return 'Today' - elif date_ == dt.date.today() + dt.timedelta(days=1): + elif date_ == today + dt.timedelta(days=1): return 'Tomorrow' else: return date_.strftime('%A') -def calc_day(dayname: str) -> dt.datetime: +def calc_day(dayname: str, timezone: dt.tzinfo) -> dt.datetime: """converts a relative date's description to a datetime object :param dayname: relative day name (like 'today' or 'monday') + :param timezone: timezone to use for the calculation :returns: date """ - today = dt.datetime.combine(dt.date.today(), dt.time.min) + today = dt.datetime.combine(dt.date.today(), dt.time.min).replace(tzinfo=timezone) dayname = dayname.lower() if dayname == 'today': return today @@ -160,7 +167,7 @@ def calc_day(dayname: str) -> dt.datetime: return day -def datefstr_weekday(dtime_list: List[str], timeformat: str, infer_year: bool) -> dt.datetime: +def datefstr_weekday(dtime_list: List[str], timeformat: str, infer_year: bool, timezone: dt.tzinfo) -> dt.datetime: """interprets first element of a list as a relative date and removes that element @@ -171,22 +178,22 @@ def datefstr_weekday(dtime_list: List[str], timeformat: str, infer_year: bool) - """ if len(dtime_list) == 0: raise ValueError() - day = calc_day(dtime_list[0]) + day = calc_day(dtime_list[0], timezone=timezone) dtime_list.pop(0) return day -def datetimefstr_weekday(dtime_list: List[str], timeformat: str, infer_year: bool) -> dt.datetime: +def datetimefstr_weekday(dtime_list: List[str], timeformat: str, infer_year: bool, timezone: dt.tzinfo) -> dt.datetime: """ :param infer_year: only here for compat reasons (having the same function signature) """ if len(dtime_list) == 0: raise ValueError() - day = calc_day(dtime_list[0]) - this_time = timefstr(dtime_list[1:], timeformat) + day = calc_day(dtime_list[0], timezone=timezone) + this_time = timefstr(dtime_list[1:], timeformat, timezone) dtime_list.pop(0) - dtime_list.pop(0) # we need to pop twice as timefstr gets a copy - dtime = dt.datetime.combine(day, this_time.time()) + dtime_list.pop(0) # we need to pop twice as timefstr gets a copy and does't remove a used element + dtime = dt.datetime.combine(day, this_time.time()).replace(tzinfo=timezone) return dtime @@ -199,27 +206,33 @@ def guessdatetimefstr( """ :param in_future: if set, shortdate(time) events will be set in the future """ + orig = list(dtime_list) # TODO remove this line -- only for debugging # if now() is called as default param, mocking with freezegun won't work - day = default_day or dt.datetime.now().date() + day = default_day or dt.datetime.now(locale['local_timezone']).date() # TODO rename in guessdatetimefstrLIST or something saner altogether - def timefstr_day(dtime_list: List[str], timeformat: str, infer_year: bool) -> dt.datetime: + def timefstr_day(dtime_list: List[str], timeformat: str, infer_year: bool, timezone: dt.tzinfo) -> dt.datetime: if locale['timeformat'] == '%H:%M' and dtime_list[0] == '24:00': - a_date = dt.datetime.combine(day, dt.time(0)) + a_date = dt.datetime.combine(day, dt.time(0)).replace(tzinfo=timezone) dtime_list.pop(0) else: - a_date = timefstr(dtime_list, timeformat) - a_date = dt.datetime(*(day.timetuple()[:3] + a_date.timetuple()[3:5])) + a_date = timefstr(dtime_list, timeformat, timezone=timezone) + a_date = dt.datetime(*(day.timetuple()[:3] + a_date.timetuple()[3:5])).replace(tzinfo=timezone) + assert a_date.tzinfo is not None return a_date - def datetimefwords(dtime_list: List[str], _: str, infer_year: bool) -> dt.datetime: + def datetimefwords(dtime_list: List[str], _: str, infer_year: bool, timezone: dt.tzinfo) -> dt.datetime: + """converts words to datetimes + + for now, this only knows "now" + """ if len(dtime_list) > 0 and dtime_list[0].lower() == 'now': dtime_list.pop(0) - return dt.datetime.now() + return dt.datetime.now(timezone) raise ValueError - def datefstr_year(dtime_list: List[str], dtformat: str, infer_year: bool) -> dt.datetime: - return datetimefstr(dtime_list, dtformat, day, infer_year, in_future) + def datefstr_year(dtime_list: List[str], dtformat: str, infer_year: bool, timezone: dt.tzinfo) -> dt.datetime: + return datetimefstr(dtime_list, dtformat, day, infer_year, in_future, timezone) dtstart = None fun: Callable[[List[str], str, bool], dt.datetime] @@ -240,10 +253,12 @@ def datefstr_year(dtime_list: List[str], dtformat: str, infer_year: bool) -> dt. if infer_year and '97' in dt.datetime(1997, 10, 11).strftime(dtformat): infer_year = False try: - dtstart = fun(dtime_list, dtformat, infer_year=infer_year) + timezone = locale['local_timezone'] + dtstart = fun(dtime_list, dtformat, infer_year=infer_year, timezone=timezone) except (ValueError, DateTimeParseError): pass else: + assert dtstart.tzinfo is not None return dtstart, all_day raise DateTimeParseError( f"Could not parse \"{dtime_list}\".\nPlease check your configuration " @@ -340,9 +355,13 @@ def guessrangefstr(daterange: Union[str, List[str]], range_list = daterange.split(' ') assert isinstance(range_list, list) + orig = list(range_list) # TODO remove this line -- only for debugging + if range_list == ['week']: today_weekday = dt.datetime.today().weekday() - startdt = dt.datetime.today() - dt.timedelta(days=(today_weekday - locale['firstweekday'])) + today = dt.datetime.now(locale['local_timezone']).date() + today = dt.datetime.combine(today, dt.time.min).replace(tzinfo=locale['local_timezone']) + startdt = today - dt.timedelta(days=(today_weekday - locale['firstweekday'])) enddt = startdt + dt.timedelta(days=8) return startdt, enddt, True @@ -364,7 +383,7 @@ def guessrangefstr(daterange: Union[str, List[str]], else: end = start + default_timedelta_datetime elif endstr.lower() == 'eod': - end = dt.datetime.combine(start.date(), dt.time.max) + end = dt.datetime.combine(start.date(), dt.time.max).replace(tzinfo=locale['local_timezone']) elif endstr.lower() == 'week': start -= dt.timedelta(days=(start.weekday() - locale['firstweekday'])) end = start + dt.timedelta(days=8) @@ -396,7 +415,7 @@ def guessrangefstr(daterange: Union[str, List[str]], if adjust_reasonably: if allday: # test if end's year is this year, but start's year is not - today = dt.datetime.today() + today = dt.datetime.now(locale['default_timezone']).date() if end.year == today.year and start.year != today.year: end = dt.datetime(start.year, *end.timetuple()[1:6]) @@ -404,9 +423,14 @@ def guessrangefstr(daterange: Union[str, List[str]], end = dt.datetime(end.year + 1, *end.timetuple()[1:6]) if end < start: - end = dt.datetime(*start.timetuple()[0:3] + end.timetuple()[3:5]) + # if end is before start date we are using the year, month and day of start + # and the time of end to create a new end date + end = dt.datetime(*start.timetuple()[0:3] + end.timetuple()[3:5]).replace(tzinfo=locale['local_timezone']) if end < start: + # if end is still before start date we are adding a day end = end + dt.timedelta(days=1) + assert start.tzinfo is not None + assert end.tzinfo is not None return start, end, allday except (ValueError, DateTimeParseError): pass @@ -422,18 +446,13 @@ def guessrangefstr(daterange: Union[str, List[str]], def rrulefstr(repeat: str, until: str, locale: LocaleConfiguration, - timezone: Optional[dt.tzinfo], ) -> RRuleMapType: if repeat in ["daily", "weekly", "monthly", "yearly"]: rrule_settings: RRuleMapType = {'freq': repeat} if until: until_dt, _ = guessdatetimefstr(until.split(' '), locale) - if timezone: - rrule_settings['until'] = until_dt.\ - replace(tzinfo=timezone).\ - astimezone(pytz.UTC) - else: - rrule_settings['until'] = until_dt + assert until_dt.tzinfo is not None + rrule_settings['until'] = until_dt return rrule_settings else: logger.fatal("Invalid value for the repeat option. \ diff --git a/khal/ui/__init__.py b/khal/ui/__init__.py index f3ea06de0..cbf88dac5 100644 --- a/khal/ui/__init__.py +++ b/khal/ui/__init__.py @@ -130,11 +130,12 @@ def relative_day(self, day: dt.date, dtformat: str) -> str: weekday = day.strftime('%A') daystr = day.strftime(dtformat) - if day == dt.date.today(): + today = dt.datetime.now(self._conf['locale']['local_timezone']).date() + if day == today: return f'Today ({weekday}, {daystr})' - elif day == dt.date.today() + dt.timedelta(days=1): + elif day == today + dt.timedelta(days=1): return f'Tomorrow ({weekday}, {daystr})' - elif day == dt.date.today() - dt.timedelta(days=1): + elif day == today - dt.timedelta(days=1): return f'Yesterday ({weekday}, {daystr})' approx_delta = utils.relative_timedelta_str(day) diff --git a/tests/cli_test.py b/tests/cli_test.py index 5f90dbaaf..269164d82 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -1018,3 +1018,71 @@ def test_list_now(runner, tmpdir): result = runner.invoke(main_khal, ['list', 'now']) assert not result.exception + + +@freeze_time('2019-01-22 06:30:00', tz_offset=0) +def test_reproduce_836(runner, tmpdir): + # while it is already 2019-01-22 in UTC, it is still 2019-01-21 in + # America/Los_Angeles + import datetime as dt + print(dt.datetime.now()) + import pytz + local_tz = pytz.timezone('America/Los_Angeles') + print(dt.datetime.now(local_tz)) + runner = runner() + + + xdg_config_home = tmpdir.join('.config') + config_file = xdg_config_home.join('khal').join('config') + config_file.write(""" + [calendars] + [[one]] + path = {} + color = dark blue + [[two]] + path = {} + color = dark green + [[three]] + path = {} + [locale] + dateformat = %Y-%m-%d + longdateformat = %Y-%m-%d + datetimeformat = %Y-%m-%d %H:%M + longdatetimeformat = %Y-%m-%d %H:%M + timeformat = %H:%M + default_timezone = America/Los_Angeles + local_timezone = America/Los_Angeles + """.format( + tmpdir.join('calendar'), + tmpdir.join('calendar2'), + tmpdir.join('calendar3'), + )) + + print(runner.invoke(main_khal, 'printformats --now'.split()).output) + + result = runner.invoke(main_khal, 'new -a one 2019-01-20 23:00 24:00 Meeting on 20st at 23:00'.split()) + result = runner.invoke(main_khal, 'new -a one 2019-01-21 23:00 24:00 Meeting on 21st at 23:00'.split()) + result = runner.invoke(main_khal, 'new -a one 2019-01-22 17:00 20:00 Meeting on 22nd at 17:00'.split()) + result = runner.invoke(main_khal, 'new -a one 23:00 Meeting today at 23:00'.split()) + + print('$ khal calendar 2019-01-20') + result = runner.invoke(main_khal, ['list', '2019-01-20']) + print(result.output) + assert result.output.startswith('Sunday, 2019-01-20') + + print('$ khal calendar 2019-01-21') + result = runner.invoke(main_khal, ['list', '2019-01-21']) + print(result.output) + assert result.output.startswith('Today, 2019-01-21') + + print('$ khal calendar 2019-01-22') + result = runner.invoke(main_khal, ['list', '2019-01-22']) + print(result.output) + assert result.output.startswith('Tomorrow, 2019-01-22') + + # this part tests, if the default start (today) is also correctly set to the + # 21st + print('$ khal calendar') + result = runner.invoke(main_khal, ['list']) + print(result.output) + assert result.output.startswith('Today, 2019-01-21') diff --git a/tests/controller_test.py b/tests/controller_test.py index dfe52ff94..4b6dbc43d 100644 --- a/tests/controller_test.py +++ b/tests/controller_test.py @@ -143,40 +143,47 @@ def test_mix_datetime_types(self, coll_vdirs): utils.BERLIN.localize(dt.datetime(2015, 6, 2, 16, 0)) -def test_start_end(): - with freeze_time('2016-04-10'): - start = dt.datetime(2016, 4, 10, 0, 0) - end = dt.datetime(2016, 4, 11, 0, 0) - assert (start, end) == start_end_from_daterange(('today',), locale=utils.LOCALE_BERLIN) - - -def test_start_end_default_delta(): - with freeze_time('2016-04-10'): - start = dt.datetime(2016, 4, 10, 0, 0) - end = dt.datetime(2016, 4, 11, 0, 0) - assert (start, end) == start_end_from_daterange(('today',), utils.LOCALE_BERLIN) - - -def test_start_end_delta(): - with freeze_time('2016-04-10'): - start = dt.datetime(2016, 4, 10, 0, 0) - end = dt.datetime(2016, 4, 12, 0, 0) - assert (start, end) == start_end_from_daterange(('today', '2d'), utils.LOCALE_BERLIN) - - -def test_start_end_empty(): - with freeze_time('2016-04-10'): - start = dt.datetime(2016, 4, 10, 0, 0) - end = dt.datetime(2016, 4, 11, 0, 0) - assert (start, end) == start_end_from_daterange([], utils.LOCALE_BERLIN) - - -def test_start_end_empty_default(): - with freeze_time('2016-04-10'): - start = dt.datetime(2016, 4, 10, 0, 0) - end = dt.datetime(2016, 4, 13, 0, 0) - assert (start, end) == start_end_from_daterange( - [], utils.LOCALE_BERLIN, - default_timedelta_date=dt.timedelta(days=3), - default_timedelta_datetime=dt.timedelta(hours=1), - ) +class TestStartEndFromDaterange: + def test_start_end(self): + with freeze_time('2016-04-10'): + start = dt.datetime(2016, 4, 10, 0, 0) + end = dt.datetime(2016, 4, 11, 0, 0) + assert (start, end) == start_end_from_daterange(('today',), locale=utils.LOCALE_BERLIN) + + + def test_start_end_default_delta(self): + with freeze_time('2016-04-10'): + start = dt.datetime(2016, 4, 10, 0, 0) + end = dt.datetime(2016, 4, 11, 0, 0) + assert (start, end) == start_end_from_daterange(('today',), utils.LOCALE_BERLIN) + + + def test_start_end_delta(self): + with freeze_time('2016-04-10'): + start = dt.datetime(2016, 4, 10, 0, 0) + end = dt.datetime(2016, 4, 12, 0, 0) + assert (start, end) == start_end_from_daterange(('today', '2d'), utils.LOCALE_BERLIN) + + + def test_start_end_empty(self): + with freeze_time('2016-04-10'): + start = dt.datetime(2016, 4, 10, 0, 0) + end = dt.datetime(2016, 4, 11, 0, 0) + assert (start, end) == start_end_from_daterange([], utils.LOCALE_BERLIN) + + + def test_start_end_empty_default(self): + with freeze_time('2016-04-10'): + start = dt.datetime(2016, 4, 10, 0, 0) + end = dt.datetime(2016, 4, 13, 0, 0) + assert (start, end) == start_end_from_daterange( + [], utils.LOCALE_BERLIN, + default_timedelta_date=dt.timedelta(days=3), + default_timedelta_datetime=dt.timedelta(hours=1), + ) + + def test_start_end_berlin(self): + with freeze_time('2016-04-10 23:30'): + start = dt.datetime(2016, 4, 10, 0, 0) + end = dt.datetime(2016, 4, 11, 0, 0) + assert (start, end) == start_end_from_daterange(('today',), locale=utils.LOCALE_BERLIN) diff --git a/tests/parse_datetime_test.py b/tests/parse_datetime_test.py index 3eaa568c1..9044bd7f9 100644 --- a/tests/parse_datetime_test.py +++ b/tests/parse_datetime_test.py @@ -2,6 +2,7 @@ from collections import OrderedDict import pytest +import pytz from freezegun import freeze_time from khal.exceptions import DateTimeParseError, FatalError @@ -24,6 +25,8 @@ normalize_component, ) +BERLIN = pytz.timezone('Europe/Berlin') + def _create_testcases(*cases): return [(userinput, ('\r\n'.join(output) + '\r\n').encode('utf-8')) @@ -34,11 +37,14 @@ def _construct_event(info, locale, defaulttimelen=60, defaultdatelen=1, description=None, location=None, categories=None, repeat=None, until=None, alarm=None, **kwargs): + orig = list(info) info = eventinfofstr(' '.join(info), locale, default_event_duration=dt.timedelta(hours=1), default_dayevent_duration=dt.timedelta(days=1), adjust_reasonably=True, ) + if 'America/New_York' in orig: + breakpoint() if description is not None: info["description"] = description event = new_vevent( @@ -108,49 +114,84 @@ def test_weekdaypstr_invalid(): weekdaypstr('foobar') -def test_construct_daynames(): - with freeze_time('2016-9-19'): - assert construct_daynames(dt.date(2016, 9, 19)) == 'Today' - assert construct_daynames(dt.date(2016, 9, 20)) == 'Tomorrow' - assert construct_daynames(dt.date(2016, 9, 21)) == 'Wednesday' - +class TestConstructDayNames: + def test_construct_daynames(self): + with freeze_time('2016-9-19'): + assert construct_daynames(dt.date(2016, 9, 18), timezone=LOCALE_BERLIN['local_timezone']) == 'Sunday' + assert construct_daynames(dt.date(2016, 9, 19), timezone=LOCALE_BERLIN['local_timezone']) == 'Today' + assert construct_daynames(dt.date(2016, 9, 20), timezone=LOCALE_BERLIN['local_timezone']) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 21), timezone=LOCALE_BERLIN['local_timezone']) == 'Wednesday' + + # freeztime freezes to UTC but construct_daynames should give as back the + # daynames relative the the user's local timezone + def test_construct_daynames_with_datetime(self): + with freeze_time('2016-9-19 22:53'): + assert construct_daynames(dt.date(2016, 9, 18), timezone=LOCALE_BERLIN['local_timezone']) == 'Sunday' + assert construct_daynames(dt.date(2016, 9, 19), timezone=LOCALE_BERLIN['local_timezone']) == 'Monday' + assert construct_daynames(dt.date(2016, 9, 20), timezone=LOCALE_BERLIN['local_timezone']) == 'Today' + assert construct_daynames(dt.date(2016, 9, 21), timezone=LOCALE_BERLIN['local_timezone']) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 22), timezone=LOCALE_BERLIN['local_timezone']) == 'Thursday' + + def test_construct_daynames_los_angeles(self): + with freeze_time('2016-9-19 22:53'): + assert construct_daynames(dt.date(2016, 9, 18), timezone=pytz.timezone('America/Los_Angeles')) == 'Sunday' + assert construct_daynames(dt.date(2016, 9, 19), timezone=pytz.timezone('America/Los_Angeles')) == 'Today' + assert construct_daynames(dt.date(2016, 9, 20), timezone=pytz.timezone('America/Los_Angeles')) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 21), timezone=pytz.timezone('America/Los_Angeles')) == 'Wednesday' + assert construct_daynames(dt.date(2016, 9, 22), timezone=pytz.timezone('America/Los_Angeles')) == 'Thursday' + + def test_construct_daynames_los_angeles_morning(self): + with freeze_time('2016-9-19 06:30'): + assert construct_daynames(dt.date(2016, 9, 18), timezone=pytz.timezone('America/Los_Angeles')) == 'Today' + assert construct_daynames(dt.date(2016, 9, 19), timezone=pytz.timezone('America/Los_Angeles')) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 20), timezone=pytz.timezone('America/Los_Angeles')) == 'Tuesday' + assert construct_daynames(dt.date(2016, 9, 21), timezone=pytz.timezone('America/Los_Angeles')) == 'Wednesday' + assert construct_daynames(dt.date(2016, 9, 22), timezone=pytz.timezone('America/Los_Angeles')) == 'Thursday' + + def test_construct_daynames_auckland(self): + with freeze_time('2016-9-19 22:53'): + assert construct_daynames(dt.date(2016, 9, 18), timezone=pytz.timezone('Pacific/Auckland')) == 'Sunday' + assert construct_daynames(dt.date(2016, 9, 19), timezone=pytz.timezone('Pacific/Auckland')) == 'Monday' + assert construct_daynames(dt.date(2016, 9, 20), timezone=pytz.timezone('Pacific/Auckland')) == 'Today' + assert construct_daynames(dt.date(2016, 9, 21), timezone=pytz.timezone('Pacific/Auckland')) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 22), timezone=pytz.timezone('Pacific/Auckland')) == 'Thursday' class TestGuessDatetimefstr: @freeze_time('2016-9-19T8:00') def test_today(self): - assert (dt.datetime(2016, 9, 19, 13), False) == \ - guessdatetimefstr(['today', '13:00'], LOCALE_BERLIN) - assert dt.date.today() == guessdatetimefstr(['today'], LOCALE_BERLIN)[0].date() + assert (dt.datetime(2016, 9, 19, 13, tzinfo=BERLIN), False) == guessdatetimefstr(['today', '13:00'], LOCALE_BERLIN) + assert (dt.datetime(2016, 9, 19, tzinfo=BERLIN), True) == guessdatetimefstr(['today'], LOCALE_BERLIN) @freeze_time('2016-9-19T8:00') def test_tomorrow(self): - assert (dt.datetime(2016, 9, 20, 16), False) == \ + assert (dt.datetime(2016, 9, 20, 16, tzinfo=BERLIN), False) == \ guessdatetimefstr('tomorrow 16:00 16:00'.split(), locale=LOCALE_BERLIN) @freeze_time('2016-9-19T8:00') def test_time_tomorrow(self): - assert (dt.datetime(2016, 9, 20, 16), False) == \ + assert (dt.datetime(2016, 9, 20, 16, tzinfo=BERLIN), False) == \ guessdatetimefstr( '16:00'.split(), locale=LOCALE_BERLIN, default_day=dt.date(2016, 9, 20)) @freeze_time('2016-9-19T8:00') def test_time_yesterday(self): - assert (dt.datetime(2016, 9, 18, 16), False) == guessdatetimefstr( + assert (dt.datetime(2016, 9, 18, 16, tzinfo=BERLIN), False) == guessdatetimefstr( 'Yesterday 16:00'.split(), locale=LOCALE_BERLIN, default_day=dt.datetime.today()) - @freeze_time('2016-9-19') + @freeze_time('2016-9-19 ') def test_time_weekday(self): - assert (dt.datetime(2016, 9, 23, 16), False) == guessdatetimefstr( + assert (dt.datetime(2016, 9, 23, 16, tzinfo=BERLIN), False) == guessdatetimefstr( 'Friday 16:00'.split(), locale=LOCALE_BERLIN, default_day=dt.datetime.today()) - @freeze_time('2016-9-19 17:53') + time_to_freeze = dt.datetime(2016, 9, 19, 17, 53, tzinfo=BERLIN) + @freeze_time(time_to_freeze) def test_time_now(self): - assert (dt.datetime(2016, 9, 19, 17, 53), False) == guessdatetimefstr( + assert (dt.datetime(2016, 9, 19, 17, 53, tzinfo=BERLIN), False) == guessdatetimefstr( 'now'.split(), locale=LOCALE_BERLIN, default_day=dt.datetime.today()) @freeze_time('2016-12-30 17:53') @@ -162,10 +203,11 @@ def test_long_not_configured(self): 'longdateformat': '', 'datetimeformat': '%Y-%m-%d %H:%M', 'longdatetimeformat': '', + 'local_timezone': LOCALE_BERLIN['local_timezone'], } - assert (dt.datetime(2017, 1, 1), True) == guessdatetimefstr( + assert (dt.datetime(2017, 1, 1, tzinfo=BERLIN), True) == guessdatetimefstr( '2017-1-1'.split(), locale=locale, default_day=dt.datetime.today()) - assert (dt.datetime(2017, 1, 1, 16, 30), False) == guessdatetimefstr( + assert (dt.datetime(2017, 1, 1, 16, 30, tzinfo=BERLIN), False) == guessdatetimefstr( '2017-1-1 16:30'.split(), locale=locale, default_day=dt.datetime.today()) @freeze_time('2016-12-30 17:53') @@ -178,10 +220,11 @@ def test_short_format_contains_year(self): 'longdateformat': '%Y-%m-%d', 'datetimeformat': '%Y-%m-%d %H:%M', 'longdatetimeformat': '%Y-%m-%d %H:%M', + 'local_timezone': LOCALE_BERLIN['local_timezone'], } - assert (dt.datetime(2017, 1, 1), True) == guessdatetimefstr( + assert (dt.datetime(2017, 1, 1, tzinfo=BERLIN), True) == guessdatetimefstr( '2017-1-1'.split(), locale=locale, default_day=dt.datetime.today()) - assert (dt.datetime(2017, 1, 1, 16, 30), False) == guessdatetimefstr( + assert (dt.datetime(2017, 1, 1, 16, 30, tzinfo=BERLIN), False) == guessdatetimefstr( '2017-1-1 16:30'.split(), locale=locale, default_day=dt.datetime.today()) @@ -238,55 +281,55 @@ class TestGuessRangefstr: @freeze_time('2016-9-19') def test_today(self): - assert (dt.datetime(2016, 9, 19, 13), dt.datetime(2016, 9, 19, 14), False) == \ + assert (dt.datetime(2016, 9, 19, 13, tzinfo=BERLIN), dt.datetime(2016, 9, 19, 14, tzinfo=BERLIN), False) == \ guessrangefstr('13:00 14:00', locale=LOCALE_BERLIN) - assert (dt.datetime(2016, 9, 19), dt.datetime(2016, 9, 21), True) == \ + assert (dt.datetime(2016, 9, 19, tzinfo=BERLIN), dt.datetime(2016, 9, 21, tzinfo=BERLIN), True) == \ guessrangefstr('today tomorrow', LOCALE_BERLIN) @freeze_time('2016-9-19 16:34') def test_tomorrow(self): # XXX remove this funtionality, we shouldn't support this anyway - assert (dt.datetime(2016, 9, 19), dt.datetime(2016, 9, 21, 16), True) == \ + assert (dt.datetime(2016, 9, 19, tzinfo=BERLIN), dt.datetime(2016, 9, 21, 16, tzinfo=BERLIN), True) == \ guessrangefstr('today tomorrow 16:00', locale=LOCALE_BERLIN) @freeze_time('2016-9-19 13:34') def test_time_tomorrow(self): - assert (dt.datetime(2016, 9, 19, 16), dt.datetime(2016, 9, 19, 17), False) == \ + assert (dt.datetime(2016, 9, 19, 16, tzinfo=BERLIN), dt.datetime(2016, 9, 19, 17, tzinfo=BERLIN), False) == \ guessrangefstr('16:00', locale=LOCALE_BERLIN) - assert (dt.datetime(2016, 9, 19, 16), dt.datetime(2016, 9, 19, 17), False) == \ + assert (dt.datetime(2016, 9, 19, 16, tzinfo=BERLIN), dt.datetime(2016, 9, 19, 17, tzinfo=BERLIN), False) == \ guessrangefstr('16:00 17:00', locale=LOCALE_BERLIN) def test_start_and_end_date(self): - assert (dt.datetime(2016, 1, 1), dt.datetime(2017, 1, 2), True) == \ + assert (dt.datetime(2016, 1, 1, tzinfo=BERLIN), dt.datetime(2017, 1, 2, tzinfo=BERLIN), True) == \ guessrangefstr('1.1.2016 1.1.2017', locale=LOCALE_BERLIN) def test_start_and_no_end_date(self): - assert (dt.datetime(2016, 1, 1), dt.datetime(2016, 1, 2), True) == \ + assert (dt.datetime(2016, 1, 1, tzinfo=BERLIN), dt.datetime(2016, 1, 2, tzinfo=BERLIN), True) == \ guessrangefstr('1.1.2016', locale=LOCALE_BERLIN) def test_start_and_end_date_time(self): - assert (dt.datetime(2016, 1, 1, 10), dt.datetime(2017, 1, 1, 22), False) == \ + assert (dt.datetime(2016, 1, 1, 10, tzinfo=BERLIN), dt.datetime(2017, 1, 1, 22, tzinfo=BERLIN), False) == \ guessrangefstr( '1.1.2016 10:00 1.1.2017 22:00', locale=LOCALE_BERLIN) def test_start_and_eod(self): - start, end = dt.datetime(2016, 1, 1, 10), dt.datetime(2016, 1, 1, 23, 59, 59, 999999) + start, end = dt.datetime(2016, 1, 1, 10, tzinfo=BERLIN), dt.datetime(2016, 1, 1, 23, 59, 59, 999999, tzinfo=BERLIN) assert (start, end, False) == guessrangefstr('1.1.2016 10:00 eod', locale=LOCALE_BERLIN) def test_start_and_week(self): - assert (dt.datetime(2015, 12, 28), dt.datetime(2016, 1, 5), True) == \ + assert (dt.datetime(2015, 12, 28, tzinfo=BERLIN), dt.datetime(2016, 1, 5, tzinfo=BERLIN), True) == \ guessrangefstr('1.1.2016 week', locale=LOCALE_BERLIN) def test_start_and_delta_1d(self): - assert (dt.datetime(2016, 1, 1), dt.datetime(2016, 1, 2), True) == \ + assert (dt.datetime(2016, 1, 1, tzinfo=BERLIN), dt.datetime(2016, 1, 2, tzinfo=BERLIN), True) == \ guessrangefstr('1.1.2016 1d', locale=LOCALE_BERLIN) def test_start_and_delta_3d(self): - assert (dt.datetime(2016, 1, 1), dt.datetime(2016, 1, 4), True) == \ + assert (dt.datetime(2016, 1, 1, tzinfo=BERLIN), dt.datetime(2016, 1, 4, tzinfo=BERLIN), True) == \ guessrangefstr('1.1.2016 3d', locale=LOCALE_BERLIN) def test_start_dt_and_delta(self): - assert (dt.datetime(2016, 1, 1, 10), dt.datetime(2016, 1, 4, 10), False) == \ + assert (dt.datetime(2016, 1, 1, 10, tzinfo=BERLIN), dt.datetime(2016, 1, 4, 10, tzinfo=BERLIN), False) == \ guessrangefstr('1.1.2016 10:00 3d', locale=LOCALE_BERLIN) def test_start_allday_and_delta_datetime(self): @@ -299,7 +342,7 @@ def test_start_zero_day_delta(self): @freeze_time('20160216') def test_week(self): - assert (dt.datetime(2016, 2, 15), dt.datetime(2016, 2, 23), True) == \ + assert (dt.datetime(2016, 2, 15, tzinfo=BERLIN), dt.datetime(2016, 2, 23, tzinfo=BERLIN), True) == \ guessrangefstr('week', locale=LOCALE_BERLIN) def test_invalid(self): @@ -327,8 +370,9 @@ def test_short_format_contains_year(self): 'longdateformat': '%Y-%m-%d', 'datetimeformat': '%Y-%m-%d %H:%M', 'longdatetimeformat': '%Y-%m-%d %H:%M', + 'local_timezone': LOCALE_BERLIN['local_timezone'], } - assert (dt.datetime(2017, 1, 1), dt.datetime(2017, 1, 2), True) == \ + assert (dt.datetime(2017, 1, 1, tzinfo=BERLIN), dt.datetime(2017, 1, 2, tzinfo=BERLIN), True) == \ guessrangefstr('2017-1-1 2017-1-1', locale=locale)