Khal: Codebase review
Project information
- Code repository: https://github.com/pimutils/khal
- License: Expat/MIT License (license and copyright notice)
- Type of program: Python cli and terminal application
- Supported Python versions: 3.4+ (see setup.py or tox.ini)
- Version reviewed: v0.10.1
Documentation
High-level analysis
This analysis is based on setup.py contents and on a first look at the code.
Concepts
- iCalendar: A standard specification for Internet Calendaring and Scheduling. It defines a MIME type and files that comply with this specification usually have an extension of
.icf. - vdir: A storage format for storing calendars and contacts in the filesystem with the goal of being easy to implement. It uses the iCalendar and vCard formats, where each file contains one contact or event.
- etag and ctag: The concept of ETag is used in HTTP as a mechanism to provide web cache validation. In Khal, both etag and ctag are used for synchronizing the SQLite database cache with the actual events and calendars.
Dependencies
Runtime dependencies:
click: composable command line interface toolkitclick_log: logging integration for Clickicalendar: iCalendar parser/generatorurwid: a full-featured console (xterm et al.) user interface librarypyxdg: implementations of freedesktop.org standards in Pythonpytz: world timezone definitions, modern and historicalpython-dateutil: extensions to the standard Python datetime moduleconfigobj: config file reading, writing and validationatomicwrites: atomic file writestzlocal: tzinfo object for the local timezone
Testing dependencies:
freezegun: let tests travel through time by mocking the datetime modulevdirsyncer: synchronize calendars and contacts
Extra dependencies:
proctitle: context manager to set/reset the current process name
Package organization
khal: The main package. It contains all CLI-related code based inClickthat handles the creation of theCalendarCollectionaccording to the configuration and arguments, and delegates to the multiplecontrollerfunctions operations over the collection depending on the requested user action. There are modules foriCalendarutils, converting strings todatetimeoreventsobjects, handling theTerminal(colors, columns), and displaying one or more months along its corresponding events in the console.khal.khalendar: Exports aCalendarCollectionclass which allows access to various calendars stored in vdirs and cached in an SQLiteDB for performance. This collection uses anEventmodel that represents recurring or non-recurring instances of events. This package also contains a module with tools to read/write from/tovdirs.khal.settings: Handles Khal configuration, file paths, and vdir config.khal.ui: Contains multiple widgets forurwid(a console UI library) and all the code that glues together thekhal.khalendarpackage with the UI. In this case, the controlling is performed byurwidwhich handles events to the UI classes defined in this module.
Entry points
Two console scripts:
- khal =
khal.cli:main_khal, which provides a command-result terminal experience. - ikhal =
khal.cli:main_ikhal, which provides a TUI (terminal-user interface) handled by code inkhal.ui.
Configuration and data files
Configuration
- The configuration file is looked up into the list of paths defined by the XDG Base Directory Standard using the
pyxdgpackage. The preferred name of the file isconfig. - The configuration file contents are handle using the
configobjlibrary, and its contents specified by the filekhal.spec.
Calendars (data)
- The path of calendars is defined in the configuration file.
- The used data format is the Vdir Storage Format, defined in the vdirsyncer project (also under the pimutils organization umbrella).
Architecture
The following diagram tries to represent the components and relations of the khal (cli) application. This is not complete by any means, many pieces are missing and some might be wrong.

