Repository: pimutils/khal
Pull request #1160 (2022-06-28 15:08:59)

Typing Party II - Revenge of Mypy
Apart from a lot of typing, this contains some light refactoring.

* Some imports were moved around, to have cleaner dependencies
* Some renaming of CalendarCollection's methods to make their intent clearer

This PR leaves 39.6% of the code base imprecise (as reported by mypy) compared to 45.9% before


Patch:
diff --git a/khal/cli.py b/khal/cli.py
index 3447354..3465022 100644
--- a/khal/cli.py
+++ b/khal/cli.py
@@ -32,8 +32,7 @@ import click_log
 
 from . import __version__, controllers, khalendar
 from .exceptions import FatalError
-from .settings import InvalidSettingsError, get_config
-from .settings.exceptions import NoConfigFile
+from .settings import InvalidSettingsError, NoConfigFile, get_config
 from .terminal import colored
 
 try:
diff --git a/khal/configwizard.py b/khal/configwizard.py
index 5254594..87b9e6d 100644
--- a/khal/configwizard.py
+++ b/khal/configwizard.py
@@ -31,7 +31,7 @@ import xdg
 from click import Choice, UsageError, confirm, prompt
 
 from .exceptions import FatalError
-from .settings import settings
+from .settings import find_configuration_file
 
 logger = logging.getLogger('khal')
 
@@ -210,7 +210,7 @@ def create_config(vdirs, dateformat, timeformat, default_calendar=None):
 
 
 def configwizard():
