[notmuch] SWIG (and particularly Python) bindings

Subject: [notmuch] SWIG (and particularly Python) bindings

Date: Tue, 29 Dec 2009 04:16:43 -0500

To: notmuch

Cc:

From: Ben Gamari


Hey all,

I've been looking at switching away from sup recently to something with
a slightly little less everything-and-the-kitchen-sink philosophy.
Notmuch looks excellent, although it appears that its current front-end
for my editor of choice (vim) is a little lacking in some ways
(operations involving a call to notmuch invariably lock up vi for the
duration of the operation).

Regardless, I thought it might be nice to have access to the notmuch
backend from a language other than C (preferably my high-level language
of choice, python). To this end, I took a few hours today acquainting
myself with SWIG and produced these bindings for Python. Unfortunately,
it doesn't appear that SWIG has particularly good support for
object-oriented C, so the approach I took was using SWIG to generate the
low-level glue, on top of which I wrote a properly structured set of
Python bindings. While I theoretically have full interface coverage,
I unfortunately haven't had a chance to use these for anything useful,
so who knows whether much of it actually works. Regardless, I thought
folks might be interested.

While the bindings are currently in the form of a patch to notmuch
(creating a top-level swig directory in the source tree), they could
certainly be moved out-of-tree if the powers that be don't feel it
appropriate to include them. Unfortunately, the build system is
currently almost entirely independent from the notmuch build system. If
these are to be included in-tree, I would be curious to hear people have
to say about how we might integrate it into the sup build system.

Hope this helps someone. I'll hopefully have a chance to work with the
bindings soon and will send a new set of patches once I think I've found
all of the obvious issues.

- Ben


---
 swig/Makefile        |   18 ++++
 swig/notmuch.py      |  222 ++++++++++++++++++++++++++++++++++++++++++++++++++
 swig/notmuch_funcs.i |    9 ++
 3 files changed, 249 insertions(+), 0 deletions(-)
 create mode 100644 swig/Makefile
 create mode 100644 swig/notmuch.py
 create mode 100644 swig/notmuch_funcs.i