Object-oriented design
The khalendar package contains code which is more object-oriented than in other khal's modules. Let's analyze its classes:
The CalendarCollection class can be seen as this package interface. It uses composition to make use of the SQLiteDb class implementation and also is composed of one Vdir object per configured calendar. It also allows CRUD of Events saved in the SQLite cache and in vdirs using multiple criteria.
Events are represented as Event class instances, which is a base class for different derived classes:
LocalizedEvent, which handles start and end datetime timezones.FloatingEvent, which are not all day events.AllDayEvent, which are all day events.
The vdir module makes use of many OO design concepts:
- The class
Vdir, which is the one used from the outside, is just a mixture of behavior and interfaces from three other classes it inherits from:VdirBasewhich implements operations in a vdir folder, including handling metadata information or writing, reading and deleting data.DisplayNameMixinwhich provides methods for accesing thedisplaynamemetadata file.ColorMixinwhich provides methods for accessing thecolormetadata file.
- Multiple exception classes inheriting from
VdirError(IOError). - An
Itemclass, used from the controllers to import items in a calendar. - A
Colorclass only used in theColorMixinclass.
Besides the khalendar package, the other interesting package is ui, which contains multiple classes following urwid's object oriented development design.
Python techniques
-
The
loggingmodule is used to show the user warnings in multiple parts of the code. There is a khal logger:logger = logging.getLogger('khal') -
Use
__init__.pyfiles in subpackages to auto-import "exported" functions/classes.-
In
khal/settings/__init__.pyone function and one class are made available throughsettings.get_configandsettings.InvalidSettingsErrorrespectively:from .settings import get_config # noqa from .exceptions import InvalidSettingsError # noqa -
Then, in
khal/cli.pyis enough to do:from .settings import InvalidSettingsError, get_config
-
-
Type hints are used in most method arguments of
backend.SQLiteDb(see code), while they're not used at all in other classes. -
Avoid class instantiation to simulate a pure abstract base class (e.g
Event):def __init__(self, vevents, ref=None, **kwargs): if self.__class__.__name__ == 'Event': raise ValueError('do not initialize this class directly') -
Factory methods using
@classmethodvs@staticmethodinEventbase class (see code):- A simple factory method is provided by
Event._create_calendar()using the@staticmethoddecorator, because the method is logically contained inEventbut does not use any of its methods nor it creates aEventobject. - The
@classmethoddecorator is used to define a couple of more complex factory methods. They are based on VEvents (Event.fromVEvents()) or on a string (Event.fromString()), detecting the correct class to instantiate based on the attributes of the provided elements. - In the same module,
event.py, there is a factory method for creating icalendar vtimezones frompytz.tzinfoobjects. This method is outside any class definition but is there probably because of its relation with events and is, in fact, used only from a method in theEventclass (see code).
- A simple factory method is provided by
-
Comparable events by overloading
__lt__()in the Event class. This class handles the different types of date/time objects that can be used in anEvent-derived object. -
Context managers, for example
SQLiteDb.at_once(), allows to perform multiple operations on the database atomically, by delaying the SQLite transaction commit until the scope is exited without exceptions being raised (see code).@contextlib.contextmanager def at_once(self): assert not self._at_once self._at_once = True try: yield self except: # noqa raise else: self.conn.commit() finally: self._at_once = False -
Example usage in
khalendar.py:update()(see code) where an event is updated both in avdirand in the cache database.
with self._backend.at_once():
event.etag = self._storages[event.calendar].update(event.href, event, event.etag)
self._backend.update(event.raw, event.href, event.etag, calendar=event.calendar)
self._backend.set_ctag(self._local_ctag(event.calendar), calendar=event.calendar)
- A class-based decorator named
cached_propertyin thekhalendar.vdirmodule which transforms an instance method into a descriptor object attribute:# Implements the descriptor protocol making this object a descriptor, # specifically a non-data descriptor because it implements only the # __get__ method. 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__ self.__doc__ = doc or fget.__doc__ self.fget = fget # This method is called once because an attribute is added to the # object via the self.__dict__ object and, in the case of non-data # descriptors, the instance's dictionary entry takes precedence. def __get__(self, obj, cls): if obj is None: # pragma: no cover return self obj.__dict__[self.__name__] = result = self.fget(obj) return result - We can use it like this:
class A: # When this method is processed by the interpreter, an instance # of the class cached_property is created with this function as # argument 'fget'. @cached_property def prop_name(self): return 1 - Usage example
>>> a = A() >>> vars(a) {} >>> print(a.prop_name) 1 >>> vars(a) {'prop_name': 1}
There is much more in khal and I recommend you to take a look at the code and find out by yourself ;-)