-    config_file = settings.find_configuration_file()
+    config_file = find_configuration_file()
     if config_file is not None:
         logger.fatal(f"Found an existing config file at {config_file}.")
         logger.fatal(
diff --git a/khal/controllers.py b/khal/controllers.py
index 5dd059c..09e1319 100644
--- a/khal/controllers.py
+++ b/khal/controllers.py
@@ -27,13 +27,14 @@ import re
 import textwrap
 from collections import OrderedDict, defaultdict
 from shutil import get_terminal_size
-from typing import Any, Dict, Iterable, List
+from typing import List, Optional
 
 import pytz
 from click import confirm, echo, prompt, style
 
 from khal import (__productname__, __version__, calendar_display,
                   parse_datetime, utils)
+from khal.custom_types import EventCreationTypes, LocaleConfiguration
 from khal.exceptions import DateTimeParseError, FatalError
 from khal.khalendar import CalendarCollection
 from khal.khalendar.event import Event
@@ -41,7 +42,6 @@ from khal.khalendar.exceptions import DuplicateUid, ReadOnlyCalendarError
 
 from .exceptions import ConfigurationError
 from .icalendar import cal_from_ics
-from .icalendar import new_event as new_vevent
 from .icalendar import sort_key as sort_vevent_key
 from .icalendar import split_ics
 from .khalendar.vdir import Item
@@ -132,8 +132,8 @@ def calendar(collection, agenda_format=None, notstarted=False, once=False, dater
 
 
 def start_end_from_daterange(
-    daterange: Iterable[str],
-    locale: Dict[Any, Any],
+    daterange: List[str],
+    locale: LocaleConfiguration,
     default_timedelta_date: dt.timedelta=dt.timedelta(days=1),
     default_timedelta_datetime: dt.timedelta=dt.timedelta(hours=1),
 ):
@@ -225,7 +225,7 @@ def get_events_between(
 
 def khal_list(
     collection,
-    daterange: Iterable[str] = None,
+    daterange: Optional[List[str]]=None,
     conf: dict = None,
     agenda_format=None,
     day_format: str=None,
@@ -304,6 +304,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):
+    info: EventCreationTypes
     try:
         info = parse_datetime.eventinfofstr(
             info, conf['locale'],
@@ -353,12 +354,23 @@ def new_interactive(collection, calendar_name, conf, info, location=None,
     if info['description'] == 'None':
         info['description'] = ''
 
-    event = new_from_args(
-        collection, calendar_name, conf, format=format, env=env,
-        location=location, categories=categories,
-        repeat=repeat, until=until, alarms=alarms, url=url,
-        **info)
-
+    info.update({
+        'location': location,
+        'categories': categories,
+        'repeat': repeat,
+        'until': until,
+        'alarms': alarms,
+        'url': url,
+    })
+
+    event = new_from_dict(
+        info,
+        collection,
+        conf,
+        format=format,
+        env=env,
+        calendar_name=calendar_name,
+    )
     echo("event saved")
 
     term_width, _ = get_terminal_size()
@@ -375,36 +387,42 @@ def new_from_string(collection, calendar_name, conf, info, location=None,
         conf['default']['default_dayevent_duration'],
         adjust_reasonably=True,
     )
-    new_from_args(
-        collection, calendar_name, conf, format=format, env=env,
-        location=location, categories=categories, repeat=repeat,
-        until=until, alarms=alarms, url=url, **info
-    )
-
+    info.update({
+        'location': location,
+        'categories': categories,
+        'repeat': repeat,
+        'until': until,
+        'alarms': alarms,
+        'url': url,
+    })
+    new_from_dict(info, collection, conf=conf, format=format, env=env, calendar_name=calendar_name)
+
+
+def new_from_dict(
+    event_args: EventCreationTypes,
+    collection: CalendarCollection,
+    conf,
+    calendar_name: Optional[str]=None,
+    format=None,
+    env=None,
+) -> Event:
+    """Create a new event from arguments and save in vdirs
 
-def new_from_args(collection, calendar_name, conf, dtstart=None, dtend=None,
-                  summary=None, description=None, allday=None, location=None,
-                  categories=None, repeat=None, until=None, alarms=None,
-                  timezone=None, url=None, format=None, env=None):
-    """Create a new event from arguments and add to vdirs"""
-    if isinstance(categories, str):
-        categories = [category.strip() for category in categories.split(',')]
+    This is a wrapper around CalendarCollection.create_event_from_dict()
+    """
+    if isinstance(event_args['categories'], str):
+        event_args['categories'] = [event_args['categories'].strip()
+                                    for category in event_args['categories'].split(',')]
     try:
-        event = new_vevent(
-            locale=conf['locale'], location=location, categories=categories,
-            repeat=repeat, until=until, alarms=alarms, dtstart=dtstart,
-            dtend=dtend, summary=summary, description=description, timezone=timezone, url=url,
-        )
+        event = collection.create_event_from_dict(event_args, calendar_name=calendar_name)
     except ValueError as error:
         raise FatalError(error)
-    event = Event.fromVEvents(
-        [event], calendar=calendar_name, locale=conf['locale'])
 
     try:
-        collection.new(event)
+        collection.insert(event)
     except ReadOnlyCalendarError:
         raise FatalError(
-            f'ERROR: Cannot modify calendar "{calendar_name}" as it is read-only'
+            f'ERROR: Cannot modify calendar `{calendar_name}` as it is read-only'
         )
 
     if conf['default']['print_new'] == 'event':
@@ -412,10 +430,8 @@ def new_from_args(collection, calendar_name, conf, dtstart=None, dtend=None,
             format = conf['view']['event_format']
         echo(event.format(format, dt.datetime.now(), env=env))
     elif conf['default']['print_new'] == 'path':
-        path = os.path.join(
-            collection._calendars[event.calendar]['path'],
-            event.href
-        )
+        assert event.href
+        path = os.path.join(collection._calendars[event.calendar]['path'], event.href)
         echo(path)
     return event
 
@@ -642,7 +658,7 @@ def import_event(vevent, collection, locale, batch, format=None, env=None):
 
     if batch or confirm(f"Do you want to import this event into `{calendar_name}`?"):
         try:
-            collection.new(Item(vevent), collection=calendar_name)
+            collection.insert(Item(vevent), collection=calendar_name)
         except DuplicateUid:
             if batch or confirm(
                     "An event with the same UID already exists. Do you want to update it?"):
diff --git a/khal/custom_types.py b/khal/custom_types.py
index 2d74c11..509fa65 100644
--- a/khal/custom_types.py
+++ b/khal/custom_types.py
@@ -1,5 +1,6 @@
 import datetime as dt
-from typing import Protocol, Tuple, TypedDict, Union
+import os
+from typing import List, Optional, Protocol, Tuple, TypedDict, Union
 
 import pytz
 
@@ -28,7 +29,7 @@ class LocaleConfiguration(TypedDict):
 
 class SupportsRaw(Protocol):
     @property
-    def uid(self) -> str:
+    def uid(self) -> Optional[str]:
         ...
 
     @property
@@ -46,3 +47,33 @@ EventTuple = Tuple[
     str,
     str,
 ]
+
+
+# Only need for RRuleMapType
+class RRuleMapBase(TypedDict):
+    freq: str
+
+
+class RRuleMapType(RRuleMapBase, total=False):
+    # not required keys go in here
+    # TODO remove if either `NotRequired` is supported by mypy or the oldest
+    # python we support is 3.11 (see PEP 655)
+    until: dt.datetime
+
+
+class EventCreationTypes(TypedDict):
+    dtstart: dt.datetime
+    dtend: dt.datetime
+    summary: str
+    description: str
+    allday: bool
+    location: Optional[str]
+    categories: Optional[Union[str, List[str]]]
+    repeat: Optional[str]
+    until: str
+    alarms: str
+    timezone: pytz.BaseTzInfo
+    url: str
+
+
+PathLike = Union[str, os.PathLike]
diff --git a/khal/icalendar.py b/khal/icalendar.py
index d91b987..bfbec9d 100644
--- a/khal/icalendar.py
+++ b/khal/icalendar.py
@@ -25,6 +25,7 @@ import datetime as dt
 import logging
 from collections import defaultdict
 from hashlib import sha256
+from typing import List, Optional, Tuple, Union
 
 import dateutil.rrule
 import icalendar
@@ -37,14 +38,12 @@ from .utils import generate_random_uid, localize_strip_tz, to_unix_time
 logger = logging.getLogger('khal')
 
 
-def split_ics(ics, random_uid=False, default_timezone=None):
+def split_ics(ics: str, random_uid: bool=False, default_timezone=None):
     """split an ics string into several according to VEVENT's UIDs
 
     and sort the right VTIMEZONEs accordingly
     ignores all other ics components
-    :type ics: str
     :param random_uid: assign random uids to all events
-    :type random_uid: bool
     :rtype list:
     """
     cal = cal_from_ics(ics)
@@ -77,37 +76,33 @@ def split_ics(ics, random_uid=False, default_timezone=None):
             sorted(events_grouped.items())]
 
 
-def new_event(locale, dtstart=None, dtend=None, summary=None, timezone=None,
-              allday=False, description=None, location=None, categories=None,
-              repeat=None, until=None, alarms=None, url=None):
+def new_vevent(locale,
+               dtstart: dt.datetime,
+               dtend: dt.datetime,
+               summary: str,
+               timezone: Optional[pytz.BaseTzInfo]=None,
+               allday: bool=False,
+               description: Optional[str]=None,
+               location: Optional[str]=None,
+               categories: Optional[Union[List[str], str]]=None,
+               repeat: Optional[str]=None,
+               until=None,
+               alarms: Optional[str]=None,
+               url: Optional[str]=None,
+               ) -> icalendar.Event:
     """create a new event
 
     :param dtstart: starttime of that event
-    :type dtstart: datetime
     :param dtend: end time of that event, if this is a *date*, this value is
         interpreted as being the last date the event is scheduled on, i.e.
         the VEVENT DTEND will be *one day later*
-    :type dtend: datetime
     :param summary: description of the event, used in the SUMMARY property
-    :type summary: unicode
     :param timezone: timezone of the event (start and end)
-    :type timezone: pytz.timezone
     :param allday: if set to True, we will not transform dtstart and dtend to
         datetime
-    :type allday: bool
     :param url: url of the event
-    :type url: string
     :returns: event
-    :rtype: icalendar.Event
     """
-
-    if dtstart is None:
-        raise ValueError("no start given")
-    if dtend is None:
-        raise ValueError("no end given")
-    if summary is None:
-        raise ValueError("no summary given")
-
     if not allday and timezone is not None:
         dtstart = timezone.localize(dtstart)
         dtend = timezone.localize(dtend)
@@ -143,13 +138,16 @@ def new_event(locale, dtstart=None, dtend=None, summary=None, timezone=None,
     return event
 
 
-def ics_from_list(events, tzs, random_uid=False, default_timezone=None):
-    """convert an iterable of icalendar.Events to an icalendar.Calendar
+def ics_from_list(
+    events: List[icalendar.Event],
+    tzs,
+    random_uid: bool=False,
+    default_timezone=None
+) -> str:
+    """convert an iterable of icalendar.Events to an icalendar str
 
     :params events: list of events all with the same uid
-    :type events: list(icalendar.cal.Event)
     :param random_uid: assign random uids to all events
-    :type random_uid: bool
     :param tzs: collection of timezones
     :type tzs: dict(icalendar.cal.Vtimzone
     """
@@ -481,12 +479,10 @@ def _get_all_properties(vevent, prop):
     return rdates
 
 
-def delete_instance(vevent, instance):
+def delete_instance(vevent: icalendar.Calendar, instance: dt.datetime) -> None:
     """remove a recurrence instance from a VEVENT's RRDATE list or add it
     to the EXDATE list
 
-    :type vevent: icalendar.cal.Event
-    :type instance: datetime.datetime
     """
     # TODO check where this instance is coming from and only call the
     # appropriate function
@@ -502,7 +498,7 @@ def delete_instance(vevent, instance):
             vevent.add('RDATE', rdates)
 
 
-def sort_key(vevent):
+def sort_key(vevent: icalendar.Event) -> Tuple[str, float]:
     """helper function to determine order of VEVENTS
     so that recurrence-id events come after the corresponding rrule event, etc
     :param vevent: icalendar.Event
@@ -520,7 +516,10 @@ def sort_key(vevent):
         return uid, 1
 
 
-def cal_from_ics(ics):
+def cal_from_ics(ics: str) -> icalendar.Calendar:
+    """
+    :param ics: an icalendar formatted string
+    """
     try:
         cal = icalendar.Calendar.from_ical(ics)
     except ValueError as error:
diff --git a/khal/khalendar/backend.py b/khal/khalendar/backend.py
index 9776a1e..faa5a66 100644
--- a/khal/khalendar/backend.py
+++ b/khal/khalendar/backend.py
@@ -28,7 +28,7 @@ import logging
 import sqlite3
 from enum import IntEnum
 from os import makedirs, path
-from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
+from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
 
 import icalendar
 import icalendar.cal
@@ -91,7 +91,7 @@ class SQLiteDb:
         self._check_table_version()
 
     @contextlib.contextmanager
-    def at_once(self):
+    def at_once(self) -> Iterator['SQLiteDb']:
         assert not self._at_once
         self._at_once = True
         try:
@@ -251,7 +251,7 @@ class SQLiteDb:
         self.sql_ex(sql_s, stuple)
 
     def update_vcf_dates(self, vevent_str: str, href: str, etag: str='',
-                         calendar: str=None) -> None:
+                         calendar: Optional[str]=None) -> None:
         """insert events from a vcard into the db
 
         This is will parse BDAY, ANNIVERSARY, X-ANNIVERSARY and X-ABDATE fields.
@@ -576,7 +576,7 @@ class SQLiteDb:
         return item
 
     def search(self, search_string: str) \
-            -> Iterable[Tuple[str, str, dt.datetime, dt.datetime, str, str, str]]:
+            -> Iterable[Tuple[str, str, dt.date, dt.date, str, str, str]]:
         """search for events matching `search_string`"""
         sql_s = (
             'SELECT item, recs_loc.href, dtstart, dtend, ref, etag, dtype, events.calendar '
@@ -613,7 +613,7 @@ class SQLiteDb:
             yield item, href, start, end, ref, etag, calendar
 
 
-def check_support(vevent: icalendar.cal.Event, href: str, calendar: str):
+def check_support(vevent: icalendar.cal.Event, href: str, calendar: str) -> None:
     """test if all icalendar features used in this event are supported,
     raise `UpdateFailed` otherwise.
     :param vevent: event to test
@@ -638,7 +638,7 @@ def check_support(vevent: icalendar.cal.Event, href: str, calendar: str):
         )
 
 
-def check_for_errors(component: icalendar.cal.Component, calendar: str, href: str):
+def check_for_errors(component: icalendar.cal.Component, calendar: str, href: str) -> None:
     """checking if component.errors exists, is not empty and if so warn the user"""
     if hasattr(component, 'errors') and component.errors:
         logger.error(
@@ -653,7 +653,7 @@ def calc_shift_deltas(vevent: icalendar.Event) -> Tuple[dt.timedelta, dt.timedel
     """calculate an event's duration and by how much its start time has shifted
     versus its recurrence-id time
 
-    :param event: an event with an RECURRENCE-ID property
+    :param event: an event with a RECURRENCE-ID property
     """
     assert isinstance(vevent, icalendar.Event)  # REMOVE ME
     start_shift = vevent['DTSTART'].dt - vevent['RECURRENCE-ID'].dt
diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py
index b8a10bf..870d7ee 100644
--- a/khal/khalendar/event.py
+++ b/khal/khalendar/event.py
@@ -25,7 +25,7 @@ helper functions."""
 import datetime as dt
 import logging
 import os
-from typing import Dict, List, Optional, Type
+from typing import Dict, List, Optional, Type, Union
 
 import icalendar
 import icalendar.cal
@@ -48,7 +48,7 @@ class Event:
     """base Event class for representing a *recurring instance* of an Event
 
     (in case of non-recurring events this distinction is irrelevant)
-    We keep a copy of the start and end time around, because for recurring
+    We keep a copy of this instances start and end time around, because for recurring
     events it might be costly to expand the recursion rules
 
     important distinction for AllDayEvents:
@@ -59,7 +59,7 @@ class Event:
     allday: bool = False
 
     def __init__(self,
-                 vevents,
+                 vevents: Dict[str, icalendar.Event],
                  locale: LocaleConfiguration,
                  ref: Optional[str] = None,
                  readonly: bool = False,
@@ -84,8 +84,10 @@ class Event:
         self.readonly = readonly
         self.href = href
         self.etag = etag
-        self.calendar = calendar
+        self.calendar = calendar if calendar else ''
         self.color = color
+        self._start: dt.datetime
+        self._end: dt.datetime
 
         if start is None:
             self._start = self._vevents[self.ref]['DTSTART'].dt
@@ -123,7 +125,7 @@ class Event:
 
     @classmethod
     def fromVEvents(cls,
-                    events_list: List[icalendar.cal.Event],
+                    events_list: List[icalendar.Event],
                     ref: Optional[str]=None,
                     start: Optional[dt.datetime]=None,
                     **kwargs) -> 'Event':
@@ -157,12 +159,12 @@ class Event:
         return instcls(vevents, ref=ref, start=start, **kwargs)
 
     @classmethod
-    def fromString(cls, event_str, ref=None, **kwargs):
-        calendar_collection = cal_from_ics(event_str)
+    def fromString(cls, ics: str, ref=None, **kwargs) -> 'Event':
+        calendar_collection = cal_from_ics(ics)
         events = [item for item in calendar_collection.walk() if item.name == 'VEVENT']
         return cls.fromVEvents(events, ref, **kwargs)
 
-    def __lt__(self, other):
+    def __lt__(self, other: 'Event') -> bool:
         start = self.start_local
         other_start = other.start_local
         if isinstance(start, dt.date) and not isinstance(start, dt.datetime):
@@ -199,7 +201,7 @@ class Event:
         except TypeError:
             raise ValueError(f'Cannot compare events {start} and {other_start}')
 
-    def update_start_end(self, start, end):
+    def update_start_end(self, start: dt.datetime, end: dt.datetime) -> None:
         """update start and end time of this event
 
         calling this on a recurring event will lead to the proto instance
@@ -225,7 +227,7 @@ class Event:
             self._vevents[self.ref].add('DURATION', end - start)
 
     @property
-    def recurring(self):
+    def recurring(self) -> bool:
         try:
             rval = 'RRULE' in self._vevents[self.ref] or \
                 'RECURRENCE-ID' in self._vevents[self.ref] or \
@@ -240,26 +242,26 @@ class Event:
             return rval
 
     @property
-    def recurpattern(self):
+    def recurpattern(self) -> str:
         if 'RRULE' in self._vevents[self.ref]:
             return self._vevents[self.ref]['RRULE'].to_ical().decode('utf-8')
         else:
             return ''
 
     @property
-    def recurobject(self):
+    def recurobject(self) -> icalendar.vRecur:
         if 'RRULE' in self._vevents[self.ref]:
             return self._vevents[self.ref]['RRULE']
         else:
             return icalendar.vRecur()
 
-    def update_rrule(self, rrule):
+    def update_rrule(self, rrule: str) -> None:
         self._vevents['PROTO'].pop('RRULE')
         if rrule is not None:
             self._vevents['PROTO'].add('RRULE', rrule)
 
     @property
-    def recurrence_id(self):
+    def recurrence_id(self) -> Union[dt.datetime, str]:
         """return the "original" start date of this event (i.e. their recurrence-id)
         """
         if self.ref == 'PROTO':
@@ -267,7 +269,7 @@ class Event:
         else:
             return pytz.UTC.localize(dt.datetime.utcfromtimestamp(int(self.ref)))
 
-    def increment_sequence(self):
+    def increment_sequence(self) -> None:
         """update the SEQUENCE number, call before saving this event"""
         # TODO we might want to do this automatically in raw() everytime
         # the event has changed, this will f*ck up the tests though
@@ -298,28 +300,28 @@ class Event:
             }
 
     @property
-    def start_local(self):
+    def start_local(self) -> dt.datetime:
         """self.start() localized to local timezone"""
         return self.start
 
     @property
-    def end_local(self):
+    def end_local(self) -> dt.datetime:
         """self.end() localized to local timezone"""
         return self.end
 
     @property
-    def start(self):
+    def start(self) -> dt.datetime:
         """this should return the start date(time) as saved in the event"""
         return self._start
 
     @property
-    def end(self):
+    def end(self) -> dt.datetime:
         """this should return the end date(time) as saved in the event or
         implicitly defined by start and duration"""
         return self._end
 
     @property
-    def duration(self):
+    def duration(self) -> dt.timedelta:
         try:
             return self._vevents[self.ref]['DURATION'].dt
         except KeyError:
@@ -330,7 +332,7 @@ class Event:
         return self._vevents[self.ref]['UID']
 
     @property
-    def organizer(self):
+    def organizer(self) -> str:
         if 'ORGANIZER' not in self._vevents[self.ref]:
             return ''
         organizer = self._vevents[self.ref]['ORGANIZER']
@@ -365,9 +367,7 @@ class Event:
 
     @property
     def raw(self) -> str:
-        """needed for vdirsyncer compatibility
-
-        return text
+        """Creates a VCALENDAR containing VTIMEZONEs
         """
         calendar = self._create_calendar()
         tzs = []
@@ -389,7 +389,7 @@ class Event:
             calendar.add_component(vevent)
         return calendar.to_ical().decode('utf-8')
 
-    def export_ics(self, path):
+    def export_ics(self, path: str) -> None:
         """export event as ICS
         """
         export_path = os.path.expanduser(path)
@@ -432,11 +432,11 @@ class Event:
         else:
             return self._vevents[self.ref].get('SUMMARY', '')
 
-    def update_summary(self, summary):
+    def update_summary(self, summary: str) -> None:
         self._vevents[self.ref]['SUMMARY'] = summary
 
     @staticmethod
-    def _can_handle_alarm(alarm):
+    def _can_handle_alarm(alarm) -> bool:
         """
         Decides whether we can handle a certain alarm.
         """
@@ -471,24 +471,24 @@ class Event:
         self._vevents[self.ref].subcomponents = components
 
     @property
-    def location(self):
+    def location(self) -> str:
         return self._vevents[self.ref].get('LOCATION', '')
 
-    def update_location(self, location):
+    def update_location(self, location: str) -> None:
         if location:
             self._vevents[self.ref]['LOCATION'] = location
         else:
             self._vevents[self.ref].pop('LOCATION')
 
     @property
-    def attendees(self):
+    def attendees(self) -> str:
         addresses = self._vevents[self.ref].get('ATTENDEE', [])
         if not isinstance(addresses, list):
             addresses = [addresses, ]
         return ", ".join([address.split(':')[-1]
                           for address in addresses])
 
-    def update_attendees(self, attendees):
+    def update_attendees(self, attendees: List[str]):
         assert isinstance(attendees, list)
         attendees = [a.strip().lower() for a in attendees if a != ""]
         if len(attendees) > 0:
@@ -518,20 +518,20 @@ class Event:
             self._vevents[self.ref].pop('ATTENDEE')
 
     @property
-    def categories(self):
+    def categories(self) -> str:
         try:
             return self._vevents[self.ref].get('CATEGORIES', '').to_ical().decode('utf-8')
         except AttributeError:
             return ''
 
-    def update_categories(self, categories):
+    def update_categories(self, categories: List[str]) -> None:
         assert isinstance(categories, list)
         self._vevents[self.ref].pop('CATEGORIES', False)
         if categories:
             self._vevents[self.ref].add('CATEGORIES', categories)
 
     @property
-    def description(self):
+    def description(self) -> str:
         return self._vevents[self.ref].get('DESCRIPTION', '')
 
     def update_description(self, description):
@@ -541,7 +541,7 @@ class Event:
             self._vevents[self.ref].pop('DESCRIPTION')
 
     @property
-    def _recur_str(self):
+    def _recur_str(self) -> str:
         if self.recurring:
             recurstr = ' ' + self.symbol_strings['recurring']
         else:
@@ -549,14 +549,14 @@ class Event:
         return recurstr
 
     @property
-    def _alarm_str(self):
+    def _alarm_str(self) -> str:
         if self.alarms:
             alarmstr = ' ' + self.symbol_strings['alarming']
         else:
             alarmstr = ''
         return alarmstr
 
-    def format(self, format_string, relative_to, env=None, colors=True):
+    def format(self, format_string: str, relative_to, env=None, colors: bool=True):
         """
         :param colors: determines if colors codes should be printed or not
         :type colors: bool
@@ -690,7 +690,7 @@ class Event:
             attributes["description-separator"] = " :: "
         attributes["location"] = self.location.strip()
         attributes["attendees"] = self.attendees
-        attributes["all-day"] = allday
+        attributes["all-day"] = str(allday)
         attributes["categories"] = self.categories
         attributes['uid'] = self.uid
         attributes['url'] = self.url
@@ -754,7 +754,7 @@ class Event:
             self._vevents.pop(key)
 
     @property
-    def status(self):
+    def status(self) -> str:
         return self._vevents[self.ref].get('STATUS', '')
 
 
@@ -766,6 +766,7 @@ class LocalizedEvent(DatetimeEvent):
     """
     see parent
     """
+
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         try:
@@ -801,14 +802,14 @@ class LocalizedEvent(DatetimeEvent):
             self._end = endtz.localize(self._end)
 
     @property
-    def start_local(self):
+    def start_local(self) -> dt.datetime:
         """
         see parent
         """
         return self.start.astimezone(self._locale['local_timezone'])
 
     @property
-    def end_local(self):
+    def end_local(self) -> dt.datetime:
         """
         see parent
         """
@@ -818,22 +819,22 @@ class LocalizedEvent(DatetimeEvent):
 class FloatingEvent(DatetimeEvent):
     """
     """
-    allday = False
+    allday: bool = False
 
     @property
-    def start_local(self):
+    def start_local(self) -> dt.datetime:
         return self._locale['local_timezone'].localize(self.start)
 
     @property
-    def end_local(self):
+    def end_local(self) -> dt.datetime:
         return self._locale['local_timezone'].localize(self.end)
 
 
 class AllDayEvent(Event):
-    allday = True
+    allday: bool = True
 
     @property
-    def end(self):
+    def end(self) -> dt.datetime:
         end = super().end
         if end == self.start:
             # https://github.com/pimutils/khal/issues/129
@@ -846,7 +847,7 @@ class AllDayEvent(Event):
         return end - dt.timedelta(days=1)
 
     @property
-    def duration(self):
+    def duration(self) -> dt.timedelta:
         try:
             return self._vevents[self.ref]['DURATION'].dt
         except KeyError:
diff --git a/khal/khalendar/khalendar.py b/khal/khalendar/khalendar.py
index 7ccf646..8e44912 100644
--- a/khal/khalendar/khalendar.py
+++ b/khal/khalendar/khalendar.py
@@ -33,29 +33,18 @@ import os.path
 from typing import (Any, Dict, Iterable, List, Optional, Set, Tuple,  # noqa
                     Union)
 
-from ..custom_types import CalendarConfiguration
+from ..custom_types import CalendarConfiguration, EventCreationTypes
+from ..icalendar import new_vevent
 from . import backend
 from .event import Event
-from .exceptions import (CouldNotCreateDbDir, DuplicateUid, NonUniqueUID,
-                         ReadOnlyCalendarError, UnsupportedFeatureError,
-                         UpdateFailed)
+from .exceptions import (DuplicateUid, NonUniqueUID, ReadOnlyCalendarError,
+                         UnsupportedFeatureError, UpdateFailed)
 from .vdir import (AlreadyExistingError, CollectionNotFoundError, Vdir,
                    get_etag_from_file)
 
 logger = logging.getLogger('khal')
 
 
-def create_directory(path: str) -> None:
-    if not os.path.isdir(path):
-        if os.path.exists(path):
-            raise RuntimeError(f'{path} is not a directory.')
-        try:
-            os.makedirs(path, mode=0o750)
-        except OSError as error:
-            logger.critical(f'failed to create {path}: {error}')
-            raise CouldNotCreateDbDir()
-
-
 class CalendarCollection:
     """CalendarCollection allows access to various calendars stored in vdirs
 
@@ -126,7 +115,7 @@ class CalendarCollection:
         return self._default_calendar_name
 
     @default_calendar_name.setter
-    def default_calendar_name(self, default: str):
+    def default_calendar_name(self, default: str) -> None:
         if default is None:
             self._default_calendar_name = default
         elif default not in self.names:
@@ -170,7 +159,7 @@ class CalendarCollection:
         )
         return list(set(calendars))
 
-    def update(self, event: Event):
+    def update(self, event: Event) -> None:
         """update `event` in vdir and db"""
         assert event.etag is not None
         assert event.calendar is not None
@@ -183,7 +172,7 @@ class CalendarCollection:
             self._backend.update(event.raw, event.href, event.etag, calendar=event.calendar)
             self._backend.set_ctag(self._local_ctag(event.calendar), calendar=event.calendar)
 
-    def force_update(self, event: Event, collection: Optional[str]=None):
+    def force_update(self, event: Event, collection: Optional[str]=None) -> None:
         """update `event` even if an event with the same uid/href already exists"""
         href: str
         calendar = collection if collection is not None else event.calendar
@@ -201,12 +190,16 @@ class CalendarCollection:
             self._backend.update(event.raw, href, etag, calendar=calendar)
             self._backend.set_ctag(self._local_ctag(calendar), calendar=calendar)
 
-    def new(self, event: Event, collection: Optional[str]=None):
-        """save a new event to the vdir and the database
+    def insert(self, event: Event, collection: Optional[str]=None):
+        """insert a new event to the vdir and the database
 
-        :param event: the event that should be updated, it will get a new href
+        :param event: the event that should be inserted, it will get a new href
             and etag properties
         """
+        # TODO FIXME not all `event`s are actually of type Event, we also uptade
+        # with vdir.Items. Those don't have an .href or .etag property which we
+        # than attach anyway. Works, but pretty ugly and any type checker will
+        # complain.
         calendar = collection if collection is not None else event.calendar
         assert calendar is not None
         if hasattr(event, 'etag'):
@@ -259,19 +252,36 @@ class CalendarCollection:
         )
         return event
 
-    def change_collection(self, event: Event, new_collection: str):
+    def change_collection(self, event: Event, new_collection: str) -> None:
+        """Moves `event` to a new collection (calendar)"""
         href, etag, calendar = event.href, event.etag, event.calendar
         event.etag = None
-        self.new(event, new_collection)
+        self.insert(event, new_collection)
         assert href is not None
         assert calendar is not None
         self.delete(href, etag, calendar=calendar)
 
-    def new_event(self, ical: str, collection: str):
-        """creates and returns (but does not insert) new event from ical
+    def create_event_from_ics(self,
+                              ical: str,
+                              calendar_name: str,
+                              etag: Optional[str]=None,
+                              href: Optional[str]=None,
+                              ) -> Event:
+        """creates and returns (but does not insert) a new event from ical
         string"""
-        calendar = collection or self.writable_names[0]
-        return Event.fromString(ical, locale=self._locale, calendar=calendar)
+        calendar = calendar_name or self.writable_names[0]
+        return Event.fromString(ical, locale=self._locale, calendar=calendar, etag=etag, href=href)
+
+    def create_event_from_dict(self,
+                               event_dict: EventCreationTypes,
+                               calendar_name: Optional[str] = None,
+                               ) -> Event:
+        """Creates an Event from the method's arguments
+        """
+        vevent = new_vevent(locale=self._locale, **event_dict)
+        calendar_name = calendar_name or self.default_calendar_name
+        assert calendar_name is not None
+        return self.create_event_from_ics(vevent.to_ical(), calendar_name)
 
     def update_db(self) -> None:
         """update the db from the vdir,
@@ -315,7 +325,7 @@ class CalendarCollection:
             self._last_ctags[calendar] = local_ctag
         return local_ctag != self._backend.get_ctag(calendar)
 
-    def _db_update(self, calendar: str):
+    def _db_update(self, calendar: str) -> None:
         """implements the actual db update on a per calendar base"""
         local_ctag = self._local_ctag(calendar)
         db_hrefs = {href for href, etag in self._backend.list(calendar)}
diff --git a/khal/khalendar/vdir.py b/khal/khalendar/vdir.py
index 60bbec2..b442d5d 100644
--- a/khal/khalendar/vdir.py
+++ b/khal/khalendar/vdir.py
@@ -7,17 +7,29 @@ import errno
 import os
 import uuid
 from hashlib import sha1
-from typing import Iterable, Optional, Tuple, Union  # noqa
+from typing import (IO, Callable, Dict, Iterable, Optional, Protocol, Tuple,
+                    Type)
 
 from atomicwrites import atomic_write
 
-from ..custom_types import SupportsRaw
+from ..custom_types import PathLike, SupportsRaw
+
+
+class HasMetaProtocol(Protocol):
+    color_type: Callable
+
+    def get_meta(self, key: str) -> str:
+        ...
+
+    def set_meta(self, key: str, value: str) -> None:
+        ...
 
 
 class cached_property:
     '''A read-only @property that is only evaluated once. Only usable on class
     instances' methods.
     '''
+
     def __init__(self, fget, doc=None):
         self.__name__ = fget.__name__
         self.__module__ = fget.__module__
@@ -36,11 +48,11 @@ SAFE_UID_CHARS = ('abcdefghijklmnopqrstuvwxyz'
                   '0123456789_.-+@')
 
 
-def _href_safe(uid, safe=SAFE_UID_CHARS):
+def _href_safe(uid: str, safe: str=SAFE_UID_CHARS) -> bool:
     return not bool(set(uid) - set(safe))
 
 
-def _generate_href(uid=None, safe=SAFE_UID_CHARS) -> str:
+def _generate_href(uid: Optional[str]=None, safe: str=SAFE_UID_CHARS) -> str:
     if not uid:
         return str(uuid.uuid4().hex)
     elif not _href_safe(uid, safe):
@@ -49,7 +61,7 @@ def _generate_href(uid=None, safe=SAFE_UID_CHARS) -> str:
         return uid
 
 
-def get_etag_from_file(f):
+def get_etag_from_file(f) -> str:
     '''Get mtime-based etag from a filepath, file-like object or raw file
     descriptor.
 
@@ -109,12 +121,12 @@ class AlreadyExistingError(VdirError):
 
 
 class Item:
-    def __init__(self, raw):
+    def __init__(self, raw: str):
         assert isinstance(raw, str)
         self.raw = raw
 
     @cached_property
-    def uid(self):
+    def uid(self) -> Optional[str]:
         uid = ''
         lines = iter(self.raw.splitlines())
         for line in lines:
@@ -134,7 +146,7 @@ class VdirBase:
     item_class = Item
     default_mode = 0o750
 
-    def __init__(self, path: str, fileext: str, encoding='utf-8'):
+    def __init__(self, path: str, fileext: str, encoding: str='utf-8'):
         if not os.path.isdir(path):
             raise CollectionNotFoundError(path)
         self.path = path
@@ -142,7 +154,7 @@ class VdirBase:
         self.fileext = fileext
 
     @classmethod
-    def discover(cls, path, **kwargs):
+    def discover(cls, path: str, **kwargs) -> Iterable['VdirBase']:
         try:
             collections = os.listdir(path)
         except OSError as e:
@@ -156,23 +168,23 @@ class VdirBase:
                 yield cls(path=collection_path, **kwargs)
 
     @classmethod
-    def create(cls, collection_name, **kwargs):
+    def create(cls, collection_name: PathLike, **kwargs: PathLike) -> Dict[str, PathLike]:
         kwargs = dict(kwargs)
         path = kwargs['path']
 
-        path = os.path.join(path, collection_name)
-        if not os.path.exists(path):
-            os.makedirs(path, mode=cls.default_mode)
-        elif not os.path.isdir(path):
-            raise OSError(f'{repr(path)} is not a directory.')
+        pathn = os.path.join(path, collection_name)
+        if not os.path.exists(pathn):
+            os.makedirs(pathn, mode=cls.default_mode)
+        elif not os.path.isdir(pathn):
+            raise OSError(f'{repr(pathn)} is not a directory.')
 
-        kwargs['path'] = path
+        kwargs['path'] = pathn
         return kwargs
 
-    def _get_filepath(self, href) -> str:
+    def _get_filepath(self, href: str) -> str:
         return os.path.join(self.path, href)
 
-    def _get_href(self, uid):
+    def _get_href(self, uid: Optional[str]) -> str:
         return _generate_href(uid) + self.fileext
 
     def list(self) -> Iterable[Tuple[str, str]]:
@@ -195,13 +207,13 @@ class VdirBase:
             else:
                 raise
 
-    def upload(self, item) -> Tuple[str, str]:
+    def upload(self, item: SupportsRaw) -> Tuple[str, str]:
         if not isinstance(item.raw, str):
             raise TypeError('item.raw must be a unicode string.')
 
         try:
             href = self._get_href(item.uid)
-            fpath, etag = self._upload_impl(item, href)
+            _, etag = self._upload_impl(item, href)
         except OSError as e:
             if e.errno in (
                 errno.ENAMETOOLONG,  # Unix
@@ -209,14 +221,15 @@ class VdirBase:
             ):
                 # random href instead of UID-based
                 href = self._get_href(None)
-                fpath, etag = self._upload_impl(item, href)
+                _, etag = self._upload_impl(item, href)
             else:
                 raise
         return href, etag
 
-    def _upload_impl(self, item, href: str) -> Tuple[str, str]:
+    def _upload_impl(self, item: SupportsRaw, href: str) -> Tuple[str, str]:
         fpath = self._get_filepath(href)
         try:
+            f: IO
             with atomic_write(fpath, mode='wb', overwrite=False) as f:
                 f.write(item.raw.encode(self.encoding))
                 return fpath, get_etag_from_file(f)
@@ -243,7 +256,7 @@ class VdirBase:
 
         return etag
 
-    def delete(self, href: str, etag: Optional[str]):
+    def delete(self, href: str, etag: Optional[str]) -> None:
         fpath = self._get_filepath(href)
         if not os.path.isfile(fpath):
             raise NotFoundError(href)
@@ -252,7 +265,7 @@ class VdirBase:
             raise WrongEtagError(etag, actual_etag)
         os.remove(fpath)
 
-    def get_meta(self, key):
+    def get_meta(self, key: str) -> Optional[str]:
         fpath = os.path.join(self.path, key)
         try:
             with open(fpath, 'rb') as f:
@@ -263,7 +276,7 @@ class VdirBase:
             else:
                 raise
 
-    def set_meta(self, key, value):
+    def set_meta(self, key: str, value: str) -> None:
         value = value or ''
         assert isinstance(value, str)
         fpath = os.path.join(self.path, key)
@@ -272,7 +285,7 @@ class VdirBase:
 
 
 class Color:
-    def __init__(self, x):
+    def __init__(self, x: str):
         if not x:
             raise ValueError('Color is false-ish.')
         if not x.startswith('#'):
@@ -280,10 +293,10 @@ class Color:
         if len(x) != 7:
             raise ValueError('Color must not have shortcuts. '
                              '#ffffff instead of #fff')
-        self.raw = x.upper()
+        self.raw: str = x.upper()
 
     @cached_property
-    def rgb(self):
+    def rgb(self) -> Tuple[int, int, int]:
         x = self.raw
 
         r = x[1:3]
@@ -293,27 +306,27 @@ class Color:
         if len(r) == len(g) == len(b) == 2:
             return int(r, 16), int(g, 16), int(b, 16)
         else:
-            raise ValueError(f'Unable to parse color value: {self.value}')
+            raise ValueError(f'Unable to parse color value: {self.raw}')
 
 
-class ColorMixin:
-    color_type = Color
+class ColorMixin():
+    color_type: Type[Color] = Color
 
-    def get_color(self):
+    def get_color(self: HasMetaProtocol) -> Optional[str]:
         try:
             return self.color_type(self.get_meta('color'))
         except ValueError:
             return None
 
-    def set_color(self, value):
+    def set_color(self: HasMetaProtocol, value: str) -> None:
         self.set_meta('color', self.color_type(value).raw)
 
 
 class DisplayNameMixin:
-    def get_displayname(self):
+    def get_displayname(self: HasMetaProtocol) -> str:
         return self.get_meta('displayname')
 
-    def set_displayname(self, value):
+    def set_displayname(self: HasMetaProtocol, value: str) -> None:
         self.set_meta('displayname', value)
 
 
diff --git a/khal/parse_datetime.py b/khal/parse_datetime.py
index 544c17c..c57683a 100644
--- a/khal/parse_datetime.py
+++ b/khal/parse_datetime.py
@@ -27,13 +27,13 @@ import logging
 import re
 from calendar import isleap
 from time import strptime
-from typing import Callable, List, Optional, Tuple
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
 
 import pytz
 
 from khal.exceptions import DateTimeParseError, FatalError
 
-from .custom_types import LocaleConfiguration
+from .custom_types import LocaleConfiguration, RRuleMapType
 
 logger = logging.getLogger('khal')
 
@@ -188,7 +188,7 @@ def datetimefstr_weekday(dtime_list: List[str], timeformat: str, infer_year: boo
 
 
 def guessdatetimefstr(
-    dtime_list: list,
+    dtime_list: List[str],
     locale: LocaleConfiguration,
     default_day: Optional[dt.date]=None,
     in_future=True,
@@ -215,7 +215,7 @@ def guessdatetimefstr(
             return dt.datetime.now()
         raise ValueError
 
-    def datefstr_year(dtime_list: list, dtformat: str, infer_year: bool) -> dt.datetime:
+    def datefstr_year(dtime_list: List[str], dtformat: str, infer_year: bool) -> dt.datetime:
         return datetimefstr(dtime_list, dtformat, day, infer_year, in_future)
 
     dtstart = None
@@ -250,7 +250,7 @@ def guessdatetimefstr(
     )
 
 
-def timedelta2str(delta):
+def timedelta2str(delta: dt.timedelta) -> str:
     # we deliberately ignore any subsecond deltas
     total_seconds = int(abs(delta).total_seconds())
 
@@ -280,7 +280,7 @@ def timedelta2str(delta):
     return ' '.join(s)
 
 
-def guesstimedeltafstr(delta_string):
+def guesstimedeltafstr(delta_string: str) -> dt.timedelta:
     """parses a timedelta from a string
 
     :param delta_string: string encoding time-delta, e.g. '1h 15m'
@@ -320,11 +320,12 @@ def guesstimedeltafstr(delta_string):
     return res
 
 
-def guessrangefstr(daterange, locale,
-                   default_timedelta_date=dt.timedelta(days=1),
-                   default_timedelta_datetime=dt.timedelta(hours=1),
-                   adjust_reasonably=False,
-                   ):
+def guessrangefstr(daterange: Union[str, List[str]],
+                   locale: LocaleConfiguration,
+                   default_timedelta_date: dt.timedelta=dt.timedelta(days=1),
+                   default_timedelta_datetime: dt.timedelta=dt.timedelta(hours=1),
+                   adjust_reasonably: bool=False,
+                   ) -> Tuple[dt.datetime, dt.datetime, bool]:
     """parses a range string
 
     :param daterange: date1 [date2 | timedelta]
@@ -333,44 +334,43 @@ def guessrangefstr(daterange, locale,
     :returns: start and end of the date(time) range  and if
         this is an all-day time range or not,
         **NOTE**: the end is *exclusive* if this is an allday event
-    :rtype: (datetime, datetime, bool)
-
     """
     range_list = daterange
     if isinstance(daterange, str):
         range_list = daterange.split(' ')
+        assert isinstance(range_list, list)
 
     if range_list == ['week']:
         today_weekday = dt.datetime.today().weekday()
-        start = dt.datetime.today() - dt.timedelta(days=(today_weekday - locale['firstweekday']))
-        end = start + dt.timedelta(days=8)
-        return start, end, True
+        startdt = dt.datetime.today() - dt.timedelta(days=(today_weekday - locale['firstweekday']))
+        enddt = startdt + dt.timedelta(days=8)
+        return startdt, enddt, True
 
     for i in reversed(range(1, len(range_list) + 1)):
-        start = ' '.join(range_list[:i])
-        end = ' '.join(range_list[i:])
+        startstr = ' '.join(range_list[:i])
+        endstr = ' '.join(range_list[i:])
         allday = False
         try:
             # figuring out start
-            split = start.split(" ")
+            split = startstr.split(" ")
             start, allday = guessdatetimefstr(split, locale)
             if len(split) != 0:
                 continue
 
             # and end
-            if len(end) == 0:
+            if len(endstr) == 0:
                 if allday:
                     end = start + default_timedelta_date
                 else:
                     end = start + default_timedelta_datetime
-            elif end.lower() == 'eod':
+            elif endstr.lower() == 'eod':
                 end = dt.datetime.combine(start.date(), dt.time.max)
-            elif end.lower() == 'week':
+            elif endstr.lower() == 'week':
                 start -= dt.timedelta(days=(start.weekday() - locale['firstweekday']))
                 end = start + dt.timedelta(days=8)
             else:
                 try:
-                    delta = guesstimedeltafstr(end)
+                    delta = guesstimedeltafstr(endstr)
                     if allday and delta.total_seconds() % (3600 * 24):
                         # TODO better error class, no logging in here
                         logger.fatal(
@@ -385,7 +385,7 @@ def guessrangefstr(daterange, locale,
 
                     end = start + delta
                 except (ValueError, DateTimeParseError):
-                    split = end.split(" ")
+                    split = endstr.split(" ")
                     end, end_allday = guessdatetimefstr(
                         split, locale, default_day=start.date(), in_future=False)
                     if len(split) != 0:
@@ -419,9 +419,13 @@ def guessrangefstr(daterange, locale,
     )
 
 
-def rrulefstr(repeat, until, locale, timezone):
+def rrulefstr(repeat: str,
+              until: str,
+              locale: LocaleConfiguration,
+              timezone: Optional[dt.tzinfo],
+              ) -> RRuleMapType:
     if repeat in ["daily", "weekly", "monthly", "yearly"]:
-        rrule_settings = {'freq': repeat}
+        rrule_settings: RRuleMapType = {'freq': repeat}
         if until:
             until_dt, _ = guessdatetimefstr(until.split(' '), locale)
             if timezone:
@@ -437,20 +441,15 @@ def rrulefstr(repeat, until, locale, timezone):
         raise FatalError()
 
 
-def eventinfofstr(info_string, locale, default_event_duration, default_dayevent_duration,
-                  adjust_reasonably=False):
+def eventinfofstr(info_string: str,
+                  locale: LocaleConfiguration,
+                  default_event_duration: dt.timedelta,
+                  default_dayevent_duration: dt.timedelta,
+                  adjust_reasonably: bool=False,
+                  ) -> Dict[str, Any]:
     """parses a string of the form START [END | DELTA] [TIMEZONE] [SUMMARY] [::
     DESCRIPTION] into a dictionary with keys: dtstart, dtend, timezone, allday,
     summary, description
-
-    :param info_string:
-    :type info_string: string fitting the form
-    :param locale:
-    :type locale: locale
-    :param adjust_reasonably:
-    :type adjust_reasonably: passed on to guessrangefstr
-    :rtype: dictionary
-
     """
     description = None
     if " :: " in info_string:
@@ -458,10 +457,10 @@ def eventinfofstr(info_string, locale, default_event_duration, default_dayevent_
 
     parts = info_string.split(' ')
     summary = None
-    start = None
-    end = None
-    tz = None
-    allday = False
+    start: Optional[Union[dt.datetime, dt.date]] = None
+    end: Optional[Union[dt.datetime, dt.date]] = None
+    tz: Optional[pytz.BaseTzInfo] = None
+    allday: bool = False
     for i in reversed(range(1, len(parts) + 1)):
         try:
             start, end, allday = guessrangefstr(
@@ -495,10 +494,12 @@ def eventinfofstr(info_string, locale, default_event_duration, default_dayevent_
         tz = locale['default_timezone']
 
     if allday:
+        assert isinstance(start, dt.datetime)
+        assert isinstance(end, dt.datetime)
         start = start.date()
         end = end.date()
 
-    info = {}
+    info: Dict[str, Any] = {}
     info["dtstart"] = start
     info["dtend"] = end
     info["summary"] = summary if summary else None
diff --git a/khal/settings/__init__.py b/khal/settings/__init__.py
index 3226d09..99b11ca 100644
--- a/khal/settings/__init__.py
+++ b/khal/settings/__init__.py
@@ -1,2 +1,4 @@
 from .exceptions import InvalidSettingsError  # noqa  # type: ignore
+from .exceptions import NoConfigFile  # noqa  # type: ignore
+from .settings import find_configuration_file  # noqa  # type: ignore
 from .settings import get_config  # noqa  # type: ignore
diff --git a/khal/settings/settings.py b/khal/settings/settings.py
index 359b942..0683a39 100644
--- a/khal/settings/settings.py
+++ b/khal/settings/settings.py
@@ -35,6 +35,8 @@ try:
 except ModuleNotFoundError:
     from validate import Validator
 
+from typing import Callable, List, Optional
+
 from .exceptions import (CannotParseConfigFileError, InvalidSettingsError,
                          NoConfigFile)
 from .utils import (config_checks, expand_db_path, expand_path,
@@ -45,7 +47,7 @@ logger = logging.getLogger('khal')
 SPECPATH = os.path.join(os.path.dirname(__file__), 'khal.spec')
 
 
-def find_configuration_file():
+def find_configuration_file() -> Optional[str]:
     """Return the configuration filename.
 
     This function builds the list of paths known by khal and then return the
@@ -76,18 +78,16 @@ def find_configuration_file():
 
 
 def get_config(
-        config_path=None,
-        _get_color_from_vdir=get_color_from_vdir,
-        _get_vdir_type=get_vdir_type) -> ConfigObj:
+        config_path: Optional[str]=None,
+        _get_color_from_vdir: Callable=get_color_from_vdir,
+        _get_vdir_type: Callable=get_vdir_type) -> ConfigObj:
     """reads the config file, validates it and return a config dict
 
     :param config_path: path to a custom config file, if none is given the
                         default locations will be searched
-    :type config_path: str
     :param _get_color_from_vdir: override get_color_from_vdir for testing purposes
     :param _get_vdir_type: override get_vdir_type for testing purposes
     :returns: configuration
-    :rtype: dict
     """
     if config_path is None:
         config_path = find_configuration_file()
@@ -159,7 +159,7 @@ def get_config(
     return user_config
 
 
-def sectionize(sections, depth=1):
+def sectionize(sections: List[str], depth: int=1) -> str:
     """converts list of string into [list][[of]][[[strings]]]"""
     this_part = depth * '[' + sections[0] + depth * ']'
     if len(sections) > 1:
diff --git a/khal/settings/utils.py b/khal/settings/utils.py
index fa6cf27..5b8f91c 100644
--- a/khal/settings/utils.py
+++ b/khal/settings/utils.py
@@ -25,7 +25,7 @@ import glob
 import logging
 import os
 from os.path import expanduser, expandvars, join
-from typing import Optional
+from typing import Callable, Iterable, List, Literal, Optional, Union
 
 import pytz
 import xdg
@@ -61,20 +61,19 @@ def is_timezone(tzstring: Optional[str]) -> dt.tzinfo:
         raise VdtValueError(f"Unknown timezone {tzstring}")
 
 
-def is_timedelta(string):
+def is_timedelta(string: str) -> dt.timedelta:
     try:
         return guesstimedeltafstr(string)
     except ValueError:
         raise VdtValueError(f"Invalid timedelta: {string}")
 
 
-def weeknumber_option(option):
+def weeknumber_option(option: str) -> Union[str, Literal[False]]:
     """checks if *option* is a valid value
 
     :param option: the option the user set in the config file
     :type option: str
-    :returns: off, left, right
-    :rtype: str/bool
+    :returns: 'off', 'left', 'right' or False
     """
     option = option.lower()
     if option == 'left':
@@ -89,13 +88,11 @@ def weeknumber_option(option):
             "'off', 'left' or 'right'")
 
 
-def monthdisplay_option(option):
+def monthdisplay_option(option: str) -> str:
     """checks if *option* is a valid value
 
     :param option: the option the user set in the config file
-    :type option: str
     :returns: firstday, firstfullweek
-    :rtype: str/bool
     """
     option = option.lower()
     if option == 'firstday':
@@ -109,19 +106,19 @@ def monthdisplay_option(option):
         )
 
 
-def expand_path(path):
+def expand_path(path: str) -> str:
     """expands `~` as well as variable names"""
     return expanduser(expandvars(path))
 
 
-def expand_db_path(path):
+def expand_db_path(path: str) -> str:
     """expands `~` as well as variable names, defaults to $XDG_DATA_HOME"""
     if path is None:
         path = join(xdg.BaseDirectory.xdg_data_home, 'khal', 'khal.db')
     return expanduser(expandvars(path))
 
 
-def is_color(color):
+def is_color(color: str) -> str:
     """checks if color represents a valid color
 
     raises a VdtValueError if color is not valid
@@ -141,7 +138,7 @@ def is_color(color):
     raise VdtValueError(color)
 
 
-def test_default_calendar(config):
+def test_default_calendar(config) -> None:
     """test if config['default']['default_calendar'] is set to a sensible
     value
     """
@@ -159,7 +156,7 @@ def test_default_calendar(config):
         raise InvalidSettingsError()
 
 
-def get_color_from_vdir(path):
+def get_color_from_vdir(path: str) -> Optional[str]:
     try:
         color = Vdir(path, '.ics').get_meta('color')
     except CollectionNotFoundError:
@@ -176,7 +173,7 @@ def get_color_from_vdir(path):
     return color
 
 
-def get_unique_name(path, names):
+def get_unique_name(path: str, names: Iterable[str]) -> str:
     # TODO take care of edge cases, make unique name finding less brain-dead
     try:
         name = Vdir(path, '.ics').get_meta('displayname')
@@ -192,22 +189,24 @@ def get_unique_name(path, names):
     return name
 
 
-def get_all_vdirs(path):
+def get_all_vdirs(path: str) -> Iterable[str]:
     """returns a list of paths, expanded using glob
     """
+    # FIXME currently returns a list of all files in path
     items = glob.glob(path)
     return items
 
 
-def get_vdir_type(_):
+def get_vdir_type(_: str) -> str:
     # TODO implement
     return 'calendar'
 
 
 def config_checks(
-        config,
-        _get_color_from_vdir=get_color_from_vdir,
-        _get_vdir_type=get_vdir_type):
+    config,
+    _get_color_from_vdir: Callable=get_color_from_vdir,
+    _get_vdir_type: Callable=get_vdir_type,
+) -> None:
     """do some tests on the config we cannot do with configobj's validator"""
     if len(config['calendars'].keys()) < 1:
         logger.fatal('Found no calendar section in the config file')
@@ -221,7 +220,7 @@ def config_checks(
             config['locale']['local_timezone'])
 
     # expand calendars with type = discover
-    vdirs_complete = []
+    vdirs_complete: List[str] = []
     vdir_colors_from_config = {}
     for calendar in list(config['calendars'].keys()):
         if not isinstance(config['calendars'][calendar], dict):
diff --git a/khal/terminal.py b/khal/terminal.py
index 398aca0..2274a17 100644
--- a/khal/terminal.py
+++ b/khal/terminal.py
@@ -21,33 +21,36 @@
 #
 """all functions related to terminal display are collected here"""
 
-from collections import namedtuple
 from itertools import zip_longest
-from typing import Dict, List, Optional
+from typing import Dict, List, NamedTuple, Optional
+
+
+class NamedColor(NamedTuple):
+    color_index: int
+    light: bool
 
-NamedColor = namedtuple('NamedColor', ['index', 'light'])
 
 RTEXT = '\x1b[7m'  # reverse
 NTEXT = '\x1b[0m'  # normal
 BTEXT = '\x1b[1m'  # bold
 RESET = '\33[0m'
 COLORS: Dict[str, NamedColor] = {
-    'black': NamedColor(index=0, light=False),
-    'dark red': NamedColor(index=1, light=False),
-    'dark green': NamedColor(index=2, light=False),
-    'brown': NamedColor(index=3, light=False),
-    'dark blue': NamedColor(index=4, light=False),
-    'dark magenta': NamedColor(index=5, light=False),
-    'dark cyan': NamedColor(index=6, light=False),
-    'white': NamedColor(index=7, light=False),
-    'light gray': NamedColor(index=7, light=True),
-    'dark gray': NamedColor(index=0, light=True),  # actually light black
-    'light red': NamedColor(index=1, light=True),
-    'light green': NamedColor(index=2, light=True),
-    'yellow': NamedColor(index=3, light=True),
-    'light blue': NamedColor(index=4, light=True),
-    'light magenta': NamedColor(index=5, light=True),
-    'light cyan': NamedColor(index=6, light=True)
+    'black': NamedColor(color_index=0, light=False),
+    'dark red': NamedColor(color_index=1, light=False),
+    'dark green': NamedColor(color_index=2, light=False),
+    'brown': NamedColor(color_index=3, light=False),
+    'dark blue': NamedColor(color_index=4, light=False),
+    'dark magenta': NamedColor(color_index=5, light=False),
+    'dark cyan': NamedColor(color_index=6, light=False),
+    'white': NamedColor(color_index=7, light=False),
+    'light gray': NamedColor(color_index=7, light=True),
+    'dark gray': NamedColor(color_index=0, light=True),  # actually light black
+    'light red': NamedColor(color_index=1, light=True),
+    'light green': NamedColor(color_index=2, light=True),
+    'yellow': NamedColor(color_index=3, light=True),
+    'light blue': NamedColor(color_index=4, light=True),
+    'light magenta': NamedColor(color_index=5, light=True),
+    'light cyan': NamedColor(color_index=6, light=True)
 }
 
 
@@ -75,7 +78,7 @@ def get_color(
                 # 16 color palette
                 if not is_bg:
                     # foreground color
-                    c = 30 + COLORS[colorstring].index
+                    c = 30 + COLORS[colorstring].color_index
                     if COLORS[colorstring].light:
                         if bold_for_light_color:
                             color += '1;'
@@ -83,7 +86,7 @@ def get_color(
                             c += 60
                 else:
                     # background color
-                    c = 40 + COLORS[colorstring].index
+                    c = 40 + COLORS[colorstring].color_index
                     if COLORS[colorstring].light:
                         if not bold_for_light_color:
                             c += 60
diff --git a/khal/ui/__init__.py b/khal/ui/__init__.py
index 42336be..183ef19 100644
--- a/khal/ui/__init__.py
+++ b/khal/ui/__init__.py
@@ -23,18 +23,17 @@ import datetime as dt
 import logging
 import signal
 import sys
+from typing import Optional
 
 import click
 import urwid
 
-from .. import icalendar as icalendar_helpers
 from .. import utils
-from ..khalendar.event import Event
 from ..khalendar.exceptions import ReadOnlyCalendarError
 from . import colors
 from .base import Pane, Window
-from .calendarwidget import CalendarWidget
 from .editor import EventEditor, ExportDialog
+from .widgets import CalendarWidget
 from .widgets import ExtendedEdit as Edit
 from .widgets import NColumns, NPile, linebox
 
@@ -237,6 +236,7 @@ class U_Event(urwid.Text):
 
 class EventListBox(urwid.ListBox):
     """Container for list of U_Events"""
+
     def __init__(
             self, *args, parent, conf,
             delete_status, toggle_delete_instance, toggle_delete_all,
@@ -277,6 +277,7 @@ class EventListBox(urwid.ListBox):
 class DListBox(EventListBox):
     """Container for a DayWalker"""
     # XXX unfortunate naming, there is also DateListBox
+
     def __init__(self, *args, **kwargs):
         dynamic_days = kwargs.pop('dynamic_days', True)
         super().__init__(*args, **kwargs)
@@ -739,18 +740,17 @@ class EventColumn(urwid.WidgetWrap):
         assert not self.editor
         if external_edit:
             self.pane.window.loop.stop()
-            text = click.edit(event.raw)
+            ics = click.edit(event.raw)
             self.pane.window.loop.start()
-            if text is None:
+            if ics is None:
                 return
             # KeyErrors can occur here when we destroy DTSTART,
             # otherwise, even broken .ics files seem to be no problem
-            new_event = Event.fromString(
-                text,
-                locale=self._conf['locale'],
-                href=event.href,
-                calendar=event.calendar,
+            new_event = self.pane.collection.create_event_from_ics(
+                ics,
                 etag=event.etag,
+                collection=event.calendar,
+                href=event.href,
             )
             self.pane.collection.update(new_event)
             update_colors(
@@ -865,13 +865,11 @@ class EventColumn(urwid.WidgetWrap):
         except IndexError:
             pass
 
-    def new(self, date, end=None):
+    def new(self, date: dt.datetime, end: Optional[dt.datetime]=None):
         """create a new event on `date` at the next full hour and edit it
 
         :param date: default date for new event
-        :type date: datetime.date
         :param end: optional, date the event ends on (inclusive)
-        :type end: datetime.date
         """
         if not self.pane.collection.writable_names:
             self.pane.window.alert(('alert', 'No writable calendar.'))
@@ -879,20 +877,21 @@ class EventColumn(urwid.WidgetWrap):
         if date is None:
             date = dt.datetime.now()
         if end is None:
-            start = dt.datetime.combine(date, dt.time(dt.datetime.now().hour))
-            end = start + dt.timedelta(minutes=60)
-            event = icalendar_helpers.new_event(
-                dtstart=start, dtend=end, summary='',
-                timezone=self._conf['locale']['default_timezone'],
-                locale=self._conf['locale'],
-            )
-        else:
-            event = icalendar_helpers.new_event(
-                dtstart=date, dtend=end + dt.timedelta(days=1), summary='',
-                allday=True, locale=self._conf['locale'],
-            )
-        event = self.pane.collection.new_event(
-            event.to_ical(), self.pane.collection.default_calendar_name)
+            dtstart = dt.datetime.combine(date, dt.time(dt.datetime.now().hour))
+            dtend = dtstart + dt.timedelta(minutes=60)
+            allday = False
+        else:  # TODO XXX why do we use allday if end is not set???
+            dtstart = date
+            dtend = end + dt.timedelta(days=1)
+            allday = True
+        event = self.pane.collection.generate_event(
+            dtstart=dtstart,
+            dtend=dtend,
+            summary='',
+            timezone=self._conf['locale']['default_timezone'],
+            locale=self._conf['locale'],
+            allday=allday,
+        )
         self.edit(event)
 
     def selectable(self):
@@ -1004,6 +1003,7 @@ class EventDisplay(urwid.WidgetWrap):
 
 class SearchDialog(urwid.WidgetWrap):
     """A Search Dialog Widget"""
+
     def __init__(self, search_func, abort_func):
 
         class Search(Edit):
diff --git a/khal/ui/editor.py b/khal/ui/editor.py
index b464f80..3edf3b0 100644
--- a/khal/ui/editor.py
+++ b/khal/ui/editor.py
@@ -24,10 +24,10 @@ import datetime as dt
 import urwid
 
 from ..utils import get_weekday_occurrence, get_wrapped_text
-from .calendarwidget import CalendarWidget
-from .widgets import (AlarmsEditor, Choice, DateConversionError, DateWidget,
-                      ExtendedEdit, NColumns, NListBox, NPile, PositiveIntEdit,
-                      TimeWidget, ValidatedEdit)
+from .widgets import (AlarmsEditor, CalendarWidget, Choice,
+                      DateConversionError, DateWidget, ExtendedEdit, NColumns,
+                      NListBox, NPile, PositiveIntEdit, TimeWidget,
+                      ValidatedEdit)
 
 
 class StartEnd:
@@ -514,7 +514,7 @@ class EventEditor(urwid.WidgetWrap):
             self.event.increment_sequence()
             if self.event.etag is None:  # has not been saved before
                 self.event.calendar = self.calendar_chooser.active['name']
-                self.collection.new(self.event)
+                self.collection.insert(self.event)
             elif self.calendar_chooser.changed:
                 self.collection.change_collection(
                     self.event,
diff --git a/khal/ui/widgets.py b/khal/ui/widgets.py
index ff4c907..78d3654 100644
--- a/khal/ui/widgets.py
+++ b/khal/ui/widgets.py
@@ -29,6 +29,8 @@ import re
 
 import urwid
 
+from .calendarwidget import CalendarWidget  # noqa
+
 
 class DateConversionError(Exception):
     pass
@@ -73,6 +75,7 @@ def goto_end_of_line(text):
 
 class ExtendedEdit(urwid.Edit):
     """A text editing widget supporting some more editing commands"""
+
     def keypress(self, size, key):
         if key == 'ctrl w':
             self._delete_word()
@@ -267,6 +270,7 @@ class SupportsNext:
     _select_first_selectable
     _select_last_selectable
     """
+
     def __init__(self, *args, **kwargs):
         self.outermost = kwargs.get('outermost', False)
         if 'outermost' in kwargs:
@@ -276,6 +280,7 @@ class SupportsNext:
 
 class NextMixin(SupportsNext):
     """Implements SupportsNext for urwid.Pile and urwid.Columns"""
+
     def _select_first_selectable(self):
         """select our first selectable item (recursivly if that item SupportsNext)"""
         i = self._first_selectable()


Test patch:
diff --git a/tests/controller_test.py b/tests/controller_test.py
index 8d06532..dfe52ff 100644
--- a/tests/controller_test.py
+++ b/tests/controller_test.py
@@ -40,15 +40,15 @@ conf = {'locale': utils.LOCALE_BERLIN,
 class TestGetAgenda:
     def test_new_event(self, coll_vdirs):
         coll, vdirs = coll_vdirs
-        event = coll.new_event(event_today, utils.cal1)
-        coll.new(event)
+        event = coll.create_event_from_ics(event_today, utils.cal1)
+        coll.insert(event)
         assert ['                 a meeting :: short description\x1b[0m'] == \
             khal_list(coll, [], conf, agenda_format=event_format, day_format="")
 
     def test_new_event_day_format(self, coll_vdirs):
         coll, vdirs = coll_vdirs
-        event = coll.new_event(event_today, utils.cal1)
-        coll.new(event)
+        event = coll.create_event_from_ics(event_today, utils.cal1)
+        coll.insert(event)
         assert ['Today\x1b[0m',
                 '                 a meeting :: short description\x1b[0m'] == \
             khal_list(coll, [], conf, agenda_format=event_format, day_format="{name}")
@@ -59,8 +59,8 @@ class TestGetAgenda:
             event_today = event_allday_template.format(
                 today.strftime('%Y%m%d'), tomorrow.strftime('%Y%m%d'))
             coll, vdirs = coll_vdirs
-            event = coll.new_event(event_today, utils.cal1)
-            coll.new(event)
+            event = coll.create_event_from_ics(event_today, utils.cal1)
+            coll.insert(event)
             out = khal_list(
                 coll, conf=conf, agenda_format=event_format, datepoint=[])
             assert [
@@ -77,7 +77,7 @@ class TestGetAgenda:
 
     def test_empty_recurrence(self, coll_vdirs):
         coll, vidrs = coll_vdirs
-        coll.new(coll.new_event(dedent(
+        coll.insert(coll.create_event_from_ics(dedent(
             'BEGIN:VEVENT\r\n'
             'UID:no_recurrences\r\n'
             'SUMMARY:No recurrences\r\n'
diff --git a/tests/khalendar_test.py b/tests/khalendar_test.py
index 9ff1055..2119a0b 100644
--- a/tests/khalendar_test.py
+++ b/tests/khalendar_test.py
@@ -49,9 +49,9 @@ class TestCalendar:
 
     def test_new_event(self, coll_vdirs):
         coll, vdirs = coll_vdirs
-        event = coll.new_event(event_today, cal1)
+        event = coll.create_event_from_ics(event_today, cal1)
         assert event.calendar == cal1
-        coll.new(event)
+        coll.insert(event)
         events = list(coll.get_events_on(today))
         assert len(events) == 1
         assert events[0].color == 'dark blue'
@@ -102,11 +102,11 @@ class TestVdirsyncerCompat:
         event = Event.fromString(_get_text('event_d'), calendar=cal1, locale=LOCALE_BERLIN)
         assert event.etag is None
         assert event.href is None
-        coll.new(event)
+        coll.insert(event)
         assert event.etag is not None
         assert event.href == 'V042MJ8B3SJNFXQOJL6P53OFMHJE8Z3VZWOU.ics'
         event = Event.fromString(event_today, calendar=cal1, locale=LOCALE_BERLIN)
-        coll.new(event)
+        coll.insert(event)
         hrefs = sorted(href for href, etag in coll._backend.list(cal1))
         assert {str(coll.get_event(href, calendar=cal1).uid) for href in hrefs} == {
             'uid3@host1.com',
@@ -156,7 +156,7 @@ class TestCollection:
     def test_insert(self, coll_vdirs):
         """insert a localized event"""
         coll, vdirs = coll_vdirs
-        coll.new(
+        coll.insert(
             Event.fromString(_get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN),
             cal1)
         events = list(coll.get_localized(self.astart_berlin, self.aend_berlin))
@@ -178,7 +178,7 @@ class TestCollection:
         """insert a floating event"""
         coll, vdirs = coll_vdirs
         event = Event.fromString(_get_text('event_d'), calendar=cal1, locale=LOCALE_BERLIN)
-        coll.new(event, cal1)
+        coll.insert(event, cal1)
         events = list(coll.get_events_on(aday))
         assert len(events) == 1
         assert events[0].calendar == cal1
@@ -191,7 +191,7 @@ class TestCollection:
     def test_insert_d_no_value(self, coll_vdirs):
         """insert a date event with no VALUE=DATE option"""
         coll, vdirs = coll_vdirs
-        coll.new(
+        coll.insert(
             Event.fromString(
                 _get_text('event_d_no_value'), calendar=cal1, locale=LOCALE_BERLIN),
             cal1)
@@ -209,7 +209,7 @@ class TestCollection:
         event = Event.fromString(
             _get_text('event_dt_simple'), href='xyz.ics', calendar=cal1, locale=LOCALE_BERLIN,
         )
-        coll.new(event, cal1)
+        coll.insert(event, cal1)
         event_from_db = coll.get_event(SIMPLE_EVENT_UID + '.ics', cal1)
         with freeze_time('2016-1-1'):
             assert normalize_component(event_from_db.raw) == \
@@ -219,7 +219,7 @@ class TestCollection:
         """moving an event from one calendar to another"""
         coll, vdirs = coll_vdirs
         event = Event.fromString(_get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN)
-        coll.new(event, cal1)
+        coll.insert(event, cal1)
         event = list(coll.get_events_on(aday))[0]
         assert event.calendar == cal1
 
@@ -233,7 +233,7 @@ class TestCollection:
         coll, vdirs = coll_vdirs
         event = Event.fromString(
             _get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN)
-        coll.new(event, cal1)
+        coll.insert(event, cal1)
         events = coll.get_events_on(aday)
         event = list(events)[0]
         event.update_summary('really simple event')
@@ -249,11 +249,11 @@ class TestCollection:
         coll, vdirs = coll_vdirs
         bday = dt.datetime.combine(aday, dt.time.min)
         anend = bday + dt.timedelta(hours=1)
-        event = icalendar_helpers.new_event(
+        event = icalendar_helpers.new_vevent(
             dtstart=bday, dtend=anend, summary="hi", timezone=utils.BERLIN,
             locale=LOCALE_BERLIN,
         )
-        event = coll.new_event(event.to_ical(), coll.default_calendar_name)
+        event = coll.create_event_from_ics(event.to_ical(), coll.default_calendar_name)
         assert event.allday is False
 
     def test_modify_readonly_calendar(self, coll_vdirs):
@@ -263,7 +263,7 @@ class TestCollection:
         event = Event.fromString(_get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN)
 
         with pytest.raises(khal.khalendar.exceptions.ReadOnlyCalendarError):
-            coll.new(event, cal1)
+            coll.insert(event, cal1)
         with pytest.raises(khal.khalendar.exceptions.ReadOnlyCalendarError):
             # params don't really matter here
             coll.delete('href', 'eteg', cal1)
@@ -273,11 +273,11 @@ class TestCollection:
         assert len(list(coll.search('Event'))) == 0
         event = Event.fromString(
             _get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN)
-        coll.new(event, cal1)
+        coll.insert(event, cal1)
         assert len(list(coll.search('Event'))) == 1
         event = Event.fromString(
             _get_text('event_dt_floating'), calendar=cal1, locale=LOCALE_BERLIN)
-        coll.new(event, cal1)
+        coll.insert(event, cal1)
         assert len(list(coll.search('Search for me'))) == 1
         assert len(list(coll.search('Event'))) == 2
 
@@ -288,7 +288,7 @@ class TestCollection:
         assert len(list(coll.search('Event'))) == 0
         event = Event.fromString(
             _get_text('event_dt_recuid_no_master'), calendar=cal1, locale=LOCALE_BERLIN)
-        coll.new(event, cal1)
+        coll.insert(event, cal1)
         assert len(list(coll.search('Event'))) == 1
 
     def test_search_recurrence_id_only_multi(self, coll_vdirs):
@@ -298,7 +298,7 @@ class TestCollection:
         assert len(list(coll.search('Event'))) == 0
         event = Event.fromString(
             _get_text('event_dt_multi_recuid_no_master'), calendar=cal1, locale=LOCALE_BERLIN)
-        coll.new(event, cal1)
+        coll.insert(event, cal1)
         events = sorted(coll.search('Event'))
         assert len(events) == 2
         assert events[0].format(
@@ -314,9 +314,9 @@ class TestCollection:
             _get_text('event_dt_simple'), calendar=cal1, locale=LOCALE_BERLIN)
         event2 = Event.fromString(
             _get_text('event_dt_simple'), calendar=cal2, locale=LOCALE_BERLIN)
-        coll.new(event1, cal1)
+        coll.insert(event1, cal1)
         sleep(sleep_time)  # make sure the etags are different
-        coll.new(event2, cal2)
+        coll.insert(event2, cal2)
         etag1 = list(vdirs[cal1].list())[0][1]
         etag2 = list(vdirs[cal2].list())[0][1]
         events = list(coll.get_localized(self.astart_berlin, self.aend_berlin))
@@ -334,7 +334,7 @@ class TestCollection:
         coll, vdirs = coll_vdirs
         event = Event.fromString(
             _get_text('invalid_tzoffset'), calendar=cal1, locale=LOCALE_BERLIN)
-        coll.new(event, cal1)
+        coll.insert(event, cal1)
         events = sorted(coll.search('Event'))
         assert len(events) == 1
         assert events[0].format('{start} {end} {title}', dt.date.today()) == \
@@ -434,7 +434,7 @@ def test_default_calendar(coll_vdirs, sleep_time):
     """test if an update to the vdir is detected by the CalendarCollection"""
     coll, vdirs = coll_vdirs
     vdir = vdirs['foobar']
-    event = coll.new_event(event_today, 'foobar')
+    event = coll.create_event_from_ics(event_today, 'foobar')
 
     assert len(list(coll.get_events_on(today))) == 0
 
@@ -460,7 +460,7 @@ def test_default_calendar(coll_vdirs, sleep_time):
 def test_only_update_old_event(coll_vdirs, monkeypatch, sleep_time):
     coll, vdirs = coll_vdirs
 
-    href_one, etag_one = vdirs[cal1].upload(coll.new_event(dedent("""
+    href_one, etag_one = vdirs[cal1].upload(coll.create_event_from_ics(dedent("""
     BEGIN:VEVENT
     UID:meeting-one
     DTSTART;VALUE=DATE:20140909
@@ -471,7 +471,7 @@ def test_only_update_old_event(coll_vdirs, monkeypatch, sleep_time):
 
     sleep(sleep_time)  # Make sure we get a new etag for meeting-two
 
-    href_two, etag_two = vdirs[cal1].upload(coll.new_event(dedent("""
+    href_two, etag_two = vdirs[cal1].upload(coll.create_event_from_ics(dedent("""
     BEGIN:VEVENT
     UID:meeting-two
     DTSTART;VALUE=DATE:20140910
@@ -493,7 +493,7 @@ def test_only_update_old_event(coll_vdirs, monkeypatch, sleep_time):
         return old_update_vevent(href, calendar)
     monkeypatch.setattr(coll, '_update_vevent', _update_vevent)
 
-    href_three, etag_three = vdirs[cal1].upload(coll.new_event(dedent("""
+    href_three, etag_three = vdirs[cal1].upload(coll.create_event_from_ics(dedent("""
     BEGIN:VEVENT
     UID:meeting-three
     DTSTART;VALUE=DATE:20140911
diff --git a/tests/parse_datetime_test.py b/tests/parse_datetime_test.py
index 318781a..b7412f7 100644
--- a/tests/parse_datetime_test.py
+++ b/tests/parse_datetime_test.py
@@ -5,7 +5,7 @@ import pytest
 from freezegun import freeze_time
 
 from khal.exceptions import DateTimeParseError, FatalError
-from khal.icalendar import new_event
+from khal.icalendar import new_vevent
 from khal.parse_datetime import (construct_daynames, eventinfofstr,
                                  guessdatetimefstr, guessrangefstr,
                                  guesstimedeltafstr, timedelta2str,
@@ -31,7 +31,7 @@ def _construct_event(info, locale,
                          )
     if description is not None:
         info["description"] = description
-    event = new_event(
+    event = new_vevent(
         locale=locale, location=location,
         categories=categories, repeat=repeat, until=until,
         alarms=alarm,