From 7328e8cb39977869120e65247bea6ce3077b4540 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Thu, 6 May 2021 21:10:14 +0900 Subject: [PATCH 01/35] add json option to at command --- khal/cli.py | 9 ++++++--- khal/controllers.py | 32 ++++++++++++++++++++++++++------ khal/khalendar/event.py | 5 ++--- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/khal/cli.py b/khal/cli.py index 538ff9d4d..cb0523560 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -605,7 +605,8 @@ def search(ctx, format, search_string, include_calendar, exclude_calendar): now = dt.datetime.now() env = {"calendars": ctx.obj['conf']['calendars']} for event in events: - desc = textwrap.wrap(event.format(format, relative_to=now, env=env), term_width) + desc = textwrap.wrap(event.format(controllers.human_formatter( + format), relative_to=now, env=env), term_width) event_column.extend( [colored(d, event.color, bold_for_light_color=ctx.obj['conf']['view']['bold_for_light_color']) @@ -655,9 +656,10 @@ def edit(ctx, format, search_string, show_past, include_calendar, exclude_calend help=('The format of the day line.')) @click.option('--notstarted', help=('Print only events that have not started'), is_flag=True) + @click.option('--json', help=("Fields to output in json"), multiple=True) @click.argument('DATETIME', nargs=-1, required=False, metavar='[[START DATE] TIME | now]') @click.pass_context - def at(ctx, datetime, notstarted, format, day_format, include_calendar, exclude_calendar): + def at(ctx, datetime, notstarted, format, day_format, json, include_calendar, exclude_calendar): '''Print all events at a specific datetime (defaults to now).''' if not datetime: datetime = ("now",) @@ -675,7 +677,8 @@ def at(ctx, datetime, notstarted, format, day_format, include_calendar, exclude_ once=True, notstarted=notstarted, conf=ctx.obj['conf'], - env={"calendars": ctx.obj['conf']['calendars']} + env={"calendars": ctx.obj['conf']['calendars']}, + json=json ) if rows: click.echo('\n'.join(rows)) diff --git a/khal/controllers.py b/khal/controllers.py index cb789301c..7c292ac75 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -25,9 +25,10 @@ import os import re import textwrap +import json from collections import OrderedDict, defaultdict from shutil import get_terminal_size -from typing import List, Optional +from typing import Callable, Iterable, List, Optional import pytz from click import confirm, echo, prompt, style @@ -72,6 +73,13 @@ def format_day(day: dt.date, format_string: str, locale, attributes=None): except (KeyError, IndexError): raise KeyError("cannot format day with: %s" % format_string) +def human_formatter(format_string): + return lambda *args, **kwargs: format_string.format(*args, **kwargs) + + +def json_formatter(fields): + return lambda **kwargs: json.dumps(dict(filter(lambda e: e[0] in fields, kwargs.items())), ensure_ascii=False) + def calendar( collection: CalendarCollection, @@ -95,6 +103,7 @@ def calendar( bold_for_light_color: bool=True, env=None, ): + term_width, _ = get_terminal_size() lwidth = 27 if conf['locale']['weeknumbers'] == 'right' else 25 rwidth = term_width - lwidth - 4 @@ -170,6 +179,7 @@ def get_events_between( locale: dict, start: dt.datetime, end: dt.datetime, + formatter: Callable, agenda_format: str, notstarted: bool, env: dict, @@ -183,6 +193,7 @@ def get_events_between( :param collection: :param start: the start datetime :param end: the end datetime + :param formatter: a function that is used for formatting the event :param agenda_format: a format string that can be used in python string formatting :param env: a collection of "static" values like calendar names and color :param nostarted: True if each event should start after start (instead of @@ -218,11 +229,11 @@ def get_events_between( continue try: - event_string = event.format(agenda_format, relative_to=(start, end), env=env) + event_string = event.format(formatter, relative_to=(start, end), env=env, colors=colors) except KeyError as error: raise FatalError(error) - if width: + if width and colors: event_list += utils.color_wrap(event_string, width) else: event_list.append(event_string) @@ -243,6 +254,7 @@ def khal_list( width: Optional[int] = None, env=None, datepoint=None, + json: Iterable = [], ): """returns a list of all events in `daterange`""" assert daterange is not None or datepoint is not None @@ -252,6 +264,13 @@ def khal_list( if agenda_format is None: agenda_format = conf['view']['agenda_event_format'] + if len(json) == 0: + formatter = human_formatter(agenda_format) + colors = True + else: + formatter = json_formatter(json) + colors = False + if daterange is not None: if day_format is None: day_format = conf['view']['agenda_day_format'] @@ -296,13 +315,14 @@ def khal_list( else: day_end = dt.datetime.combine(start.date(), dt.time.max) current_events = get_events_between( - collection, locale=conf['locale'], agenda_format=agenda_format, start=start, + collection, locale=conf['locale'], formatter=formatter, start=start, end=day_end, notstarted=notstarted, original_start=original_start, env=env, seen=once, width=width, + colors=colors, ) - if day_format and (conf['default']['show_all_days'] or current_events): + if day_format and (conf['default']['show_all_days'] or current_events) and len(json) == 0: if len(event_column) != 0 and conf['view']['blank_line_before_day']: event_column.append('') event_column.append(format_day(start.date(), day_format, conf['locale'])) @@ -439,7 +459,7 @@ def new_from_dict( if conf['default']['print_new'] == 'event': if format is None: format = conf['view']['event_format'] - echo(event.format(format, dt.datetime.now(), env=env)) + echo(event.format(human_formatter(format), dt.datetime.now(), env=env)) elif conf['default']['print_new'] == 'path': assert event.href path = os.path.join(collection._calendars[event.calendar]['path'], event.href) diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index 010da1ede..487d31ca5 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -555,10 +555,9 @@ def _alarm_str(self) -> str: alarmstr = '' return alarmstr - def format(self, format_string: str, relative_to, env=None, colors: bool=True): + def format(self, formatter, format_string: str, relative_to, env=None, colors: bool=True): """ :param colors: determines if colors codes should be printed or not - :type colors: bool """ env = env or {} @@ -722,7 +721,7 @@ def format(self, format_string: str, relative_to, env=None, colors: bool=True): attributes['status'] = self.status + ' ' if self.status else '' attributes['cancelled'] = 'CANCELLED ' if self.status == 'CANCELLED' else '' - return format_string.format(**dict(attributes)) + attributes["reset"] + return formatter(**dict(attributes)) + attributes["reset"] def duplicate(self) -> 'Event': """duplicate this event's PROTO event""" From 666771572bfc3ba93d8cf004d55239ffad655747 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Thu, 6 May 2021 21:51:33 +0900 Subject: [PATCH 02/35] added list json formatting --- khal/cli.py | 4 ++-- khal/controllers.py | 45 ++++++++++++++++++++++++----------------- khal/khalendar/event.py | 4 ++-- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/khal/cli.py b/khal/cli.py index cb0523560..8200e357c 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -605,8 +605,8 @@ def search(ctx, format, search_string, include_calendar, exclude_calendar): now = dt.datetime.now() env = {"calendars": ctx.obj['conf']['calendars']} for event in events: - desc = textwrap.wrap(event.format(controllers.human_formatter( - format), relative_to=now, env=env), term_width) + desc = textwrap.wrap(controllers.human_formatter(format)( + event.format(relative_to=now, env=env), term_width)) event_column.extend( [colored(d, event.color, bold_for_light_color=ctx.obj['conf']['view']['bold_for_light_color']) diff --git a/khal/controllers.py b/khal/controllers.py index 7c292ac75..28bf2a8da 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -73,12 +73,25 @@ def format_day(day: dt.date, format_string: str, locale, attributes=None): except (KeyError, IndexError): raise KeyError("cannot format day with: %s" % format_string) -def human_formatter(format_string): - return lambda *args, **kwargs: format_string.format(*args, **kwargs) + +def human_formatter(format_string, width=None): + def fmt(rows): + results = [] + for row in rows: + s = format_string.format(**row) + if width: + s = utils.color_wrap(s, width) + results.append(s) + return '\n'.join(results) + return fmt def json_formatter(fields): - return lambda **kwargs: json.dumps(dict(filter(lambda e: e[0] in fields, kwargs.items())), ensure_ascii=False) + def fmt(rows): + return json.dumps( + [dict(filter(lambda e: e[0] in fields, row.items())) for row in rows], + ensure_ascii=False) + return fmt def calendar( @@ -180,24 +193,24 @@ def get_events_between( start: dt.datetime, end: dt.datetime, formatter: Callable, - agenda_format: str, notstarted: bool, env: dict, width: Optional[int], - seen, + seen=None, original_start: dt.datetime, ) -> List[str]: """returns a list of events scheduled between start and end. Start and end are strings or datetimes (of some kind). :param collection: + :param locale: :param start: the start datetime :param end: the end datetime :param formatter: a function that is used for formatting the event - :param agenda_format: a format string that can be used in python string formatting - :param env: a collection of "static" values like calendar names and color :param nostarted: True if each event should start after start (instead of - be active between start and end) + be active between start and end) + :param env: a collection of "static" values like calendar names and color + :param seen: :param original_start: start datetime to compare against of notstarted is set :returns: a list to be printed as the agenda for the given days """ @@ -229,18 +242,15 @@ def get_events_between( continue try: - event_string = event.format(formatter, relative_to=(start, end), env=env, colors=colors) + event_attributes = event.format(relative_to=(start, end), env=env, colors=colors) except KeyError as error: raise FatalError(error) - if width and colors: - event_list += utils.color_wrap(event_string, width) - else: - event_list.append(event_string) + event_list.append(event_attributes) if seen is not None: seen.add(event.uid) - return event_list + return formatter(event_list) def khal_list( @@ -265,7 +275,7 @@ def khal_list( agenda_format = conf['view']['agenda_event_format'] if len(json) == 0: - formatter = human_formatter(agenda_format) + formatter = human_formatter(agenda_format, width) colors = True else: formatter = json_formatter(json) @@ -319,14 +329,13 @@ def khal_list( end=day_end, notstarted=notstarted, original_start=original_start, env=env, seen=once, - width=width, colors=colors, ) if day_format and (conf['default']['show_all_days'] or current_events) and len(json) == 0: if len(event_column) != 0 and conf['view']['blank_line_before_day']: event_column.append('') event_column.append(format_day(start.date(), day_format, conf['locale'])) - event_column.extend(current_events) + event_column.append(current_events) start = dt.datetime(*start.date().timetuple()[:3]) + dt.timedelta(days=1) return event_column @@ -459,7 +468,7 @@ def new_from_dict( if conf['default']['print_new'] == 'event': if format is None: format = conf['view']['event_format'] - echo(event.format(human_formatter(format), dt.datetime.now(), env=env)) + echo(human_formatter(format)(event.format(dt.datetime.now(), env=env))) elif conf['default']['print_new'] == 'path': assert event.href path = os.path.join(collection._calendars[event.calendar]['path'], event.href) diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index 487d31ca5..f9e1e9444 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -555,7 +555,7 @@ def _alarm_str(self) -> str: alarmstr = '' return alarmstr - def format(self, formatter, format_string: str, relative_to, env=None, colors: bool=True): + def attributes(self, relative_to, env=None, colors: bool=True): """ :param colors: determines if colors codes should be printed or not """ @@ -721,7 +721,7 @@ def format(self, formatter, format_string: str, relative_to, env=None, colors: b attributes['status'] = self.status + ' ' if self.status else '' attributes['cancelled'] = 'CANCELLED ' if self.status == 'CANCELLED' else '' - return formatter(**dict(attributes)) + attributes["reset"] + return dict(attributes) def duplicate(self) -> 'Event': """duplicate this event's PROTO event""" From d996489ff2c89498bacfc836b51288bbec22143a Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Fri, 7 May 2021 19:50:56 +0900 Subject: [PATCH 03/35] fix color wrap --- khal/controllers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/khal/controllers.py b/khal/controllers.py index 28bf2a8da..7a4940f6f 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -80,8 +80,9 @@ def fmt(rows): for row in rows: s = format_string.format(**row) if width: - s = utils.color_wrap(s, width) - results.append(s) + results += utils.color_wrap(s, width) + else: + results.append(s) return '\n'.join(results) return fmt From 65a173d0cc77996fbc430774fb47e18535acbebc Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Fri, 7 May 2021 19:57:35 +0900 Subject: [PATCH 04/35] fixed formatting --- khal/controllers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/khal/controllers.py b/khal/controllers.py index 7a4940f6f..c7d0b49d3 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -83,15 +83,15 @@ def fmt(rows): results += utils.color_wrap(s, width) else: results.append(s) - return '\n'.join(results) + return results return fmt def json_formatter(fields): def fmt(rows): - return json.dumps( + return [json.dumps( [dict(filter(lambda e: e[0] in fields, row.items())) for row in rows], - ensure_ascii=False) + ensure_ascii=False)] return fmt @@ -336,7 +336,7 @@ def khal_list( if len(event_column) != 0 and conf['view']['blank_line_before_day']: event_column.append('') event_column.append(format_day(start.date(), day_format, conf['locale'])) - event_column.append(current_events) + event_column.extend(current_events) start = dt.datetime(*start.date().timetuple()[:3]) + dt.timedelta(days=1) return event_column From cdb8568aa1d969cecb5b26dac01d8044286a00c7 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Fri, 7 May 2021 20:32:15 +0900 Subject: [PATCH 05/35] added missing tail reset --- khal/controllers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khal/controllers.py b/khal/controllers.py index c7d0b49d3..03a04bae3 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -78,7 +78,7 @@ def human_formatter(format_string, width=None): def fmt(rows): results = [] for row in rows: - s = format_string.format(**row) + s = format_string.format(**row) + style('', reset=True) if width: results += utils.color_wrap(s, width) else: From 322cdc2803f38dee9bef2a3ff803eeb70129ab04 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Fri, 7 May 2021 20:48:51 +0900 Subject: [PATCH 06/35] fix search formatter usage --- khal/cli.py | 2 +- khal/controllers.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/khal/cli.py b/khal/cli.py index 8200e357c..aaaa60e72 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -606,7 +606,7 @@ def search(ctx, format, search_string, include_calendar, exclude_calendar): env = {"calendars": ctx.obj['conf']['calendars']} for event in events: desc = textwrap.wrap(controllers.human_formatter(format)( - event.format(relative_to=now, env=env), term_width)) + event.format(relative_to=now, env=env)), term_width) event_column.extend( [colored(d, event.color, bold_for_light_color=ctx.obj['conf']['view']['bold_for_light_color']) diff --git a/khal/controllers.py b/khal/controllers.py index 03a04bae3..1239c2bec 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -76,6 +76,9 @@ def format_day(day: dt.date, format_string: str, locale, attributes=None): def human_formatter(format_string, width=None): def fmt(rows): + single = type(rows) == dict + if single: + rows = [rows] results = [] for row in rows: s = format_string.format(**row) + style('', reset=True) @@ -83,7 +86,10 @@ def fmt(rows): results += utils.color_wrap(s, width) else: results.append(s) - return results + if single: + return results[0] + else: + return results return fmt From 43a172f0a22d5a5dabb9a72cce53205f6c416f2a Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Fri, 7 May 2021 21:00:52 +0900 Subject: [PATCH 07/35] fixed missing human_formatter --- khal/controllers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/khal/controllers.py b/khal/controllers.py index 1239c2bec..932207a96 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -540,7 +540,7 @@ def edit_event(event, collection, locale, allow_quit=False, width=80): collection.delete(event.href, event.etag, event.calendar) return True elif choice == "datetime range": - current = event.format("{start} {end}", relative_to=now) + current = human_formatter("{start} {end}")(event.format(relative_to=now)) value = prompt("datetime range", default=current) try: start, end, allday = parse_datetime.guessrangefstr(ansi.sub('', value), locale) @@ -624,7 +624,8 @@ def edit(collection, search_string, locale, format=None, allow_past=False, conf= continue elif not event.allday and event.end_local < now: continue - event_text = textwrap.wrap(event.format(format, relative_to=now), term_width) + event_text = textwrap.wrap(human_formatter(format)( + event.format(relative_to=now)), term_width) echo(''.join(event_text)) if not edit_event(event, collection, locale, allow_quit=True, width=term_width): return @@ -676,7 +677,7 @@ def import_event(vevent, collection, locale, batch, format=None, env=None): if item.name == 'VEVENT': event = Event.fromVEvents( [item], calendar=collection.default_calendar_name, locale=locale) - echo(event.format(format, dt.datetime.now(), env=env)) + echo(human_formatter(format)(event.format(dt.datetime.now(), env=env))) # get the calendar to insert into if not collection.writable_names: @@ -733,4 +734,4 @@ def print_ics(conf, name, ics, format): echo(f'{len(vevents)} events found in {name}') for sub_event in vevents: event = Event.fromVEvents(sub_event, locale=conf['locale']) - echo(event.format(format, dt.datetime.now())) + echo(human_formatter(format)(event.format(dt.datetime.now()))) From c7ef0079c0738d9c974835bdf1cb9b165c3edbec Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Fri, 7 May 2021 21:32:54 +0900 Subject: [PATCH 08/35] fix formatter usage in test --- tests/event_test.py | 107 ++++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/tests/event_test.py b/tests/event_test.py index 34e330439..01324de64 100644 --- a/tests/event_test.py +++ b/tests/event_test.py @@ -8,6 +8,7 @@ from icalendar import Parameters, vCalAddress, vRecur, vText from packaging import version +from khal.controllers import human_formatter from khal.khalendar.event import AllDayEvent, Event, FloatingEvent, LocalizedEvent, create_timezone from .utils import ( @@ -25,10 +26,12 @@ EVENT_KWARGS = {'calendar': 'foobar', 'locale': LOCALE_BERLIN} LIST_FORMAT = '{calendar-color}{cancelled}{start-end-time-style} {title}{repeat-symbol}' +LIST_FORMATTER = human_formatter(LIST_FORMAT) SEARCH_FORMAT = '{calendar-color}{cancelled}{start-long}{to-style}' + \ '{end-necessary-long} {title}{repeat-symbol}' FORMAT_CALENDAR = ('{calendar-color}{cancelled}{start-end-time-style} ({calendar}) ' '{title} [{location}]{repeat-symbol}') +SEARCH_FORMATTER = human_formatter(SEARCH_FORMAT) def test_no_initialization(): @@ -51,8 +54,9 @@ def test_raw_dt(): normalize_component(_get_text('event_dt_simple_inkl_vtimezone')) event = Event.fromString(event_dt, **EVENT_KWARGS) - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == '09:30-10:30 An Event\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 9)) == \ + assert LIST_FORMATTER(event.format( + dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-10:30 An Event\x1b[0m' assert event.recurring is False assert event.duration == dt.timedelta(hours=1) @@ -99,7 +103,7 @@ def test_no_end(): event = Event.fromString(_get_text('event_dt_no_end'), **EVENT_KWARGS) # TODO make sure the event also gets converted to an all day event, as we # usually do - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 12)) == \ + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 12))) == \ '16.01.2016 08:00-17.01.2016 08:00 Test\x1b[0m' @@ -150,8 +154,8 @@ def test_raw_d(): event_d = _get_text('event_d') event = Event.fromString(event_d, **EVENT_KWARGS) assert event.raw.split('\r\n') == _get_text('cal_d').split('\n') - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == ' An Event\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 9)) == '09.04.2014 An Event\x1b[0m' + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == ' An Event\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09.04.2014 An Event\x1b[0m' def test_update_sequence(): @@ -177,8 +181,8 @@ def test_transform_event(): end = BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) event.update_start_end(start, end) assert isinstance(event, LocalizedEvent) - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == '09:30-10:30 An Event\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 9)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-10:30 An Event\x1b[0m' analog_event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) assert normalize_component(event.raw) == normalize_component(analog_event.raw) @@ -191,10 +195,10 @@ def test_update_event_d(): event_d = _get_text('event_d') event = Event.fromString(event_d, **EVENT_KWARGS) event.update_start_end(dt.date(2014, 4, 20), dt.date(2014, 4, 22)) - assert event.format(LIST_FORMAT, dt.date(2014, 4, 20)) == '↦ An Event\x1b[0m' - assert event.format(LIST_FORMAT, dt.date(2014, 4, 21)) == '↔ An Event\x1b[0m' - assert event.format(LIST_FORMAT, dt.date(2014, 4, 22)) == '⇥ An Event\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 20)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 20))) == '↦ An Event\x1b[0m' + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 21))) == '↔ An Event\x1b[0m' + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 22))) == '⇥ An Event\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 20))) == \ '20.04.2014-22.04.2014 An Event\x1b[0m' assert 'DTSTART;VALUE=DATE:20140420' in event.raw.split('\r\n') assert 'DTEND;VALUE=DATE:20140423' in event.raw.split('\r\n') @@ -226,8 +230,8 @@ def test_dt_two_tz(): # local (Berlin) time! assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end_local == BERLIN.localize(dt.datetime(2014, 4, 9, 16, 30)) - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == '09:30-16:30 An Event\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 9)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30-16:30 An Event\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-16:30 An Event\x1b[0m' @@ -237,10 +241,10 @@ def test_event_dt_duration(): event = Event.fromString(event_dt_duration, **EVENT_KWARGS) assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == '09:30-10:30 An Event\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 9)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-10:30 An Event\x1b[0m' - assert event.format('{duration}', relative_to=dt.date.today()) == '1h\x1b[0m' + assert human_formatter('{duration}')(event.format(relative_to=dt.date.today())) == '1h\x1b[0m' def test_event_dt_floating(): @@ -248,9 +252,9 @@ def test_event_dt_floating(): event_str = _get_text('event_dt_floating') event = Event.fromString(event_str, **EVENT_KWARGS) assert isinstance(event, FloatingEvent) - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == '09:30-10:30 An Event\x1b[0m' - assert event.format('{duration}', relative_to=dt.date.today()) == '1h\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 9)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' + assert human_formatter('{duration}')(event.format(relative_to=dt.date.today())) == '1h\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-10:30 An Event\x1b[0m' assert event.start == dt.datetime(2014, 4, 9, 9, 30) assert event.end == dt.datetime(2014, 4, 9, 10, 30) @@ -272,7 +276,7 @@ def test_event_dt_tz_missing(): assert event.end == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end_local == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) - assert event.format('{duration}', relative_to=dt.date.today()) == '1h\x1b[0m' + assert human_formatter('{duration}')(event.format(relative_to=dt.date.today())) == '1h\x1b[0m' event = Event.fromString(event_str, calendar='foobar', locale=LOCALE_MIXED) assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) @@ -286,27 +290,29 @@ def test_event_dt_rr(): event = Event.fromString(event_dt_rr, **EVENT_KWARGS) assert event.recurring is True - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == '09:30-10:30 An Event ⟳\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 9)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30-10:30 An Event ⟳\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-10:30 An Event ⟳\x1b[0m' - assert event.format('{repeat-pattern}', dt.date(2014, 4, 9)) == 'FREQ=DAILY;COUNT=10\x1b[0m' + assert human_formatter('{repeat-pattern}')(event.format(dt.date(2014, 4, 9)) + ) == 'FREQ=DAILY;COUNT=10\x1b[0m' def test_event_d_rr(): event_d_rr = _get_text('event_d_rr') event = Event.fromString(event_d_rr, **EVENT_KWARGS) assert event.recurring is True - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == ' Another Event ⟳\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 9)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == ' Another Event ⟳\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ '09.04.2014 Another Event ⟳\x1b[0m' - assert event.format('{repeat-pattern}', dt.date(2014, 4, 9)) == 'FREQ=DAILY;COUNT=10\x1b[0m' + assert human_formatter('{repeat-pattern}')(event.format(dt.date(2014, 4, 9)) + ) == 'FREQ=DAILY;COUNT=10\x1b[0m' start = dt.date(2014, 4, 10) end = dt.date(2014, 4, 11) event = Event.fromString(event_d_rr, start=start, end=end, **EVENT_KWARGS) assert event.recurring is True - assert event.format(LIST_FORMAT, dt.date(2014, 4, 10)) == ' Another Event ⟳\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 10)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 10))) == ' Another Event ⟳\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 10))) == \ '10.04.2014 Another Event ⟳\x1b[0m' @@ -319,23 +325,24 @@ def test_event_rd(): def test_event_d_long(): event_d_long = _get_text('event_d_long') event = Event.fromString(event_d_long, **EVENT_KWARGS) - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == '↦ Another Event\x1b[0m' - assert event.format(LIST_FORMAT, dt.date(2014, 4, 10)) == '↔ Another Event\x1b[0m' - assert event.format(LIST_FORMAT, dt.date(2014, 4, 11)) == '⇥ Another Event\x1b[0m' - assert event.format(LIST_FORMAT, dt.date(2014, 4, 12)) == ' Another Event\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 16)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '↦ Another Event\x1b[0m' + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 10))) == '↔ Another Event\x1b[0m' + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 11))) == '⇥ Another Event\x1b[0m' + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 12))) == ' Another Event\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 16))) == \ '09.04.2014-11.04.2014 Another Event\x1b[0m' - assert event.format('{duration}', relative_to=dt.date(2014, 4, 11)) == '3d\x1b[0m' + assert human_formatter('{duration}')(event.format( + relative_to=dt.date(2014, 4, 11))) == '3d\x1b[0m' def test_event_d_two_days(): event_d_long = _get_text('event_d_long') event = Event.fromString(event_d_long, **EVENT_KWARGS) event.update_start_end(dt.date(2014, 4, 9), dt.date(2014, 4, 10)) - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == '↦ Another Event\x1b[0m' - assert event.format(LIST_FORMAT, dt.date(2014, 4, 10)) == '⇥ Another Event\x1b[0m' - assert event.format(LIST_FORMAT, dt.date(2014, 4, 12)) == ' Another Event\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 10)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '↦ Another Event\x1b[0m' + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 10))) == '⇥ Another Event\x1b[0m' + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 12))) == ' Another Event\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 10))) == \ '09.04.2014-10.04.2014 Another Event\x1b[0m' @@ -343,10 +350,10 @@ def test_event_dt_long(): event_dt_long = _get_text('event_dt_long') event = Event.fromString(event_dt_long, **EVENT_KWARGS) - assert event.format(LIST_FORMAT, dt.date(2014, 4, 9)) == '09:30→ An Event\x1b[0m' - assert event.format(LIST_FORMAT, dt.date(2014, 4, 10)) == '↔ An Event\x1b[0m' - assert event.format(LIST_FORMAT, dt.date(2014, 4, 12)) == '→10:30 An Event\x1b[0m' - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 10)) == \ + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30→ An Event\x1b[0m' + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 10))) == '↔ An Event\x1b[0m' + assert LIST_FORMATTER(event.format(dt.date(2014, 4, 12))) == '→10:30 An Event\x1b[0m' + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 10))) == \ '09.04.2014 09:30-12.04.2014 10:30 An Event\x1b[0m' @@ -368,7 +375,7 @@ def test_event_no_dst(): ) assert normalize_component(event.raw) == normalize_component(cal_no_dst) - assert event.format(SEARCH_FORMAT, dt.date(2014, 4, 10)) == \ + assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 10))) == \ '09.04.2014 09:30-10:30 An Event\x1b[0m' @@ -415,10 +422,10 @@ def test_multi_uid(): def test_cancelled_instance(): orig_event_str = _get_text('event_rrule_recuid_cancelled') event = Event.fromString(orig_event_str, ref='1405314000', **EVENT_KWARGS) - assert event.format(SEARCH_FORMAT, dt.date(2014, 7, 14)) == \ + assert SEARCH_FORMATTER(event.format(dt.date(2014, 7, 14))) == \ 'CANCELLED 14.07.2014 07:00-12:00 Arbeit ⟳\x1b[0m' event = Event.fromString(orig_event_str, ref='PROTO', **EVENT_KWARGS) - assert event.format(SEARCH_FORMAT, dt.date(2014, 7, 14)) == \ + assert SEARCH_FORMATTER(event.format(dt.date(2014, 7, 14))) == \ '30.06.2014 07:00-12:00 Arbeit ⟳\x1b[0m' @@ -487,7 +494,8 @@ def test_format_24(): event = Event.fromString(event_dt, **EVENT_KWARGS) event.update_start_end(start, end) format_ = '{start-end-time-style} {title}{repeat-symbol}' - assert event.format(format_, dt.date(2014, 4, 9)) == '19:30-24:00 An Event\x1b[0m' + assert human_formatter(format_)(event.format(dt.date(2014, 4, 9)) + ) == '19:30-24:00 An Event\x1b[0m' def test_invalid_format_string(): @@ -495,14 +503,15 @@ def test_invalid_format_string(): event = Event.fromString(event_dt, **EVENT_KWARGS) format_ = '{start-end-time-style} {title}{foo}' with pytest.raises(KeyError): - event.format(format_, dt.date(2014, 4, 9)) + human_formatter(format_)(event.format(dt.date(2014, 4, 9))) def test_format_colors(): event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) format_ = '{red}{title}{reset}' - assert event.format(format_, dt.date(2014, 4, 9)) == '\x1b[31mAn Event\x1b[0m\x1b[0m' - assert event.format(format_, dt.date(2014, 4, 9), colors=False) == 'An Event' + formatter = human_formatter(format_) + assert formatter(event.format(dt.date(2014, 4, 9))) == '\x1b[31mAn Event\x1b[0m\x1b[0m' + assert formatter(event.format(dt.date(2014, 4, 9), colors=False)) == 'An Event' def test_event_alarm(): From c7dc9fa4a710469322a39a01989d6f3548b46628 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Fri, 7 May 2021 21:52:12 +0900 Subject: [PATCH 09/35] added colors param to human formatter --- khal/controllers.py | 6 ++++-- tests/event_test.py | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/khal/controllers.py b/khal/controllers.py index 932207a96..21cbaf396 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -74,14 +74,16 @@ def format_day(day: dt.date, format_string: str, locale, attributes=None): raise KeyError("cannot format day with: %s" % format_string) -def human_formatter(format_string, width=None): +def human_formatter(format_string, width=None, colors=True): def fmt(rows): single = type(rows) == dict if single: rows = [rows] results = [] for row in rows: - s = format_string.format(**row) + style('', reset=True) + s = format_string.format(**row) + if colors: + s += style('', reset=True) if width: results += utils.color_wrap(s, width) else: diff --git a/tests/event_test.py b/tests/event_test.py index 01324de64..7a9bd8626 100644 --- a/tests/event_test.py +++ b/tests/event_test.py @@ -509,9 +509,10 @@ def test_invalid_format_string(): def test_format_colors(): event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) format_ = '{red}{title}{reset}' - formatter = human_formatter(format_) - assert formatter(event.format(dt.date(2014, 4, 9))) == '\x1b[31mAn Event\x1b[0m\x1b[0m' - assert formatter(event.format(dt.date(2014, 4, 9), colors=False)) == 'An Event' + assert human_formatter(format_)(event.format(dt.date(2014, 4, 9)) + ) == '\x1b[31mAn Event\x1b[0m\x1b[0m' + assert human_formatter(format_, colors=False)( + event.format(dt.date(2014, 4, 9), colors=False)) == 'An Event' def test_event_alarm(): From eb0fdfc6e9bd35944691c9599a4fc3df6538d3a1 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Fri, 7 May 2021 21:57:53 +0900 Subject: [PATCH 10/35] fix formatter usage in test --- tests/khalendar_test.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/khalendar_test.py b/tests/khalendar_test.py index 2915b62c9..412a37eeb 100644 --- a/tests/khalendar_test.py +++ b/tests/khalendar_test.py @@ -10,6 +10,7 @@ import khal.khalendar.exceptions import khal.utils from khal import icalendar as icalendar_helpers +from khal.controllers import human_formatter from khal.khalendar import CalendarCollection from khal.khalendar.backend import CouldNotCreateDbDir from khal.khalendar.event import Event @@ -314,10 +315,10 @@ def test_search_recurrence_id_only_multi(self, coll_vdirs): coll.insert(event, cal1) events = sorted(coll.search('Event')) assert len(events) == 2 - assert events[0].format( - '{start} {end} {title}', dt.date.today()) == '30.06. 07:30 30.06. 12:00 Arbeit\x1b[0m' - assert events[1].format( - '{start} {end} {title}', dt.date.today()) == '07.07. 08:30 07.07. 12:00 Arbeit\x1b[0m' + assert human_formatter('{start} {end} {title}')(events[0].format( + dt.date.today())) == '30.06. 07:30 30.06. 12:00 Arbeit\x1b[0m' + assert human_formatter('{start} {end} {title}')(events[1].format( + dt.date.today())) == '07.07. 08:30 07.07. 12:00 Arbeit\x1b[0m' def test_delete_two_events(self, coll_vdirs, sleep_time): """testing if we can delete any of two events in two different @@ -374,7 +375,7 @@ def test_invalid_timezones(self, coll_vdirs): coll.insert(event, cal1) events = sorted(coll.search('Event')) assert len(events) == 1 - assert events[0].format('{start} {end} {title}', dt.date.today()) == \ + assert human_formatter('{start} {end} {title}')(events[0].format(dt.date.today())) == \ '02.12. 08:00 02.12. 09:30 Some event\x1b[0m' def test_multi_uid_vdir(self, coll_vdirs, caplog, fix_caplog, sleep_time): From d875a4d962753bc31b77f29aaee27515bd4bb8e3 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 15:51:47 +0900 Subject: [PATCH 11/35] added cli tests --- khal/cli.py | 6 ++++-- tests/cli_test.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/khal/cli.py b/khal/cli.py index aaaa60e72..904c7a5cc 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -313,11 +313,12 @@ def calendar(ctx, include_calendar, exclude_calendar, daterange, once, ) @click.option('--notstarted', help=('Print only events that have not started.'), is_flag=True) + @click.option('--json', help=("Fields to output in json"), multiple=True) @click.argument('DATERANGE', nargs=-1, required=False, metavar='[DATETIME [DATETIME | RANGE]]') @click.pass_context def klist(ctx, include_calendar, exclude_calendar, - daterange, once, notstarted, format, day_format): + daterange, once, notstarted, json, format, day_format): """List all events between a start (default: today) and (optional) end datetime.""" try: @@ -332,7 +333,8 @@ def klist(ctx, include_calendar, exclude_calendar, once=once, notstarted=notstarted, conf=ctx.obj['conf'], - env={"calendars": ctx.obj['conf']['calendars']} + env={"calendars": ctx.obj['conf']['calendars']}, + json=json ) if event_column: click.echo('\n'.join(event_column)) diff --git a/tests/cli_test.py b/tests/cli_test.py index f2b827103..870397a56 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -387,6 +387,19 @@ def test_at(runner): assert result.output.startswith('myevent') +def test_at_json(runner): + runner = runner(days=2) + now = dt.datetime.now().strftime('%d.%m.%Y') + end_date = dt.datetime.now() + dt.timedelta(days=10) + result = runner.invoke( + main_khal, + 'new {} {} 18:00 myevent'.format(now, end_date.strftime('%d.%m.%Y')).split()) + args = ['--color', 'at', '--json', 'start-time', '--json', 'title', '18:30'] + result = runner.invoke(main_khal, args) + assert not result.exception + assert result.output.startswith('[{"start-time": "", "title": "myevent"}]') + + def test_at_day_format(runner): runner = runner(days=2) now = dt.datetime.now().strftime('%d.%m.%Y') @@ -414,6 +427,20 @@ def test_list(runner): assert result.output.startswith(expected) +def test_list_json(runner): + runner = runner(days=2) + now = dt.datetime.now().strftime('%d.%m.%Y') + result = runner.invoke( + main_khal, + 'new {} 18:00 myevent'.format(now).split()) + args = ['list', '--json', 'start-end-time-style', + '--json', 'title', '--json', 'description', '18:30'] + result = runner.invoke(main_khal, args) + expected = '[{"start-end-time-style": "18:00-19:00", "title": "myevent", "description": ""}]' + assert not result.exception + assert result.output.startswith(expected) + + def test_search(runner): runner = runner(days=2) now = dt.datetime.now().strftime('%d.%m.%Y') From 245c1051c41ba4ef8ddfaaca95d3ee0576431def Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 16:12:30 +0900 Subject: [PATCH 12/35] added json option to search --- khal/cli.py | 10 ++++++++-- khal/controllers.py | 9 ++++++++- tests/cli_test.py | 10 ++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/khal/cli.py b/khal/cli.py index 904c7a5cc..9de746ed0 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -34,6 +34,7 @@ from .exceptions import FatalError from .settings import InvalidSettingsError, NoConfigFile, get_config from .terminal import colored +from .controllers import (human_formatter, json_formatter) try: from setproctitle import setproctitle @@ -585,9 +586,10 @@ def printics(ctx, ics, format): @multi_calendar_option @click.option('--format', '-f', help=('The format of the events.')) + @click.option('--json', help=("Fields to output in json"), multiple=True) @click.argument('search_string') @click.pass_context - def search(ctx, format, search_string, include_calendar, exclude_calendar): + def search(ctx, format, json, search_string, include_calendar, exclude_calendar): '''Search for events matching SEARCH_STRING. For recurring events, only the master event and different overwritten @@ -606,8 +608,12 @@ def search(ctx, format, search_string, include_calendar, exclude_calendar): term_width, _ = get_terminal_size() now = dt.datetime.now() env = {"calendars": ctx.obj['conf']['calendars']} + if len(json) == 0: + formatter = human_formatter(format) + else: + formatter = json_formatter(json) for event in events: - desc = textwrap.wrap(controllers.human_formatter(format)( + desc = textwrap.wrap(formatter( event.format(relative_to=now, env=env)), term_width) event_column.extend( [colored(d, event.color, diff --git a/khal/controllers.py b/khal/controllers.py index 21cbaf396..97ed3fe28 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -97,9 +97,16 @@ def fmt(rows): def json_formatter(fields): def fmt(rows): - return [json.dumps( + single = type(rows) == dict + if single: + rows = [rows] + results = [json.dumps( [dict(filter(lambda e: e[0] in fields, row.items())) for row in rows], ensure_ascii=False)] + if single: + return results[0] + else: + return results return fmt diff --git a/tests/cli_test.py b/tests/cli_test.py index 870397a56..a82aa1083 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -451,6 +451,16 @@ def test_search(runner): assert result.output.startswith('\x1b[34m\x1b[31m18:00') +def test_search_json(runner): + runner = runner(days=2) + now = dt.datetime.now().strftime('%d.%m.%Y') + result = runner.invoke(main_khal, 'new {} 18:00 myevent'.format(now).split()) + result = runner.invoke(main_khal, ['search', '--json', 'start-end-time-style', + '--json', 'title', '--json', 'description', 'myevent']) + assert not result.exception + assert result.output.startswith('[{"start-end-time-style": "18:00') + + def test_no_default_new(runner): runner = runner(default_calendar=False) result = runner.invoke(main_khal, 'new 18:00 beer'.split()) From 5062071e800cc1c30f3107d1fbf8b34e21c3a869 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 16:21:40 +0900 Subject: [PATCH 13/35] moved formatters to util --- khal/cli.py | 2 +- khal/controllers.py | 38 +------------------------------------- khal/utils.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/khal/cli.py b/khal/cli.py index 9de746ed0..11adc4fda 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -34,7 +34,7 @@ from .exceptions import FatalError from .settings import InvalidSettingsError, NoConfigFile, get_config from .terminal import colored -from .controllers import (human_formatter, json_formatter) +from .utils import human_formatter, json_formatter try: from setproctitle import setproctitle diff --git a/khal/controllers.py b/khal/controllers.py index 97ed3fe28..aeac9632e 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -25,7 +25,6 @@ import os import re import textwrap -import json from collections import OrderedDict, defaultdict from shutil import get_terminal_size from typing import Callable, Iterable, List, Optional @@ -50,6 +49,7 @@ from .icalendar import sort_key as sort_vevent_key from .khalendar.vdir import Item from .terminal import merge_columns +from .utils import (human_formatter, json_formatter) logger = logging.getLogger('khal') @@ -74,42 +74,6 @@ def format_day(day: dt.date, format_string: str, locale, attributes=None): raise KeyError("cannot format day with: %s" % format_string) -def human_formatter(format_string, width=None, colors=True): - def fmt(rows): - single = type(rows) == dict - if single: - rows = [rows] - results = [] - for row in rows: - s = format_string.format(**row) - if colors: - s += style('', reset=True) - if width: - results += utils.color_wrap(s, width) - else: - results.append(s) - if single: - return results[0] - else: - return results - return fmt - - -def json_formatter(fields): - def fmt(rows): - single = type(rows) == dict - if single: - rows = [rows] - results = [json.dumps( - [dict(filter(lambda e: e[0] in fields, row.items())) for row in rows], - ensure_ascii=False)] - if single: - return results[0] - else: - return results - return fmt - - def calendar( collection: CalendarCollection, agenda_format=None, diff --git a/khal/utils.py b/khal/utils.py index cac1226ef..ed5775b43 100644 --- a/khal/utils.py +++ b/khal/utils.py @@ -23,6 +23,7 @@ import datetime as dt +import json import random import re import string @@ -32,6 +33,7 @@ import pytz import urwid +from click import style def generate_random_uid() -> str: @@ -180,3 +182,39 @@ def relative_timedelta_str(day: dt.date) -> str: def get_wrapped_text(widget: urwid.AttrMap) -> str: return widget.original_widget.get_edit_text() + + +def human_formatter(format_string, width=None, colors=True): + def fmt(rows): + single = type(rows) == dict + if single: + rows = [rows] + results = [] + for row in rows: + s = format_string.format(**row) + if colors: + s += style('', reset=True) + if width: + results += utils.color_wrap(s, width) + else: + results.append(s) + if single: + return results[0] + else: + return results + return fmt + + +def json_formatter(fields): + def fmt(rows): + single = type(rows) == dict + if single: + rows = [rows] + results = [json.dumps( + [dict(filter(lambda e: e[0] in fields, row.items())) for row in rows], + ensure_ascii=False)] + if single: + return results[0] + else: + return results + return fmt From 5955c5d61ad52cbaf984026726d74b4242dca7fc Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 16:54:36 +0900 Subject: [PATCH 14/35] add json to new --- khal/cli.py | 4 +++- khal/controllers.py | 22 ++++++++++++++-------- tests/cli_test.py | 24 ++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/khal/cli.py b/khal/cli.py index 11adc4fda..a0e6a8926 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -361,6 +361,7 @@ def klist(ctx, include_calendar, exclude_calendar, help=('Stop an event repeating on this date.')) @click.option('--format', '-f', help=('The format to print the event.')) + @click.option('--json', help=("Fields to output in json"), multiple=True) @click.option('--alarms', '-m', help=('Alarm times for the new event as DELTAs comma separated')) @click.option('--url', help=("URI for the event.")) @@ -368,7 +369,7 @@ def klist(ctx, include_calendar, exclude_calendar, nargs=-1) @click.pass_context def new(ctx, calendar, info, location, categories, repeat, until, alarms, url, format, - interactive): + json, interactive): '''Create a new event from arguments. START and END can be either dates, times or datetimes, please have a @@ -416,6 +417,7 @@ def new(ctx, calendar, info, location, categories, repeat, until, alarms, url, f alarms=alarms, url=url, format=format, + json=json ) except FatalError as error: logger.debug(error, exc_info=True) diff --git a/khal/controllers.py b/khal/controllers.py index aeac9632e..41401a995 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -175,9 +175,9 @@ def get_events_between( formatter: Callable, notstarted: bool, env: dict, - width: Optional[int], - seen=None, original_start: dt.datetime, + seen=None, + colors: bool = True, ) -> List[str]: """returns a list of events scheduled between start and end. Start and end are strings or datetimes (of some kind). @@ -323,7 +323,7 @@ def khal_list( def new_interactive(collection, calendar_name, conf, info, location=None, categories=None, repeat=None, until=None, alarms=None, - format=None, env=None, url=None): + format=None, json=[], env=None, url=None): info: EventCreationTypes try: info = parse_datetime.eventinfofstr( @@ -390,6 +390,7 @@ def new_interactive(collection, calendar_name, conf, info, location=None, format=format, env=env, calendar_name=calendar_name, + json=json, ) echo("event saved") @@ -399,7 +400,7 @@ def new_interactive(collection, calendar_name, conf, info, location=None, def new_from_string(collection, calendar_name, conf, info, location=None, categories=None, repeat=None, until=None, alarms=None, - url=None, format=None, env=None): + url=None, format=None, json=[], env=None): """construct a new event from a string and add it""" info = parse_datetime.eventinfofstr( info, conf['locale'], @@ -415,7 +416,7 @@ def new_from_string(collection, calendar_name, conf, info, location=None, 'alarms': alarms, 'url': url, }) - new_from_dict(info, collection, conf=conf, format=format, env=env, calendar_name=calendar_name) + new_from_dict(info, collection, conf=conf, format=format, env=env, calendar_name=calendar_name, json=json) def new_from_dict( @@ -425,6 +426,7 @@ def new_from_dict( calendar_name: Optional[str]=None, format=None, env=None, + json=[], ) -> Event: """Create a new event from arguments and save in vdirs @@ -446,9 +448,13 @@ def new_from_dict( ) if conf['default']['print_new'] == 'event': - if format is None: - format = conf['view']['event_format'] - echo(human_formatter(format)(event.format(dt.datetime.now(), env=env))) + if len(json) == 0: + if format is None: + format = conf['view']['event_format'] + formatter = human_formatter(format) + else: + formatter = json_formatter(json) + echo(formatter(event.format(dt.datetime.now(), env=env))) elif conf['default']['print_new'] == 'path': assert event.href path = os.path.join(collection._calendars[event.calendar]['path'], event.href) diff --git a/tests/cli_test.py b/tests/cli_test.py index a82aa1083..121dda0b9 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -875,7 +875,27 @@ def test_new(runner): assert result.output.startswith(str(runner.tmpdir)) -@freeze_time('2015-6-1 8:00') +def test_new_format(runner): + runner = runner(print_new='event') + + format = '{start-end-time-style}: {title}' + result = runner.invoke(main_khal, ['new', '13.03.2016 12:00', '3d', + '--format', format, 'Visit']) + assert not result.exception + assert result.output.startswith('→12:00: Visit') + + +def test_new_json(runner): + runner = runner(print_new='event') + + result = runner.invoke(main_khal, ['new', '13.03.2016 12:00', '3d', + '--json', 'start-end-time-style', '--json', 'title', 'Visit']) + assert not result.exception + assert result.output.startswith( + '[{"start-end-time-style": "→12:00", "title": "Visit"}]') + + +@ freeze_time('2015-6-1 8:00') def test_new_interactive(runner): runner = runner(print_new='path') @@ -896,7 +916,7 @@ def test_debug(runner): assert not result.exception -@freeze_time('2015-6-1 8:00') +@ freeze_time('2015-6-1 8:00') def test_new_interactive_extensive(runner): runner = runner(print_new='path', default_calendar=False) From bf2062b96b15735e21574870381724f8fcc6ce28 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 17:06:47 +0900 Subject: [PATCH 15/35] corrected style --- khal/controllers.py | 5 +++-- khal/utils.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/khal/controllers.py b/khal/controllers.py index 41401a995..7585ff99f 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -32,7 +32,7 @@ import pytz from click import confirm, echo, prompt, style -from khal import __productname__, __version__, calendar_display, parse_datetime, utils +from khal import __productname__, __version__, calendar_display, parse_datetime from khal.custom_types import ( EventCreationTypes, LocaleConfiguration, @@ -41,6 +41,7 @@ ) from khal.exceptions import DateTimeParseError, FatalError from khal.khalendar import CalendarCollection +from khal.exceptions import DateTimeParseError, FatalError from khal.khalendar.event import Event from khal.khalendar.exceptions import DuplicateUid, ReadOnlyCalendarError @@ -49,7 +50,7 @@ from .icalendar import sort_key as sort_vevent_key from .khalendar.vdir import Item from .terminal import merge_columns -from .utils import (human_formatter, json_formatter) +from .utils import human_formatter, json_formatter logger = logging.getLogger('khal') diff --git a/khal/utils.py b/khal/utils.py index ed5775b43..4925e924b 100644 --- a/khal/utils.py +++ b/khal/utils.py @@ -195,7 +195,7 @@ def fmt(rows): if colors: s += style('', reset=True) if width: - results += utils.color_wrap(s, width) + results += color_wrap(s, width) else: results.append(s) if single: From bcd3ced8800c103f8802889b6661be0fb9078576 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 20:39:43 +0900 Subject: [PATCH 16/35] added test for human_formatter width --- tests/utils_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/utils_test.py b/tests/utils_test.py index d19f3bede..d9f09bf36 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -1,6 +1,7 @@ """testing functions from the khal.utils""" import datetime as dt +from click import style from freezegun import freeze_time from khal import utils @@ -119,3 +120,9 @@ def test_get_weekday_occurrence(): assert utils.get_weekday_occurrence(dt.date(2017, 5, 8)) == (0, 2) assert utils.get_weekday_occurrence(dt.date(2017, 5, 28)) == (6, 4) assert utils.get_weekday_occurrence(dt.date(2017, 5, 29)) == (0, 5) + + +def test_human_formatter_width(): + formatter = utils.human_formatter('{red}{title}', width=10) + output = formatter({'title': 'morethan10characters', 'red': style('', reset=False, fg='red')}) + assert output.startswith('\x1b[31mmoret\x1b[0m') From e4f0cd10f3c2468a8f1f2feee14e5015dfe91dee Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 20:57:00 +0900 Subject: [PATCH 17/35] update method docs --- khal/controllers.py | 5 +++-- khal/utils.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/khal/controllers.py b/khal/controllers.py index 7585ff99f..e4a361488 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -187,12 +187,13 @@ def get_events_between( :param locale: :param start: the start datetime :param end: the end datetime - :param formatter: a function that is used for formatting the event + :param formatter: the formatter (see :class:`.utils.human_formatter`) :param nostarted: True if each event should start after start (instead of be active between start and end) :param env: a collection of "static" values like calendar names and color - :param seen: :param original_start: start datetime to compare against of notstarted is set + :param seen: + :param colors: :returns: a list to be printed as the agenda for the given days """ assert not (notstarted and not original_start) diff --git a/khal/utils.py b/khal/utils.py index 4925e924b..05875dd6b 100644 --- a/khal/utils.py +++ b/khal/utils.py @@ -185,6 +185,7 @@ def get_wrapped_text(widget: urwid.AttrMap) -> str: def human_formatter(format_string, width=None, colors=True): + """Create a formatter that formats events to be human readable.""" def fmt(rows): single = type(rows) == dict if single: @@ -206,6 +207,7 @@ def fmt(rows): def json_formatter(fields): + """Create a formatter that formats events in JSON.""" def fmt(rows): single = type(rows) == dict if single: From 747a0c2f1c3b1a56a3019fe7a8438a47c517dbad Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 21:15:58 +0900 Subject: [PATCH 18/35] update usage doc --- doc/source/usage.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/source/usage.rst b/doc/source/usage.rst index f1ffa9f05..3a251b4fa 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -191,6 +191,13 @@ Several options are common to almost all of :program:`khal`'s commands khal list --format "{title} {description}" +.. option:: --json FIELD ... + + Works similar to :option:`--format`, but instead of defining a format string a JSON + object is created for each specified field. The matching events are collected into + a JSON array. + + .. option:: --day-format DAYFORMAT works similar to :option:`--format`, but for day headings. It only has a few @@ -231,8 +238,9 @@ shows all events scheduled for a given date (or datetime) range, with custom formatting: :: - khal list [-a CALENDAR ... | -d CALENDAR ...] [--format FORMAT] - [--day-format DAYFORMAT] [--once] [--notstarted] [START [END | DELTA] ] + khal list [-a CALENDAR ... | -d CALENDAR ...] + [--format FORMAT] [--json FIELD ...] [--day-format DAYFORMAT] + [--once] [--notstarted] [START [END | DELTA] ] START and END can both be given as dates, datetimes or times (it is assumed today is meant in the case of only a given time) in the formats configured in @@ -270,7 +278,8 @@ start. :: - khal at [-a CALENDAR ... | -d CALENDAR ...] [--format FORMAT] + khal at [-a CALENDAR ... | -d CALENDAR ...] + [--format FORMAT] [--json FIELD ...] [--notstarted] [[START DATE] TIME | now] calendar From 02ebf1f77e1cf63713a7ea89640ca79d4d788968 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 21:22:57 +0900 Subject: [PATCH 19/35] add json feature to changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fdf730a31..c9dfc4f61 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,7 @@ not released yet * NEW properties of ikhal themes (dark and light) can now be overriden from the config file (via the new [palette] section, check the documenation) * NEW timedelta strings can now have a leading `+`, e.g. `+1d` +* NEW Add `--json` option to output event data as JSON objects 0.11.2 ====== From 83cd781945c67237eb1b377505b3c35153f751f3 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 21:36:07 +0900 Subject: [PATCH 20/35] rename event.format to event.attributes --- khal/cli.py | 2 +- khal/controllers.py | 12 ++--- tests/event_test.py | 106 +++++++++++++++++++++------------------- tests/khalendar_test.py | 6 +-- 4 files changed, 65 insertions(+), 61 deletions(-) diff --git a/khal/cli.py b/khal/cli.py index a0e6a8926..ec6cbac94 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -616,7 +616,7 @@ def search(ctx, format, json, search_string, include_calendar, exclude_calendar) formatter = json_formatter(json) for event in events: desc = textwrap.wrap(formatter( - event.format(relative_to=now, env=env)), term_width) + event.attributes(relative_to=now, env=env)), term_width) event_column.extend( [colored(d, event.color, bold_for_light_color=ctx.obj['conf']['view']['bold_for_light_color']) diff --git a/khal/controllers.py b/khal/controllers.py index e4a361488..d42cdf2ed 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -224,7 +224,7 @@ def get_events_between( continue try: - event_attributes = event.format(relative_to=(start, end), env=env, colors=colors) + event_attributes = event.attributes(relative_to=(start, end), env=env, colors=colors) except KeyError as error: raise FatalError(error) @@ -456,7 +456,7 @@ def new_from_dict( formatter = human_formatter(format) else: formatter = json_formatter(json) - echo(formatter(event.format(dt.datetime.now(), env=env))) + echo(formatter(event.attributes(dt.datetime.now(), env=env))) elif conf['default']['print_new'] == 'path': assert event.href path = os.path.join(collection._calendars[event.calendar]['path'], event.href) @@ -521,7 +521,7 @@ def edit_event(event, collection, locale, allow_quit=False, width=80): collection.delete(event.href, event.etag, event.calendar) return True elif choice == "datetime range": - current = human_formatter("{start} {end}")(event.format(relative_to=now)) + current = human_formatter("{start} {end}")(event.attributes(relative_to=now)) value = prompt("datetime range", default=current) try: start, end, allday = parse_datetime.guessrangefstr(ansi.sub('', value), locale) @@ -606,7 +606,7 @@ def edit(collection, search_string, locale, format=None, allow_past=False, conf= elif not event.allday and event.end_local < now: continue event_text = textwrap.wrap(human_formatter(format)( - event.format(relative_to=now)), term_width) + event.attributes(relative_to=now)), term_width) echo(''.join(event_text)) if not edit_event(event, collection, locale, allow_quit=True, width=term_width): return @@ -658,7 +658,7 @@ def import_event(vevent, collection, locale, batch, format=None, env=None): if item.name == 'VEVENT': event = Event.fromVEvents( [item], calendar=collection.default_calendar_name, locale=locale) - echo(human_formatter(format)(event.format(dt.datetime.now(), env=env))) + echo(human_formatter(format)(event.attributes(dt.datetime.now(), env=env))) # get the calendar to insert into if not collection.writable_names: @@ -715,4 +715,4 @@ def print_ics(conf, name, ics, format): echo(f'{len(vevents)} events found in {name}') for sub_event in vevents: event = Event.fromVEvents(sub_event, locale=conf['locale']) - echo(human_formatter(format)(event.format(dt.datetime.now()))) + echo(human_formatter(format)(event.attributes(dt.datetime.now()))) diff --git a/tests/event_test.py b/tests/event_test.py index 7a9bd8626..585e25a50 100644 --- a/tests/event_test.py +++ b/tests/event_test.py @@ -29,8 +29,9 @@ LIST_FORMATTER = human_formatter(LIST_FORMAT) SEARCH_FORMAT = '{calendar-color}{cancelled}{start-long}{to-style}' + \ '{end-necessary-long} {title}{repeat-symbol}' -FORMAT_CALENDAR = ('{calendar-color}{cancelled}{start-end-time-style} ({calendar}) ' +CALENDAR_FORMAT = ('{calendar-color}{cancelled}{start-end-time-style} ({calendar}) ' '{title} [{location}]{repeat-symbol}') +CALENDAR_FORMATTER = human_formatter(CALENDAR_FORMAT) SEARCH_FORMATTER = human_formatter(SEARCH_FORMAT) @@ -54,9 +55,9 @@ def test_raw_dt(): normalize_component(_get_text('event_dt_simple_inkl_vtimezone')) event = Event.fromString(event_dt, **EVENT_KWARGS) - assert LIST_FORMATTER(event.format( + assert LIST_FORMATTER(event.attributes( dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-10:30 An Event\x1b[0m' assert event.recurring is False assert event.duration == dt.timedelta(hours=1) @@ -73,7 +74,7 @@ def test_calendar_in_format(): start = BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) end = BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) event = Event.fromString(event_dt, start=start, end=end, **EVENT_KWARGS) - assert event.format(FORMAT_CALENDAR, dt.date(2014, 4, 9)) == \ + assert CALENDAR_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ '09:30-10:30 (foobar) An Event []\x1b[0m' @@ -103,7 +104,7 @@ def test_no_end(): event = Event.fromString(_get_text('event_dt_no_end'), **EVENT_KWARGS) # TODO make sure the event also gets converted to an all day event, as we # usually do - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 12))) == \ + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == \ '16.01.2016 08:00-17.01.2016 08:00 Test\x1b[0m' @@ -154,8 +155,8 @@ def test_raw_d(): event_d = _get_text('event_d') event = Event.fromString(event_d, **EVENT_KWARGS) assert event.raw.split('\r\n') == _get_text('cal_d').split('\n') - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == ' An Event\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09.04.2014 An Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == ' An Event\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09.04.2014 An Event\x1b[0m' def test_update_sequence(): @@ -181,8 +182,8 @@ def test_transform_event(): end = BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) event.update_start_end(start, end) assert isinstance(event, LocalizedEvent) - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-10:30 An Event\x1b[0m' analog_event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) assert normalize_component(event.raw) == normalize_component(analog_event.raw) @@ -195,10 +196,10 @@ def test_update_event_d(): event_d = _get_text('event_d') event = Event.fromString(event_d, **EVENT_KWARGS) event.update_start_end(dt.date(2014, 4, 20), dt.date(2014, 4, 22)) - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 20))) == '↦ An Event\x1b[0m' - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 21))) == '↔ An Event\x1b[0m' - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 22))) == '⇥ An Event\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 20))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 20))) == '↦ An Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 21))) == '↔ An Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 22))) == '⇥ An Event\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 20))) == \ '20.04.2014-22.04.2014 An Event\x1b[0m' assert 'DTSTART;VALUE=DATE:20140420' in event.raw.split('\r\n') assert 'DTEND;VALUE=DATE:20140423' in event.raw.split('\r\n') @@ -230,8 +231,8 @@ def test_dt_two_tz(): # local (Berlin) time! assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end_local == BERLIN.localize(dt.datetime(2014, 4, 9, 16, 30)) - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30-16:30 An Event\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30-16:30 An Event\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-16:30 An Event\x1b[0m' @@ -241,10 +242,11 @@ def test_event_dt_duration(): event = Event.fromString(event_dt_duration, **EVENT_KWARGS) assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-10:30 An Event\x1b[0m' - assert human_formatter('{duration}')(event.format(relative_to=dt.date.today())) == '1h\x1b[0m' + assert human_formatter('{duration}')(event.attributes( + relative_to=dt.date.today())) == '1h\x1b[0m' def test_event_dt_floating(): @@ -252,9 +254,10 @@ def test_event_dt_floating(): event_str = _get_text('event_dt_floating') event = Event.fromString(event_str, **EVENT_KWARGS) assert isinstance(event, FloatingEvent) - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' - assert human_formatter('{duration}')(event.format(relative_to=dt.date.today())) == '1h\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30-10:30 An Event\x1b[0m' + assert human_formatter('{duration}')(event.attributes( + relative_to=dt.date.today())) == '1h\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-10:30 An Event\x1b[0m' assert event.start == dt.datetime(2014, 4, 9, 9, 30) assert event.end == dt.datetime(2014, 4, 9, 10, 30) @@ -276,7 +279,8 @@ def test_event_dt_tz_missing(): assert event.end == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) assert event.start_local == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) assert event.end_local == BERLIN.localize(dt.datetime(2014, 4, 9, 10, 30)) - assert human_formatter('{duration}')(event.format(relative_to=dt.date.today())) == '1h\x1b[0m' + assert human_formatter('{duration}')(event.attributes( + relative_to=dt.date.today())) == '1h\x1b[0m' event = Event.fromString(event_str, calendar='foobar', locale=LOCALE_MIXED) assert event.start == BERLIN.localize(dt.datetime(2014, 4, 9, 9, 30)) @@ -290,10 +294,10 @@ def test_event_dt_rr(): event = Event.fromString(event_dt_rr, **EVENT_KWARGS) assert event.recurring is True - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30-10:30 An Event ⟳\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30-10:30 An Event ⟳\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ '09.04.2014 09:30-10:30 An Event ⟳\x1b[0m' - assert human_formatter('{repeat-pattern}')(event.format(dt.date(2014, 4, 9)) + assert human_formatter('{repeat-pattern}')(event.attributes(dt.date(2014, 4, 9)) ) == 'FREQ=DAILY;COUNT=10\x1b[0m' @@ -301,18 +305,18 @@ def test_event_d_rr(): event_d_rr = _get_text('event_d_rr') event = Event.fromString(event_d_rr, **EVENT_KWARGS) assert event.recurring is True - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == ' Another Event ⟳\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 9))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == ' Another Event ⟳\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == \ '09.04.2014 Another Event ⟳\x1b[0m' - assert human_formatter('{repeat-pattern}')(event.format(dt.date(2014, 4, 9)) + assert human_formatter('{repeat-pattern}')(event.attributes(dt.date(2014, 4, 9)) ) == 'FREQ=DAILY;COUNT=10\x1b[0m' start = dt.date(2014, 4, 10) end = dt.date(2014, 4, 11) event = Event.fromString(event_d_rr, start=start, end=end, **EVENT_KWARGS) assert event.recurring is True - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 10))) == ' Another Event ⟳\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 10))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == ' Another Event ⟳\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == \ '10.04.2014 Another Event ⟳\x1b[0m' @@ -325,13 +329,13 @@ def test_event_rd(): def test_event_d_long(): event_d_long = _get_text('event_d_long') event = Event.fromString(event_d_long, **EVENT_KWARGS) - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '↦ Another Event\x1b[0m' - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 10))) == '↔ Another Event\x1b[0m' - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 11))) == '⇥ Another Event\x1b[0m' - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 12))) == ' Another Event\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 16))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '↦ Another Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == '↔ Another Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 11))) == '⇥ Another Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == ' Another Event\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 16))) == \ '09.04.2014-11.04.2014 Another Event\x1b[0m' - assert human_formatter('{duration}')(event.format( + assert human_formatter('{duration}')(event.attributes( relative_to=dt.date(2014, 4, 11))) == '3d\x1b[0m' @@ -339,10 +343,10 @@ def test_event_d_two_days(): event_d_long = _get_text('event_d_long') event = Event.fromString(event_d_long, **EVENT_KWARGS) event.update_start_end(dt.date(2014, 4, 9), dt.date(2014, 4, 10)) - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '↦ Another Event\x1b[0m' - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 10))) == '⇥ Another Event\x1b[0m' - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 12))) == ' Another Event\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 10))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '↦ Another Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == '⇥ Another Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == ' Another Event\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == \ '09.04.2014-10.04.2014 Another Event\x1b[0m' @@ -350,10 +354,10 @@ def test_event_dt_long(): event_dt_long = _get_text('event_dt_long') event = Event.fromString(event_dt_long, **EVENT_KWARGS) - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 9))) == '09:30→ An Event\x1b[0m' - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 10))) == '↔ An Event\x1b[0m' - assert LIST_FORMATTER(event.format(dt.date(2014, 4, 12))) == '→10:30 An Event\x1b[0m' - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 10))) == \ + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 9))) == '09:30→ An Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == '↔ An Event\x1b[0m' + assert LIST_FORMATTER(event.attributes(dt.date(2014, 4, 12))) == '→10:30 An Event\x1b[0m' + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == \ '09.04.2014 09:30-12.04.2014 10:30 An Event\x1b[0m' @@ -375,7 +379,7 @@ def test_event_no_dst(): ) assert normalize_component(event.raw) == normalize_component(cal_no_dst) - assert SEARCH_FORMATTER(event.format(dt.date(2014, 4, 10))) == \ + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 4, 10))) == \ '09.04.2014 09:30-10:30 An Event\x1b[0m' @@ -422,10 +426,10 @@ def test_multi_uid(): def test_cancelled_instance(): orig_event_str = _get_text('event_rrule_recuid_cancelled') event = Event.fromString(orig_event_str, ref='1405314000', **EVENT_KWARGS) - assert SEARCH_FORMATTER(event.format(dt.date(2014, 7, 14))) == \ + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 7, 14))) == \ 'CANCELLED 14.07.2014 07:00-12:00 Arbeit ⟳\x1b[0m' event = Event.fromString(orig_event_str, ref='PROTO', **EVENT_KWARGS) - assert SEARCH_FORMATTER(event.format(dt.date(2014, 7, 14))) == \ + assert SEARCH_FORMATTER(event.attributes(dt.date(2014, 7, 14))) == \ '30.06.2014 07:00-12:00 Arbeit ⟳\x1b[0m' @@ -494,7 +498,7 @@ def test_format_24(): event = Event.fromString(event_dt, **EVENT_KWARGS) event.update_start_end(start, end) format_ = '{start-end-time-style} {title}{repeat-symbol}' - assert human_formatter(format_)(event.format(dt.date(2014, 4, 9)) + assert human_formatter(format_)(event.attributes(dt.date(2014, 4, 9)) ) == '19:30-24:00 An Event\x1b[0m' @@ -503,16 +507,16 @@ def test_invalid_format_string(): event = Event.fromString(event_dt, **EVENT_KWARGS) format_ = '{start-end-time-style} {title}{foo}' with pytest.raises(KeyError): - human_formatter(format_)(event.format(dt.date(2014, 4, 9))) + human_formatter(format_)(event.attributes(dt.date(2014, 4, 9))) def test_format_colors(): event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) format_ = '{red}{title}{reset}' - assert human_formatter(format_)(event.format(dt.date(2014, 4, 9)) + assert human_formatter(format_)(event.attributes(dt.date(2014, 4, 9)) ) == '\x1b[31mAn Event\x1b[0m\x1b[0m' assert human_formatter(format_, colors=False)( - event.format(dt.date(2014, 4, 9), colors=False)) == 'An Event' + event.attributes(dt.date(2014, 4, 9), colors=False)) == 'An Event' def test_event_alarm(): diff --git a/tests/khalendar_test.py b/tests/khalendar_test.py index 412a37eeb..b09ef152a 100644 --- a/tests/khalendar_test.py +++ b/tests/khalendar_test.py @@ -315,9 +315,9 @@ def test_search_recurrence_id_only_multi(self, coll_vdirs): coll.insert(event, cal1) events = sorted(coll.search('Event')) assert len(events) == 2 - assert human_formatter('{start} {end} {title}')(events[0].format( + assert human_formatter('{start} {end} {title}')(events[0].attributes( dt.date.today())) == '30.06. 07:30 30.06. 12:00 Arbeit\x1b[0m' - assert human_formatter('{start} {end} {title}')(events[1].format( + assert human_formatter('{start} {end} {title}')(events[1].attributes( dt.date.today())) == '07.07. 08:30 07.07. 12:00 Arbeit\x1b[0m' def test_delete_two_events(self, coll_vdirs, sleep_time): @@ -375,7 +375,7 @@ def test_invalid_timezones(self, coll_vdirs): coll.insert(event, cal1) events = sorted(coll.search('Event')) assert len(events) == 1 - assert human_formatter('{start} {end} {title}')(events[0].format(dt.date.today())) == \ + assert human_formatter('{start} {end} {title}')(events[0].attributes(dt.date.today())) == \ '02.12. 08:00 02.12. 09:30 Some event\x1b[0m' def test_multi_uid_vdir(self, coll_vdirs, caplog, fix_caplog, sleep_time): From f3591c63516669b53ace08e7fc8459fc87eb6d8e Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sat, 8 May 2021 23:30:58 +0900 Subject: [PATCH 21/35] added special field formatting --- khal/khalendar/event.py | 3 +-- khal/utils.py | 26 +++++++++++++++++++++++--- tests/cli_test.py | 14 ++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index f9e1e9444..2197aee67 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -38,7 +38,6 @@ from ..exceptions import FatalError from ..icalendar import cal_from_ics, delete_instance, invalid_timezone from ..parse_datetime import timedelta2str -from ..terminal import get_color from ..utils import generate_random_uid, is_aware, to_naive_utc, to_unix_time logger = logging.getLogger('khal') @@ -698,7 +697,7 @@ def attributes(self, relative_to, env=None, colors: bool=True): if "calendars" in env and self.calendar in env["calendars"]: cal = env["calendars"][self.calendar] - attributes["calendar-color"] = get_color(cal.get('color', '')) + attributes["calendar-color"] = cal.get('color', '') attributes["calendar"] = cal.get("displayname", self.calendar) else: attributes["calendar-color"] = attributes["calendar"] = '' diff --git a/khal/utils.py b/khal/utils.py index 05875dd6b..65bb30007 100644 --- a/khal/utils.py +++ b/khal/utils.py @@ -35,6 +35,8 @@ import urwid from click import style +from .terminal import get_color + def generate_random_uid() -> str: """generate a random uid @@ -192,9 +194,14 @@ def fmt(rows): rows = [rows] results = [] for row in rows: + if 'calendar-color' in row: + row['calendar-color'] = get_color(row['calendar-color']) + s = format_string.format(**row) + if colors: s += style('', reset=True) + if width: results += color_wrap(s, width) else: @@ -212,9 +219,22 @@ def fmt(rows): single = type(rows) == dict if single: rows = [rows] - results = [json.dumps( - [dict(filter(lambda e: e[0] in fields, row.items())) for row in rows], - ensure_ascii=False)] + + filtered = [] + for row in rows: + f = dict(filter(lambda e: e[0] in fields, row.items())) + + if f.get('repeat-symbol', '') != '': + f["repeat-symbol"] = f["repeat-symbol"].strip() + if f.get('status', '') != '': + f["status"] = f["status"].strip() + if f.get('cancelled', '') != '': + f["cancelled"] = f["cancelled"].strip() + + filtered.append(f) + + results = [json.dumps(filtered, ensure_ascii=False)] + if single: return results[0] else: diff --git a/tests/cli_test.py b/tests/cli_test.py index 121dda0b9..c9b665039 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -2,6 +2,7 @@ import os import re import sys +import traceback import pytest from click.testing import CliRunner @@ -400,6 +401,19 @@ def test_at_json(runner): assert result.output.startswith('[{"start-time": "", "title": "myevent"}]') +def test_at_json_strip(runner): + runner = runner() + result = runner.invoke(main_khal, ['import', _get_ics_filepath( + 'event_rrule_recuid_cancelled')], input='0\ny\n') + assert not result.exception + result = runner.invoke(main_khal, ['at', '--json', 'repeat-symbol', + '--json', 'status', '--json', 'cancelled', '14.07.2014', '07:00']) + traceback.print_tb(result.exc_info[2]) + assert not result.exception + assert result.output.startswith( + '[{"repeat-symbol": "⟳", "status": "CANCELLED", "cancelled": "CANCELLED"}]') + + def test_at_day_format(runner): runner = runner(days=2) now = dt.datetime.now().strftime('%d.%m.%Y') From 4ec1680b41701d0f2a1485f0ef4d219fb5234f89 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sun, 9 May 2021 11:58:24 +0900 Subject: [PATCH 22/35] added missing formatter --- khal/ui/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/khal/ui/__init__.py b/khal/ui/__init__.py index 806501b43..795f8b15d 100644 --- a/khal/ui/__init__.py +++ b/khal/ui/__init__.py @@ -205,13 +205,14 @@ def set_title(self, mark: str=' ') -> None: format_ = self._conf['view']['agenda_event_format'] else: format_ = self._conf['view']['event_format'] + formatter_ = utils.human_formatter(format_, colors=False) if self.this_date: date_ = self.this_date elif self.event.allday: date_ = self.event.start else: date_ = self.event.start.date() - text = self.event.format(format_, date_, colors=False) + text = formatter_(self.event.attributes(date_, colors=False)) if self._conf['locale']['unicode_symbols']: newline = ' \N{LEFTWARDS ARROW WITH HOOK} ' else: From 0154b23283e6c191ba7910afcf58bb5276ee91e5 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Sun, 9 May 2021 16:04:02 +0900 Subject: [PATCH 23/35] add self to authors :) --- AUTHORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index 6e4e7377e..e417da915 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -54,3 +54,4 @@ Raúl Medina - raulmgcontact [at] gmail (dot] com Matthew Rademaker - matthew.rademaker [at] gmail [dot] com Valentin Iovene - val [at] too [dot] gy Julian Wollrath +Mattori Birnbaum - me [at] mattori [dot] com - https://mattori.com From 1ad1b754e19b50c7f71b445f551d0d47e69e9cb0 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Mon, 10 May 2021 19:54:54 +0900 Subject: [PATCH 24/35] add list of content attributes --- khal/utils.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/khal/utils.py b/khal/utils.py index 65bb30007..f875f8d28 100644 --- a/khal/utils.py +++ b/khal/utils.py @@ -213,6 +213,16 @@ def fmt(rows): return fmt +CONTENT_ATTRIBUTES = ['start', 'start-long', 'start-date', 'start-date-long', + 'start-time', 'end', 'end-long', 'end-date', 'end-date-long', 'end-time', + 'duration', 'start-full', 'start-long-full', 'start-date-full', 'start-date-long-full', + 'start-time-full', 'end-full', 'end-long-full', 'end-date-full', 'end-date-long-full', 'end-time-full', + 'duration-full', 'start-style', 'end-style', 'to-style', 'start-end-time-style', + 'end-necessary', 'end-necessary-long', 'repeat-symbol', 'repeat-pattern', 'title', + 'organizer', 'description', 'location', 'all-day', 'categories', 'uid', 'url', 'calendar', 'calendar-color', + 'status', 'cancelled'] + + def json_formatter(fields): """Create a formatter that formats events in JSON.""" def fmt(rows): @@ -222,7 +232,7 @@ def fmt(rows): filtered = [] for row in rows: - f = dict(filter(lambda e: e[0] in fields, row.items())) + f = dict(filter(lambda e: e[0] in fields and e[0] in CONTENT_ATTRIBUTES, row.items())) if f.get('repeat-symbol', '') != '': f["repeat-symbol"] = f["repeat-symbol"].strip() From 63d7ffb595f6b7ea30521d52b23ebf2a758f6853 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Mon, 10 May 2021 20:07:31 +0900 Subject: [PATCH 25/35] added all fields option for json --- khal/utils.py | 4 ++++ tests/cli_test.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/khal/utils.py b/khal/utils.py index f875f8d28..4c83557b8 100644 --- a/khal/utils.py +++ b/khal/utils.py @@ -225,6 +225,10 @@ def fmt(rows): def json_formatter(fields): """Create a formatter that formats events in JSON.""" + + if len(fields) == 1 and fields[0] == 'all': + fields = CONTENT_ATTRIBUTES + def fmt(rows): single = type(rows) == dict if single: diff --git a/tests/cli_test.py b/tests/cli_test.py index c9b665039..c7f9c3100 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -1,4 +1,5 @@ import datetime as dt +import json import os import re import sys @@ -9,6 +10,7 @@ from freezegun import freeze_time from khal.cli import main_ikhal, main_khal +from khal.utils import CONTENT_ATTRIBUTES from .utils import _get_ics_filepath, _get_text @@ -401,6 +403,20 @@ def test_at_json(runner): assert result.output.startswith('[{"start-time": "", "title": "myevent"}]') +def test_at_json_default_fields(runner): + runner = runner(days=2) + now = dt.datetime.now().strftime('%d.%m.%Y') + end_date = dt.datetime.now() + dt.timedelta(days=10) + result = runner.invoke( + main_khal, + 'new {} {} 18:00 myevent'.format(now, end_date.strftime('%d.%m.%Y')).split()) + args = ['--color', 'at', '--json', 'all', '18:30'] + result = runner.invoke(main_khal, args) + assert not result.exception + output_fields = json.loads(result.output)[0].keys() + assert all(x in output_fields for x in CONTENT_ATTRIBUTES) + + def test_at_json_strip(runner): runner = runner() result = runner.invoke(main_khal, ['import', _get_ics_filepath( From feacba07569f8dc8bef672a2a6ef676771bf519b Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Mon, 10 May 2021 20:13:56 +0900 Subject: [PATCH 26/35] fix long lines --- khal/utils.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/khal/utils.py b/khal/utils.py index 4c83557b8..fae820b20 100644 --- a/khal/utils.py +++ b/khal/utils.py @@ -215,12 +215,13 @@ def fmt(rows): CONTENT_ATTRIBUTES = ['start', 'start-long', 'start-date', 'start-date-long', 'start-time', 'end', 'end-long', 'end-date', 'end-date-long', 'end-time', - 'duration', 'start-full', 'start-long-full', 'start-date-full', 'start-date-long-full', - 'start-time-full', 'end-full', 'end-long-full', 'end-date-full', 'end-date-long-full', 'end-time-full', - 'duration-full', 'start-style', 'end-style', 'to-style', 'start-end-time-style', - 'end-necessary', 'end-necessary-long', 'repeat-symbol', 'repeat-pattern', 'title', - 'organizer', 'description', 'location', 'all-day', 'categories', 'uid', 'url', 'calendar', 'calendar-color', - 'status', 'cancelled'] + 'duration', 'start-full', 'start-long-full', 'start-date-full', + 'start-date-long-full', 'start-time-full', 'end-full', 'end-long-full', + 'end-date-full', 'end-date-long-full', 'end-time-full', 'duration-full', + 'start-style', 'end-style', 'to-style', 'start-end-time-style', + 'end-necessary', 'end-necessary-long', 'repeat-symbol', 'repeat-pattern', + 'title', 'organizer', 'description', 'location', 'all-day', 'categories', + 'uid', 'url', 'calendar', 'calendar-color', 'status', 'cancelled'] def json_formatter(fields): From fbe4c89d3c6eb90e33b392bffe0ccb37ca389b57 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Tue, 11 May 2021 20:01:14 +0900 Subject: [PATCH 27/35] fix formatter mistakes --- tests/cli_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cli_test.py b/tests/cli_test.py index c7f9c3100..efef810c5 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -946,7 +946,7 @@ def test_debug(runner): assert not result.exception -@ freeze_time('2015-6-1 8:00') +@freeze_time('2015-6-1 8:00') def test_new_interactive_extensive(runner): runner = runner(print_new='path', default_calendar=False) From 88801e26fa464113071a9395e58902a04707d939 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Tue, 11 May 2021 20:02:04 +0900 Subject: [PATCH 28/35] remove unnecessary dict call --- khal/khalendar/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index 2197aee67..fe506b234 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -720,7 +720,7 @@ def attributes(self, relative_to, env=None, colors: bool=True): attributes['status'] = self.status + ' ' if self.status else '' attributes['cancelled'] = 'CANCELLED ' if self.status == 'CANCELLED' else '' - return dict(attributes) + return attributes def duplicate(self) -> 'Event': """duplicate this event's PROTO event""" From 44d3282f6264d764384a07c894b0469a3c423112 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Tue, 11 May 2021 20:07:41 +0900 Subject: [PATCH 29/35] remove mutable default arguments --- khal/controllers.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/khal/controllers.py b/khal/controllers.py index d42cdf2ed..e301515fa 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -41,7 +41,6 @@ ) from khal.exceptions import DateTimeParseError, FatalError from khal.khalendar import CalendarCollection -from khal.exceptions import DateTimeParseError, FatalError from khal.khalendar.event import Event from khal.khalendar.exceptions import DuplicateUid, ReadOnlyCalendarError @@ -237,7 +236,7 @@ def get_events_between( def khal_list( collection, - daterange: Optional[List[str]]=None, + daterange: Optional[List[str]] = None, conf: Optional[dict] = None, agenda_format=None, day_format: Optional[str]=None, @@ -246,7 +245,7 @@ def khal_list( width: Optional[int] = None, env=None, datepoint=None, - json: Iterable = [], + json: Optional[List] = None, ): """returns a list of all events in `daterange`""" assert daterange is not None or datepoint is not None @@ -256,12 +255,13 @@ def khal_list( if agenda_format is None: agenda_format = conf['view']['agenda_event_format'] - if len(json) == 0: - formatter = human_formatter(agenda_format, width) - colors = True - else: + json_mode = json is not None and len(json) > 0 + if json_mode: formatter = json_formatter(json) colors = False + else: + formatter = human_formatter(agenda_format, width) + colors = True if daterange is not None: if day_format is None: @@ -313,7 +313,7 @@ def khal_list( seen=once, colors=colors, ) - if day_format and (conf['default']['show_all_days'] or current_events) and len(json) == 0: + if day_format and (conf['default']['show_all_days'] or current_events) and not json_mode: if len(event_column) != 0 and conf['view']['blank_line_before_day']: event_column.append('') event_column.append(format_day(start.date(), day_format, conf['locale'])) @@ -325,7 +325,7 @@ def khal_list( def new_interactive(collection, calendar_name, conf, info, location=None, categories=None, repeat=None, until=None, alarms=None, - format=None, json=[], env=None, url=None): + format=None, json=None, env=None, url=None): info: EventCreationTypes try: info = parse_datetime.eventinfofstr( @@ -402,7 +402,7 @@ def new_interactive(collection, calendar_name, conf, info, location=None, def new_from_string(collection, calendar_name, conf, info, location=None, categories=None, repeat=None, until=None, alarms=None, - url=None, format=None, json=[], env=None): + url=None, format=None, json=None, env=None): """construct a new event from a string and add it""" info = parse_datetime.eventinfofstr( info, conf['locale'], @@ -428,7 +428,7 @@ def new_from_dict( calendar_name: Optional[str]=None, format=None, env=None, - json=[], + json=None, ) -> Event: """Create a new event from arguments and save in vdirs @@ -450,7 +450,7 @@ def new_from_dict( ) if conf['default']['print_new'] == 'event': - if len(json) == 0: + if json is None or len(json) == 0: if format is None: format = conf['view']['event_format'] formatter = human_formatter(format) From d2e39b131c25bf66d1f921af7875c6414ac9b383 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Tue, 11 May 2021 20:26:28 +0900 Subject: [PATCH 30/35] add to json option docs --- doc/source/usage.rst | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 3a251b4fa..a7c802aac 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -195,7 +195,34 @@ Several options are common to almost all of :program:`khal`'s commands Works similar to :option:`--format`, but instead of defining a format string a JSON object is created for each specified field. The matching events are collected into - a JSON array. + a JSON array. This option accepts the following subset of :option:`--format` + template options + + :: + + title, description, uid, start, start-long, start-date, + start-date-long, start-time, end, end-long, end-date, + end-date-long, end-time, start-full, start-long-full, + start-date-full, start-date-long-full, start-time-full, + end-full, end-long-full, end-date-full, end-date-long-full, + end-time-full, repeat-symbol, location, calendar, + calendar-color, start-style, to-style, end-style, + start-end-time-style, end-necessary, end-necessary-long, + status, cancelled, organizer, url, duration, duration-full, + repeat-pattern, all-day, categories + + + Note that `calendar-color` will be the actual color name rather than the ANSI color code, + and the `repeat-symbol`, `status`, and `cancelled` values will have leading/trailing + whitespace stripped. Additionally, if only the special value `all` is specified then + all fields will be enabled. + + Below is an example command which prints a JSON list of objects containing the title and + description of all events today. + + :: + + khal list --json title --json description .. option:: --day-format DAYFORMAT From eb7b1cee26fd06cd2b262c013dc3616572b7cd69 Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Tue, 11 May 2021 20:42:20 +0900 Subject: [PATCH 31/35] added missing format template options --- doc/source/usage.rst | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/doc/source/usage.rst b/doc/source/usage.rst index a7c802aac..2e5392ac7 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -171,8 +171,21 @@ Several options are common to almost all of :program:`khal`'s commands url-separator A separator: " :: " that appears when there is a url. + duration + The duration of the event in terms of days, hours, months, and seconds + (abbreviated to `d`, `h`, `m`, and `s` respectively). + + repeat-pattern + The raw iCal recurrence rule if the event is repeating. + + all-day + A boolean indicating whether it is an all-day event or not. + + categories + The categories of the event. + By default, all-day events have no times. To see a start and end time anyway simply - add `-full` to the end of any template with start/end, for instance + add `-full` to the end of any templates with start/end or duration, for instance `start-time` becomes `start-time-full` and will always show start and end times (instead of being empty for all-day events). From f31faecddb8cfc6be91e3ce8348efba05b99893e Mon Sep 17 00:00:00 2001 From: Mattori Birnbaum Date: Tue, 11 May 2021 20:44:06 +0900 Subject: [PATCH 32/35] typo --- doc/source/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 2e5392ac7..a1c2f67d2 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -185,7 +185,7 @@ Several options are common to almost all of :program:`khal`'s commands The categories of the event. By default, all-day events have no times. To see a start and end time anyway simply - add `-full` to the end of any templates with start/end or duration, for instance + add `-full` to the end of any template with start/end or duration, for instance `start-time` becomes `start-time-full` and will always show start and end times (instead of being empty for all-day events). From f4ca976674961aeb1848b764769eb1b49069f8c1 Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Fri, 27 Oct 2023 21:58:08 +0200 Subject: [PATCH 33/35] cli_test format() --- tests/cli_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cli_test.py b/tests/cli_test.py index efef810c5..5f90dbaaf 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -462,7 +462,7 @@ def test_list_json(runner): now = dt.datetime.now().strftime('%d.%m.%Y') result = runner.invoke( main_khal, - 'new {} 18:00 myevent'.format(now).split()) + f'new {now} 18:00 myevent'.split()) args = ['list', '--json', 'start-end-time-style', '--json', 'title', '--json', 'description', '18:30'] result = runner.invoke(main_khal, args) @@ -484,7 +484,7 @@ def test_search(runner): def test_search_json(runner): runner = runner(days=2) now = dt.datetime.now().strftime('%d.%m.%Y') - result = runner.invoke(main_khal, 'new {} 18:00 myevent'.format(now).split()) + result = runner.invoke(main_khal, f'new {now} 18:00 myevent'.split()) result = runner.invoke(main_khal, ['search', '--json', 'start-end-time-style', '--json', 'title', '--json', 'description', 'myevent']) assert not result.exception From ef55a88cc9a8734dfd1e324eb06b898ae2e089da Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 20:46:23 +0000 Subject: [PATCH 34/35] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- khal/controllers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/khal/controllers.py b/khal/controllers.py index e301515fa..4674aa889 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, Iterable, List, Optional +from typing import Callable, List, Optional import pytz from click import confirm, echo, prompt, style From 1d60f784e8302bc3618203a73726539640608577 Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Fri, 27 Oct 2023 22:49:00 +0200 Subject: [PATCH 35/35] make ruff happy --- khal/controllers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/khal/controllers.py b/khal/controllers.py index 4674aa889..a850b33c8 100644 --- a/khal/controllers.py +++ b/khal/controllers.py @@ -418,7 +418,15 @@ def new_from_string(collection, calendar_name, conf, info, location=None, 'alarms': alarms, 'url': url, }) - new_from_dict(info, collection, conf=conf, format=format, env=env, calendar_name=calendar_name, json=json) + new_from_dict( + info, + collection, + conf=conf, + format=format, + env=env, + calendar_name=calendar_name, + json=json, + ) def new_from_dict(