diff --git a/swig/Makefile b/swig/Makefile
new file mode 100644
index 0000000..c01c427
--- /dev/null
+++ b/swig/Makefile
@@ -0,0 +1,18 @@
+include ../Makefile.config
+
+INCLUDES=-I../lib -I/usr/include/python2.6
+CFLAGS=${INCLUDES}
+
+all : python
+
+python : _notmuch_funcs.so notmuch.py
+
+_notmuch_funcs.so : notmuch_funcs_wrap.o
+	g++ -shared ${XAPIAN_LDFLAGS} ${GMIME_LDFLAGS} ${TALLOC_LDFLAGS} -o $@ $< ../lib/notmuch.a
+
+%_wrap.o : %_wrap.c
+	gcc ${CFLAGS} -fPIC -c -o $@ $<
+
+%_wrap.c %.py : %.i 
+	swig -python ${INCLUDES} $<
+
diff --git a/swig/notmuch.py b/swig/notmuch.py
new file mode 100644
index 0000000..95b81ad
--- /dev/null
+++ b/swig/notmuch.py
@@ -0,0 +1,222 @@
+import notmuch_funcs as nm
+from datetime import datetime
+
+class Exception(Exception):
+        def get_message():
+                errors = {
+                        nm.NOTMUCH_STATUS_OUT_OF_MEMORY: 'out of memory',
+                        nm.NOTMUCH_STATUS_READONLY_DATABASE: 'database opened as read-only',
+                        nm.NOTMUCH_STATUS_XAPIAN_EXCEPTION: 'xapian error',
+                        nm.NOTMUCH_STATUS_FILE_ERROR: 'file error',
+                        nm.NOTMUCH_STATUS_FILE_NOT_EMAIL: 'file not email message',
+                        nm.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: 'duplicate message id',
+                        nm.NOTMUCH_STATUS_NULL_POINTER: 'null pointer',
+                        nm.NOTMUCH_STATUS_TAG_TOO_LONG: 'tag name too long',
+                        nm.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: 'unbalanced freeze/thaw',
+                }
+                return errors.get(self.status, 'unknown error')
+
+        def __init__(self, status):
+                self.status = status
+
+def _handle_status(status):
+        if (status != nm.NOTMUCH_STATUS_SUCCESS):
+                raise Exception(status)
+
+class Database(object):
+        MODE_READ_ONLY = nm.NOTMUCH_DATABASE_MODE_READ_ONLY
+        MODE_READ_WRITE = nm.NOTMUCH_DATABASE_MODE_READ_WRITE
+
+        def __init__(self, db):
+                if not db:
+                        raise "Failed to open database"
+                self.db = db
+
+        @staticmethod
+        def create(path):
+                return Database(nm.notmuch_database_create(path))
+
+        @staticmethod
+        def open(path, mode):
+                return Database(nm.notmuch_database_open(path, mode))
+
+        def close(self):
+                nm.notmuch_database_close(self.db)
+
+        def get_path(self):
+                return nm.notmuch_database_get_path(self.db)
+
+        def set_timestamp(self, key, timestamp):
+                _handle_status(nm.notmuch_database_set_timestamp(self.db, key, timestamp))
+
+        def get_timestamp(self, key):
+                return datetime.fromtimestamp(nm.notmuch_database_get_timestamp(self.db, key))
+
+        def add_message(self, filename, message):
+                _handle_status(nm.notmuch_database_add_message(self.db, filename, message.message))
+
+        def find_message(self, message_id):
+                return Message(nm.notmuch_database_find_message(self.db, message_id))
+
+        def get_all_tags(self):
+                return Tags(nm.notmuch_database_get_all_tags(self.db))
+
+        def __destroy__(self):
+                self.close()
+        
+class Query(object):
+        def __init__(self, db, query_string):
+                self.query = nm.notmuch_query_create(db, query_string)
+                if not self.query: # This might not work
+                        raise "Failed to create query"
+
+        def set_sort(self, sort):
+                nm.notmuch_query_set_sort(self.query, sort)
+                
+        def search_threads(self):
+                return Threads(nm.notmuch_query_search_threads(self.query))
+
+        def search_messages(self):
+                return Messages(nm.notmuch_query_search_messages(self.query))
+
+        def count_message(self):
+                return nm.notmuch_query_count_messages(self.query)
+
+        def __destroy__(self):
+                nm.notmuch_query_destroy(self.query)
+
+class Tags(object):
+        def __init__(self, tags):
+                self.tags = tags
+
+        def __iter__(self):
+                return self
+
+        def next(self):
+                if not nm.notmuch_tags_has_more(self.tags):
+                        raise StopIteration
+                else:
+                        h = nm.notmuch_tags_get(self.tags)
+                        nm.notmuch_tags_advance(self.tags)
+                        return h
+
+        def __destroy__(self):
+                nm.notmuch_messages_destroy(self.tags)
+
+class Thread(object):
+        def __init__(self, thread):
+                self.thread = thread
+
+        def get_thread_id(self):
+                return nm.notmuch_thread_get_thread_id(self.thread)
+
+        def get_total_messages(self):
+                return nm.notmuch_thread_total_messages(self.thread)
+        
+        def get_toplevel_messages(self):
+                return Messages(nm.notmuch_thread_get_toplevel_messages(self.thread))
+
+        def get_matched_messages(self):
+                return nm.notmuch_thread_get_matched_messages(self.thread)
+
+        def get_authors(self):
+                return nm.notmuch_thread_get_authors(self.thread)
+
+        def get_subject(self):
+                return nm.notmuch_thread_get_subject(self.thread)
+
+        def get_oldest_date(self):
+                return nm.notmuch_thread_get_oldest_date(self.thread)
+
+        def get_newest_date(self):
+                return nm.notmuch_thread_get_newest_date(self.thread)
+
+        def get_tags(self):
+                return Tags(nm.notmuch_thread_get_tags(self.thread))
+
+        def __destroy__(self):
+                nm.notmuch_thread_destroy(self.thread)
+
+class Threads(object):
+        def __init__(self, threads):
+                self.threads = threads
+
+        def __iter__(self):
+                return self
+
+        def next(self):
+                if not nm.notmuch_threads_has_more(self.threads):
+                        raise StopIteration
+                else:
+                        h = Thread(nm.notmuch_threads_get(self.threads))
+                        nm.notmuch_threads_advance(self.threads)
+                        return h
+
+        def __destroy__(self):
+                nm.notmuch_threads_destroy(self.threads)
+
+class Messages(object):
+        def __init__(self, messages):
+                self.messages = messages
+
+        def __iter__(self):
+                return self
+
+        def next(self):
+                if not nm.notmuch_messages_has_more(self.messages):
+                        raise StopIteration
+                else:
+                        h = Message(nm.notmuch_messages_get(self.messages))
+                        nm.notmuch_messages_advance(self.messages)
+                        return h
+
+        def __destroy__(self):
+                nm.notmuch_messages_destroy(self.messages)
+
+        def collect_tags(self):
+                return Tags(nm.notmuch_messages_collect_tags(self.messages))
+
+class Message(object):
+        def __init__(self, message):
+                self.message = message
+
+        def get_replies(self):
+                return Messages(nm.notmuch_message_get_replies(self.message))
+
+        def get_filename(self):
+                return nm.notmuch_message_get_filename(self.message)
+
+        def get_flag(self, flag):
+                return bool(nm.notmuch_message_get_flag(self.message, flag))
+
+        def set_flag(self, flag, value):
+                return nm.notmuch_message_set_flag(self.message, flag, value)
+
+        def get_date(self):
+                return datetime.fromtimestamp(nm.notmuch_message_get_date(self.message))
+
+        def get_header(self, header):
+                return nm.notmuch_message_get_header(self.message, header)
+
+        def get_tags(self):
+                return Tags(nm.notmuch_message_get_tags(self.message))
+
+        def add_tag(self, tag):
+                _handle_status(nm.notmuch_message_add_tag(self.message, tag))
+
+        def remove_tag(self, tag):
+                _handle_status(nm.notmuch_message_remove_tag(self.message, tag))
+
+        def remove_all_tags(self):
+                nm.notmuch_message_remove_all_tags(self.message)
+
+        def freeze(self):
+                nm.notmuch_message_freeze(self.message)
+
+        def thaw(self):
+                nm.notmuch_message_thaw(self.message)
+
+        def __destroy__(self):
+                nm.notmuch_message_destroy(self.message)
+
+
diff --git a/swig/notmuch_funcs.i b/swig/notmuch_funcs.i
new file mode 100644
index 0000000..cc1826e
--- /dev/null
+++ b/swig/notmuch_funcs.i
@@ -0,0 +1,9 @@
+%module notmuch_funcs
+
+%{
+#define SWIG_FILE_WITH_INIT
+#include "notmuch.h"
+%}
+
+%include "notmuch.h"
+
-- 
1.6.3.3

Thread: