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 inClick
that handles the creation of theCalendarCollection
according to the configuration and arguments, and delegates to the multiplecontroller
functions operations over the collection depending on the requested user action. There are modules foriCalendar
utils, converting strings todatetime
orevents
objects, handling theTerminal
(colors, columns), and displaying one or more months along its corresponding events in the console.khal.khalendar
: Exports aCalendarCollection
class which allows access to various calendars stored in vdirs and cached in an SQLiteDB for performance. This collection uses anEvent
model 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.khalendar
package with the UI. In this case, the controlling is performed byurwid
which 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
pyxdg
package. The preferred name of the file isconfig
. - The configuration file contents are handle using the
configobj
library, 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 Event
s saved in the SQLite cache and in vdir
s 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:VdirBase
which implements operations in a vdir folder, including handling metadata information or writing, reading and deleting data.DisplayNameMixin
which provides methods for accesing thedisplayname
metadata file.ColorMixin
which provides methods for accessing thecolor
metadata file.
- Multiple exception classes inheriting from
VdirError(IOError)
. - An
Item
class, used from the controllers to import items in a calendar. - A
Color
class only used in theColorMixin
class.
Besides the khalendar
package, the other interesting package is ui
, which contains multiple classes following urwid
's object oriented development design.
Python techniques
-
The
logging
module is used to show the user warnings in multiple parts of the code. There is a khal logger:logger = logging.getLogger('khal')
-
Use
__init__.py
files in subpackages to auto-import "exported" functions/classes.-
In
khal/settings/__init__.py
one function and one class are made available throughsettings.get_config
andsettings.InvalidSettingsError
respectively:from .settings import get_config # noqa from .exceptions import InvalidSettingsError # noqa
-
Then, in
khal/cli.py
is 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
@classmethod
vs@staticmethod
inEvent
base class (see code):- A simple factory method is provided by
Event._create_calendar()
using the@staticmethod
decorator, because the method is logically contained inEvent
but does not use any of its methods nor it creates aEvent
object. - The
@classmethod
decorator 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.tzinfo
objects. 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 theEvent
class (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 avdir
and 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_property
in thekhalendar.vdir
module 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 ;-)