From 2a6efffec5624c50aa0bbfd10a703b229a139b36 Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Tue, 31 Oct 2023 16:59:50 +0100 Subject: [PATCH 01/11] printformats learns to print current date and time --- khal/cli.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/khal/cli.py b/khal/cli.py index 51ad78b92..755955863 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', @@ -567,3 +576,4 @@ def configure(ctx): main_khal, main_ikhal = cli, interactive_cli + From c878c10a94762f6a35dfbaa1433e5a675a1b023f Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Tue, 31 Oct 2023 17:00:04 +0100 Subject: [PATCH 02/11] trying to reproduce #836 --- tests/cli_test.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) 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') From 3c4327fd138cedf4634ed8861fabf7775b1d043a Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Fri, 3 Nov 2023 23:22:16 +0100 Subject: [PATCH 03/11] fix detection of today and tomorrow When checking if days are the current or the next day, we did not take the local timezone into account. Therefore, if the local timezone was not UTC, the previous or next day could erroneously be detected as `today` (same error for `tomorrow`). --- khal/controllers.py | 2 +- khal/parse_datetime.py | 7 ++++--- tests/parse_datetime_test.py | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/khal/controllers.py b/khal/controllers.py index abe36e9d7..ba9b1f6ce 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -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, local_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"]: diff --git a/khal/parse_datetime.py b/khal/parse_datetime.py index c7d92f2ff..51b646bc0 100644 --- a/khal/parse_datetime.py +++ b/khal/parse_datetime.py @@ -125,14 +125,15 @@ 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, local_timezone) -> str: """converts datetime.date into a string description either `Today`, `Tomorrow` or name of weekday. """ - if date_ == dt.date.today(): + today = dt.datetime.now(local_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') diff --git a/tests/parse_datetime_test.py b/tests/parse_datetime_test.py index 3eaa568c1..2ebbca04b 100644 --- a/tests/parse_datetime_test.py +++ b/tests/parse_datetime_test.py @@ -110,9 +110,9 @@ def test_weekdaypstr_invalid(): 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' + assert construct_daynames(dt.date(2016, 9, 19), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Today' + assert construct_daynames(dt.date(2016, 9, 20), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 21), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Wednesday' class TestGuessDatetimefstr: From 7b2c186c68207b1b799e8b4bbd9fc279bad739c0 Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Fri, 3 Nov 2023 23:54:29 +0100 Subject: [PATCH 04/11] fix default for `list` and `calendar` command The default (today) was until now not localized, but today in UTC was assumed. This could lead to commands such as `khal calendar` starting to show events from with either the previous or the next day instead of today. --- khal/controllers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/khal/controllers.py b/khal/controllers.py index ba9b1f6ce..b35e27e5b 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 @@ -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( From 4413328a6d9ea3323a8cc3da91f855185db7f119 Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Sat, 4 Nov 2023 00:25:22 +0100 Subject: [PATCH 05/11] more tests for dayname detection --- tests/parse_datetime_test.py | 47 +++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/tests/parse_datetime_test.py b/tests/parse_datetime_test.py index 2ebbca04b..58f53f591 100644 --- a/tests/parse_datetime_test.py +++ b/tests/parse_datetime_test.py @@ -108,12 +108,47 @@ def test_weekdaypstr_invalid(): weekdaypstr('foobar') -def test_construct_daynames(): - with freeze_time('2016-9-19'): - assert construct_daynames(dt.date(2016, 9, 19), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Today' - assert construct_daynames(dt.date(2016, 9, 20), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Tomorrow' - assert construct_daynames(dt.date(2016, 9, 21), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Wednesday' - +class TestConstructDayNames: + def test_construct_daynames(self): + with freeze_time('2016-9-19'): + assert construct_daynames(dt.date(2016, 9, 18), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Sunday' + assert construct_daynames(dt.date(2016, 9, 19), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Today' + assert construct_daynames(dt.date(2016, 9, 20), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 21), local_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), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Sunday' + assert construct_daynames(dt.date(2016, 9, 19), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Monday' + assert construct_daynames(dt.date(2016, 9, 20), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Today' + assert construct_daynames(dt.date(2016, 9, 21), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 22), local_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), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Sunday' + assert construct_daynames(dt.date(2016, 9, 19), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Today' + assert construct_daynames(dt.date(2016, 9, 20), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 21), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Wednesday' + assert construct_daynames(dt.date(2016, 9, 22), local_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), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Today' + assert construct_daynames(dt.date(2016, 9, 19), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 20), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Tuesday' + assert construct_daynames(dt.date(2016, 9, 21), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Wednesday' + assert construct_daynames(dt.date(2016, 9, 22), local_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), local_timezone=pytz.timezone('Pacific/Auckland')) == 'Sunday' + assert construct_daynames(dt.date(2016, 9, 19), local_timezone=pytz.timezone('Pacific/Auckland')) == 'Monday' + assert construct_daynames(dt.date(2016, 9, 20), local_timezone=pytz.timezone('Pacific/Auckland')) == 'Today' + assert construct_daynames(dt.date(2016, 9, 21), local_timezone=pytz.timezone('Pacific/Auckland')) == 'Tomorrow' + assert construct_daynames(dt.date(2016, 9, 22), local_timezone=pytz.timezone('Pacific/Auckland')) == 'Thursday' class TestGuessDatetimefstr: From 8c30cf382b46ab018d816db034d6a832dde8a3a2 Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Sat, 4 Nov 2023 00:35:15 +0100 Subject: [PATCH 06/11] move start_end_from_daterange tests to a class --- tests/controller_test.py | 76 +++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/tests/controller_test.py b/tests/controller_test.py index dfe52ff94..1918333b5 100644 --- a/tests/controller_test.py +++ b/tests/controller_test.py @@ -143,40 +143,42 @@ 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), + ) + From d4ac39de88ec138aef18505db57523645be9740b Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Sat, 4 Nov 2023 00:36:42 +0100 Subject: [PATCH 07/11] start berlin --- tests/controller_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/controller_test.py b/tests/controller_test.py index 1918333b5..4b6dbc43d 100644 --- a/tests/controller_test.py +++ b/tests/controller_test.py @@ -182,3 +182,8 @@ def test_start_end_empty_default(self): 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) From 5238dd065d107b48a5d4ce6f2f25810fec9a8b1c Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Sun, 14 Apr 2024 16:48:22 +0200 Subject: [PATCH 08/11] import pytz --- tests/parse_datetime_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/parse_datetime_test.py b/tests/parse_datetime_test.py index 58f53f591..4e02cba00 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 From c45bcf64ba35e788da46b8b1747f5d765b3129f2 Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Mon, 22 Apr 2024 23:18:28 +0200 Subject: [PATCH 09/11] WIP --- .gitignore | 1 + khal/controllers.py | 2 +- khal/icalendar.py | 6 +- khal/parse_datetime.py | 94 +++++++++++++++++------------ khal/ui/__init__.py | 7 ++- tests/parse_datetime_test.py | 114 +++++++++++++++++++---------------- 6 files changed, 126 insertions(+), 98 deletions(-) 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/controllers.py b/khal/controllers.py index b35e27e5b..77a58ee40 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -556,7 +556,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 51b646bc0..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,12 +130,12 @@ def weekdaypstr(dayname: str) -> int: raise ValueError('invalid weekday name `%s`' % dayname) -def construct_daynames(date_: dt.date, local_timezone) -> 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. """ - today = dt.datetime.now(local_timezone).date() + today = dt.datetime.now(timezone).date() if date_ == today: return 'Today' elif date_ == today + dt.timedelta(days=1): @@ -139,13 +144,14 @@ def construct_daynames(date_: dt.date, local_timezone) -> str: 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 @@ -161,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 @@ -172,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 @@ -200,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] @@ -241,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 " @@ -341,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 @@ -365,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) @@ -397,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]) @@ -405,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 @@ -423,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/parse_datetime_test.py b/tests/parse_datetime_test.py index 4e02cba00..9044bd7f9 100644 --- a/tests/parse_datetime_test.py +++ b/tests/parse_datetime_test.py @@ -25,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')) @@ -35,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( @@ -112,81 +117,81 @@ def test_weekdaypstr_invalid(): class TestConstructDayNames: def test_construct_daynames(self): with freeze_time('2016-9-19'): - assert construct_daynames(dt.date(2016, 9, 18), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Sunday' - assert construct_daynames(dt.date(2016, 9, 19), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Today' - assert construct_daynames(dt.date(2016, 9, 20), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Tomorrow' - assert construct_daynames(dt.date(2016, 9, 21), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Wednesday' + 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), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Sunday' - assert construct_daynames(dt.date(2016, 9, 19), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Monday' - assert construct_daynames(dt.date(2016, 9, 20), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Today' - assert construct_daynames(dt.date(2016, 9, 21), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Tomorrow' - assert construct_daynames(dt.date(2016, 9, 22), local_timezone=LOCALE_BERLIN['local_timezone']) == 'Thursday' + 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), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Sunday' - assert construct_daynames(dt.date(2016, 9, 19), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Today' - assert construct_daynames(dt.date(2016, 9, 20), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Tomorrow' - assert construct_daynames(dt.date(2016, 9, 21), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Wednesday' - assert construct_daynames(dt.date(2016, 9, 22), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Thursday' + 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), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Today' - assert construct_daynames(dt.date(2016, 9, 19), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Tomorrow' - assert construct_daynames(dt.date(2016, 9, 20), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Tuesday' - assert construct_daynames(dt.date(2016, 9, 21), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Wednesday' - assert construct_daynames(dt.date(2016, 9, 22), local_timezone=pytz.timezone('America/Los_Angeles')) == 'Thursday' + 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), local_timezone=pytz.timezone('Pacific/Auckland')) == 'Sunday' - assert construct_daynames(dt.date(2016, 9, 19), local_timezone=pytz.timezone('Pacific/Auckland')) == 'Monday' - assert construct_daynames(dt.date(2016, 9, 20), local_timezone=pytz.timezone('Pacific/Auckland')) == 'Today' - assert construct_daynames(dt.date(2016, 9, 21), local_timezone=pytz.timezone('Pacific/Auckland')) == 'Tomorrow' - assert construct_daynames(dt.date(2016, 9, 22), local_timezone=pytz.timezone('Pacific/Auckland')) == 'Thursday' + 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') @@ -198,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') @@ -214,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()) @@ -274,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): @@ -335,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): @@ -363,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) From 460c5d2b46ef72ed1d1c662adcfc3738965826a6 Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Tue, 30 Apr 2024 00:24:58 +0200 Subject: [PATCH 10/11] update --- khal/controllers.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/khal/controllers.py b/khal/controllers.py index 77a58ee40..fb5004404 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -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, local_timezone=locale['local_timezone']) + 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"]: @@ -204,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) @@ -273,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: @@ -295,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, @@ -320,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 From b52e96b1339b22b0bcafbfeb98f26e84e7dbd7f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 May 2024 20:58:33 +0000 Subject: [PATCH 11/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- khal/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/khal/cli.py b/khal/cli.py index 755955863..e4704906e 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -576,4 +576,3 @@ def configure(ctx): main_khal, main_ikhal = cli, interactive_cli -