]> git.cworth.org Git - notmuch/commitdiff
Rename package to notmuch2
authorFloris Bruynooghe <flub@devork.be>
Sun, 17 Nov 2019 16:41:35 +0000 (17:41 +0100)
committerDavid Bremner <david@tethera.net>
Tue, 3 Dec 2019 12:12:30 +0000 (08:12 -0400)
This is based on a previous discussion on the list where this was more
or less seen as the least-bad option.

25 files changed:
bindings/python-cffi/notdb/__init__.py [deleted file]
bindings/python-cffi/notdb/_base.py [deleted file]
bindings/python-cffi/notdb/_build.py [deleted file]
bindings/python-cffi/notdb/_database.py [deleted file]
bindings/python-cffi/notdb/_errors.py [deleted file]
bindings/python-cffi/notdb/_message.py [deleted file]
bindings/python-cffi/notdb/_query.py [deleted file]
bindings/python-cffi/notdb/_tags.py [deleted file]
bindings/python-cffi/notdb/_thread.py [deleted file]
bindings/python-cffi/notmuch2/__init__.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_base.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_build.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_database.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_errors.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_message.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_query.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_tags.py [new file with mode: 0644]
bindings/python-cffi/notmuch2/_thread.py [new file with mode: 0644]
bindings/python-cffi/setup.py
bindings/python-cffi/tests/test_base.py
bindings/python-cffi/tests/test_database.py
bindings/python-cffi/tests/test_message.py
bindings/python-cffi/tests/test_tags.py
bindings/python-cffi/tests/test_thread.py
bindings/python-cffi/tox.ini

diff --git a/bindings/python-cffi/notdb/__init__.py b/bindings/python-cffi/notdb/__init__.py
deleted file mode 100644 (file)
index 67051df..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-"""Pythonic API to the notmuch database.
-
-Creating Objects
-================
-
-Only the :class:`Database` object is meant to be created by the user.
-All other objects should be created from this initial object.  Users
-should consider their signatures implementation details.
-
-Errors
-======
-
-All errors occuring due to errors from the underlying notmuch database
-are subclasses of the :exc:`NotmuchError`.  Due to memory management
-it is possible to try and use an object after it has been freed.  In
-this case a :exc:`ObjectDestoryedError` will be raised.
-
-Memory Management
-=================
-
-Libnotmuch uses a hierarchical memory allocator, this means all
-objects have a strict parent-child relationship and when the parent is
-freed all the children are freed as well.  This has some implications
-for these Python bindings as parent objects need to be kept alive.
-This is normally schielded entirely from the user however and the
-Python objects automatically make sure the right references are kept
-alive.  It is however the reason the :class:`BaseObject` exists as it
-defines the API all Python objects need to implement to work
-correctly.
-
-Collections and Containers
-==========================
-
-Libnotmuch exposes nearly all collections of things as iterators only.
-In these python bindings they have sometimes been exposed as
-:class:`collections.abc.Container` instances or subclasses of this
-like :class:`collections.abc.Set` or :class:`collections.abc.Mapping`
-etc.  This gives a more natural API to work with, e.g. being able to
-treat tags as sets.  However it does mean that the
-:meth:`__contains__`, :meth:`__len__` and frieds methods on these are
-usually more and essentially O(n) rather than O(1) as you might
-usually expect from Python containers.
-"""
-
-from notdb import _capi
-from notdb._base import *
-from notdb._database import *
-from notdb._errors import *
-from notdb._message import *
-from notdb._tags import *
-from notdb._thread import *
-
-
-NOTMUCH_TAG_MAX = _capi.lib.NOTMUCH_TAG_MAX
-del _capi
-
-
-# Re-home all the objects to the package.  This leaves __qualname__ intact.
-for x in locals().copy().values():
-    if hasattr(x, '__module__'):
-        x.__module__ = __name__
-del x
diff --git a/bindings/python-cffi/notdb/_base.py b/bindings/python-cffi/notdb/_base.py
deleted file mode 100644 (file)
index acb6441..0000000
+++ /dev/null
@@ -1,238 +0,0 @@
-import abc
-import collections.abc
-
-from notdb import _capi as capi
-from notdb import _errors as errors
-
-
-__all__ = ['NotmuchObject', 'BinString']
-
-
-class NotmuchObject(metaclass=abc.ABCMeta):
-    """Base notmuch object syntax.
-
-    This base class exists to define the memory management handling
-    required to use the notmuch library.  It is meant as an interface
-    definition rather than a base class, though you can use it as a
-    base class to ensure you don't forget part of the interface.  It
-    only concerns you if you are implementing this package itself
-    rather then using it.
-
-    libnotmuch uses a hierarchical memory allocator, where freeing the
-    memory of a parent object also frees the memory of all child
-    objects.  To make this work seamlessly in Python this package
-    keeps references to parent objects which makes them stay alive
-    correctly under normal circumstances.  When an object finally gets
-    deleted the :meth:`__del__` method will be called to free the
-    memory.
-
-    However during some peculiar situations, e.g. interpreter
-    shutdown, it is possible for the :meth:`__del__` method to have
-    been called, whele there are still references to an object.  This
-    could result in child objects asking their memeory to be freed
-    after the parent has already freed the memory, making things
-    rather unhappy as double frees are not taken lightly in C.  To
-    handle this case all objects need to follow the same protocol to
-    destroy themselves, see :meth:`destroy`.
-
-    Once an object has been destroyed trying to use it should raise
-    the :exc:`ObjectDestroyedError` exception.  For this see also the
-    convenience :class:`MemoryPointer` descriptor in this module which
-    can be used as a pointer to libnotmuch memory.
-    """
-
-    @abc.abstractmethod
-    def __init__(self, parent, *args, **kwargs):
-        """Create a new object.
-
-        Other then for the toplevel :class:`Database` object
-        constructors are only ever called by internal code and not by
-        the user.  Per convention their signature always takes the
-        parent object as first argument.  Feel free to make the rest
-        of the signature match the object's requirement.  The object
-        needs to keep a reference to the parent, so it can check the
-        parent is still alive.
-        """
-
-    @property
-    @abc.abstractmethod
-    def alive(self):
-        """Whether the object is still alive.
-
-        This indicates whether the object is still alive.  The first
-        thing this needs to check is whether the parent object is
-        still alive, if it is not then this object can not be alive
-        either.  If the parent is alive then it depends on whether the
-        memory for this object has been freed yet or not.
-        """
-
-    def __del__(self):
-        self._destroy()
-
-    @abc.abstractmethod
-    def _destroy(self):
-        """Destroy the object, freeing all memory.
-
-        This method needs to destory the object on the
-        libnotmuch-level.  It must ensure it's not been destroyed by
-        it's parent object yet before doing so.  It also must be
-        idempotent.
-        """
-
-
-class MemoryPointer:
-    """Data Descriptor to handle accessing libnotmuch pointers.
-
-    Most :class:`NotmuchObject` instances will have one or more CFFI
-    pointers to C-objects.  Once an object is destroyed this pointer
-    should no longer be used and a :exc:`ObjectDestroyedError`
-    exception should be raised on trying to access it.  This
-    descriptor simplifies implementing this, allowing the creation of
-    an attribute which can be assigned to, but when accessed when the
-    stored value is *None* it will raise the
-    :exc:`ObjectDestroyedError` exception::
-
-       class SomeOjb:
-           _ptr = MemoryPointer()
-
-           def __init__(self, ptr):
-               self._ptr = ptr
-
-           def destroy(self):
-               somehow_free(self._ptr)
-               self._ptr = None
-
-           def do_something(self):
-               return some_libnotmuch_call(self._ptr)
-    """
-
-    def __get__(self, instance, owner):
-        try:
-            val = getattr(instance, self.attr_name, None)
-        except AttributeError:
-            # We're not on 3.6+ and self.attr_name does not exist
-            self.__set_name__(instance, 'dummy')
-            val = getattr(instance, self.attr_name, None)
-        if val is None:
-            raise errors.ObjectDestroyedError()
-        return val
-
-    def __set__(self, instance, value):
-        try:
-            setattr(instance, self.attr_name, value)
-        except AttributeError:
-            # We're not on 3.6+ and self.attr_name does not exist
-            self.__set_name__(instance, 'dummy')
-            setattr(instance, self.attr_name, value)
-
-    def __set_name__(self, instance, name):
-        self.attr_name = '_memptr_{}_{:x}'.format(name, id(instance))
-
-
-class BinString(str):
-    """A str subclass with binary data.
-
-    Most data in libnotmuch should be valid ASCII or valid UTF-8.
-    However since it is a C library these are represented as
-    bytestrings intead which means on an API level we can not
-    guarantee that decoding this to UTF-8 will both succeed and be
-    lossless.  This string type converts bytes to unicode in a lossy
-    way, but also makes the raw bytes available.
-
-    This object is a normal unicode string for most intents and
-    purposes, but you can get the original bytestring back by calling
-    ``bytes()`` on it.
-    """
-
-    def __new__(cls, data, encoding='utf-8', errors='ignore'):
-        if not isinstance(data, bytes):
-            data = bytes(data, encoding=encoding)
-        strdata = str(data, encoding=encoding, errors=errors)
-        inst = super().__new__(cls, strdata)
-        inst._bindata = data
-        return inst
-
-    @classmethod
-    def from_cffi(cls, cdata):
-        """Create a new string from a CFFI cdata pointer."""
-        return cls(capi.ffi.string(cdata))
-
-    def __bytes__(self):
-        return self._bindata
-
-
-class NotmuchIter(NotmuchObject, collections.abc.Iterator):
-    """An iterator for libnotmuch iterators.
-
-    It is tempting to use a generator function instead, but this would
-    not correctly respect the :class:`NotmuchObject` memory handling
-    protocol and in some unsuspecting cornercases cause memory
-    trouble.  You probably want to sublcass this in order to wrap the
-    value returned by :meth:`__next__`.
-
-    :param parent: The parent object.
-    :type parent: NotmuchObject
-    :param iter_p: The CFFI pointer to the C iterator.
-    :type iter_p: cffi.cdata
-    :param fn_destory: The CFFI notmuch_*_destroy function.
-    :param fn_valid: The CFFI notmuch_*_valid function.
-    :param fn_get: The CFFI notmuch_*_get function.
-    :param fn_next: The CFFI notmuch_*_move_to_next function.
-    """
-    _iter_p = MemoryPointer()
-
-    def __init__(self, parent, iter_p,
-                 *, fn_destroy, fn_valid, fn_get, fn_next):
-        self._parent = parent
-        self._iter_p = iter_p
-        self._fn_destroy = fn_destroy
-        self._fn_valid = fn_valid
-        self._fn_get = fn_get
-        self._fn_next = fn_next
-
-    def __del__(self):
-        self._destroy()
-
-    @property
-    def alive(self):
-        if not self._parent.alive:
-            return False
-        try:
-            self._iter_p
-        except errors.ObjectDestroyedError:
-            return False
-        else:
-            return True
-
-    def _destroy(self):
-        if self.alive:
-            try:
-                self._fn_destroy(self._iter_p)
-            except errors.ObjectDestroyedError:
-                pass
-        self._iter_p = None
-
-    def __iter__(self):
-        """Return the iterator itself.
-
-        Note that as this is an iterator and not a container this will
-        not return a new iterator.  Thus any elements already consumed
-        will not be yielded by the :meth:`__next__` method anymore.
-        """
-        return self
-
-    def __next__(self):
-        if not self._fn_valid(self._iter_p):
-            self._destroy()
-            raise StopIteration()
-        obj_p = self._fn_get(self._iter_p)
-        self._fn_next(self._iter_p)
-        return obj_p
-
-    def __repr__(self):
-        try:
-            self._iter_p
-        except errors.ObjectDestroyedError:
-            return '<NotmuchIter (exhausted)>'
-        else:
-            return '<NotmuchIter>'
diff --git a/bindings/python-cffi/notdb/_build.py b/bindings/python-cffi/notdb/_build.py
deleted file mode 100644 (file)
index 6be7e5b..0000000
+++ /dev/null
@@ -1,302 +0,0 @@
-import cffi
-
-
-ffibuilder = cffi.FFI()
-ffibuilder.set_source(
-    'notdb._capi',
-    r"""
-    #include <stdlib.h>
-    #include <time.h>
-    #include <notmuch.h>
-
-    #if LIBNOTMUCH_MAJOR_VERSION < 5
-        #error libnotmuch version not supported by notdb
-    #endif
-    """,
-    include_dirs=['../../lib'],
-    library_dirs=['../../lib'],
-    libraries=['notmuch'],
-)
-ffibuilder.cdef(
-    r"""
-    void free(void *ptr);
-    typedef int... time_t;
-
-    #define LIBNOTMUCH_MAJOR_VERSION ...
-    #define LIBNOTMUCH_MINOR_VERSION ...
-    #define LIBNOTMUCH_MICRO_VERSION ...
-
-    #define NOTMUCH_TAG_MAX ...
-
-    typedef enum _notmuch_status {
-        NOTMUCH_STATUS_SUCCESS = 0,
-        NOTMUCH_STATUS_OUT_OF_MEMORY,
-        NOTMUCH_STATUS_READ_ONLY_DATABASE,
-        NOTMUCH_STATUS_XAPIAN_EXCEPTION,
-        NOTMUCH_STATUS_FILE_ERROR,
-        NOTMUCH_STATUS_FILE_NOT_EMAIL,
-        NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID,
-        NOTMUCH_STATUS_NULL_POINTER,
-        NOTMUCH_STATUS_TAG_TOO_LONG,
-        NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
-        NOTMUCH_STATUS_UNBALANCED_ATOMIC,
-        NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
-        NOTMUCH_STATUS_UPGRADE_REQUIRED,
-        NOTMUCH_STATUS_PATH_ERROR,
-        NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
-        NOTMUCH_STATUS_LAST_STATUS
-    } notmuch_status_t;
-    typedef enum {
-        NOTMUCH_DATABASE_MODE_READ_ONLY = 0,
-        NOTMUCH_DATABASE_MODE_READ_WRITE
-    } notmuch_database_mode_t;
-    typedef int notmuch_bool_t;
-    typedef enum _notmuch_message_flag {
-        NOTMUCH_MESSAGE_FLAG_MATCH,
-        NOTMUCH_MESSAGE_FLAG_EXCLUDED,
-        NOTMUCH_MESSAGE_FLAG_GHOST,
-    } notmuch_message_flag_t;
-    typedef enum {
-        NOTMUCH_SORT_OLDEST_FIRST,
-        NOTMUCH_SORT_NEWEST_FIRST,
-        NOTMUCH_SORT_MESSAGE_ID,
-        NOTMUCH_SORT_UNSORTED
-    } notmuch_sort_t;
-    typedef enum {
-        NOTMUCH_EXCLUDE_FLAG,
-        NOTMUCH_EXCLUDE_TRUE,
-        NOTMUCH_EXCLUDE_FALSE,
-        NOTMUCH_EXCLUDE_ALL
-    } notmuch_exclude_t;
-
-    // These are fully opaque types for us, we only ever use pointers.
-    typedef struct _notmuch_database notmuch_database_t;
-    typedef struct _notmuch_query notmuch_query_t;
-    typedef struct _notmuch_threads notmuch_threads_t;
-    typedef struct _notmuch_thread notmuch_thread_t;
-    typedef struct _notmuch_messages notmuch_messages_t;
-    typedef struct _notmuch_message notmuch_message_t;
-    typedef struct _notmuch_tags notmuch_tags_t;
-    typedef struct _notmuch_string_map_iterator notmuch_message_properties_t;
-    typedef struct _notmuch_directory notmuch_directory_t;
-    typedef struct _notmuch_filenames notmuch_filenames_t;
-    typedef struct _notmuch_config_list notmuch_config_list_t;
-
-    const char *
-    notmuch_status_to_string (notmuch_status_t status);
-
-    notmuch_status_t
-    notmuch_database_create_verbose (const char *path,
-                                     notmuch_database_t **database,
-                                     char **error_message);
-    notmuch_status_t
-    notmuch_database_create (const char *path, notmuch_database_t **database);
-    notmuch_status_t
-    notmuch_database_open_verbose (const char *path,
-                                   notmuch_database_mode_t mode,
-                                   notmuch_database_t **database,
-                                   char **error_message);
-    notmuch_status_t
-    notmuch_database_open (const char *path,
-                           notmuch_database_mode_t mode,
-                           notmuch_database_t **database);
-    notmuch_status_t
-    notmuch_database_close (notmuch_database_t *database);
-    notmuch_status_t
-    notmuch_database_destroy (notmuch_database_t *database);
-    const char *
-    notmuch_database_get_path (notmuch_database_t *database);
-    unsigned int
-    notmuch_database_get_version (notmuch_database_t *database);
-    notmuch_bool_t
-    notmuch_database_needs_upgrade (notmuch_database_t *database);
-    notmuch_status_t
-    notmuch_database_begin_atomic (notmuch_database_t *notmuch);
-    notmuch_status_t
-    notmuch_database_end_atomic (notmuch_database_t *notmuch);
-    unsigned long
-    notmuch_database_get_revision (notmuch_database_t *notmuch,
-                                   const char **uuid);
-    notmuch_status_t
-    notmuch_database_add_message (notmuch_database_t *database,
-                                  const char *filename,
-                                  notmuch_message_t **message);
-    notmuch_status_t
-    notmuch_database_remove_message (notmuch_database_t *database,
-                                     const char *filename);
-    notmuch_status_t
-    notmuch_database_find_message (notmuch_database_t *database,
-                                   const char *message_id,
-                                   notmuch_message_t **message);
-    notmuch_status_t
-    notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
-                                               const char *filename,
-                                               notmuch_message_t **message);
-    notmuch_tags_t *
-    notmuch_database_get_all_tags (notmuch_database_t *db);
-
-    notmuch_query_t *
-    notmuch_query_create (notmuch_database_t *database,
-                          const char *query_string);
-    const char *
-    notmuch_query_get_query_string (const notmuch_query_t *query);
-    notmuch_database_t *
-    notmuch_query_get_database (const notmuch_query_t *query);
-    void
-    notmuch_query_set_omit_excluded (notmuch_query_t *query,
-                                     notmuch_exclude_t omit_excluded);
-    void
-    notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
-    notmuch_sort_t
-    notmuch_query_get_sort (const notmuch_query_t *query);
-    notmuch_status_t
-    notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag);
-    notmuch_status_t
-    notmuch_query_search_threads (notmuch_query_t *query,
-                                  notmuch_threads_t **out);
-    notmuch_status_t
-    notmuch_query_search_messages (notmuch_query_t *query,
-                                   notmuch_messages_t **out);
-    notmuch_status_t
-    notmuch_query_count_messages (notmuch_query_t *query, unsigned int *count);
-    notmuch_status_t
-    notmuch_query_count_threads (notmuch_query_t *query, unsigned *count);
-    void
-    notmuch_query_destroy (notmuch_query_t *query);
-
-    notmuch_bool_t
-    notmuch_threads_valid (notmuch_threads_t *threads);
-    notmuch_thread_t *
-    notmuch_threads_get (notmuch_threads_t *threads);
-    void
-    notmuch_threads_move_to_next (notmuch_threads_t *threads);
-    void
-    notmuch_threads_destroy (notmuch_threads_t *threads);
-
-    const char *
-    notmuch_thread_get_thread_id (notmuch_thread_t *thread);
-    notmuch_messages_t *
-    notmuch_message_get_replies (notmuch_message_t *message);
-    int
-    notmuch_thread_get_total_messages (notmuch_thread_t *thread);
-    notmuch_messages_t *
-    notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
-    notmuch_messages_t *
-    notmuch_thread_get_messages (notmuch_thread_t *thread);
-    int
-    notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
-    const char *
-    notmuch_thread_get_authors (notmuch_thread_t *thread);
-    const char *
-    notmuch_thread_get_subject (notmuch_thread_t *thread);
-    time_t
-    notmuch_thread_get_oldest_date (notmuch_thread_t *thread);
-    time_t
-    notmuch_thread_get_newest_date (notmuch_thread_t *thread);
-    notmuch_tags_t *
-    notmuch_thread_get_tags (notmuch_thread_t *thread);
-    void
-    notmuch_thread_destroy (notmuch_thread_t *thread);
-
-    notmuch_bool_t
-    notmuch_messages_valid (notmuch_messages_t *messages);
-    notmuch_message_t *
-    notmuch_messages_get (notmuch_messages_t *messages);
-    void
-    notmuch_messages_move_to_next (notmuch_messages_t *messages);
-    void
-    notmuch_messages_destroy (notmuch_messages_t *messages);
-    notmuch_tags_t *
-    notmuch_messages_collect_tags (notmuch_messages_t *messages);
-
-    const char *
-    notmuch_message_get_message_id (notmuch_message_t *message);
-    const char *
-    notmuch_message_get_thread_id (notmuch_message_t *message);
-    const char *
-    notmuch_message_get_filename (notmuch_message_t *message);
-    notmuch_filenames_t *
-    notmuch_message_get_filenames (notmuch_message_t *message);
-    notmuch_bool_t
-    notmuch_message_get_flag (notmuch_message_t *message,
-                              notmuch_message_flag_t flag);
-    void
-    notmuch_message_set_flag (notmuch_message_t *message,
-                              notmuch_message_flag_t flag,
-                              notmuch_bool_t value);
-    time_t
-    notmuch_message_get_date  (notmuch_message_t *message);
-    const char *
-    notmuch_message_get_header (notmuch_message_t *message,
-                                const char *header);
-    notmuch_tags_t *
-    notmuch_message_get_tags (notmuch_message_t *message);
-    notmuch_status_t
-    notmuch_message_add_tag (notmuch_message_t *message, const char *tag);
-    notmuch_status_t
-    notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);
-    notmuch_status_t
-    notmuch_message_remove_all_tags (notmuch_message_t *message);
-    notmuch_status_t
-    notmuch_message_maildir_flags_to_tags (notmuch_message_t *message);
-    notmuch_status_t
-    notmuch_message_tags_to_maildir_flags (notmuch_message_t *message);
-    notmuch_status_t
-    notmuch_message_freeze (notmuch_message_t *message);
-    notmuch_status_t
-    notmuch_message_thaw (notmuch_message_t *message);
-    notmuch_status_t
-    notmuch_message_get_property (notmuch_message_t *message,
-                                  const char *key, const char **value);
-    notmuch_status_t
-    notmuch_message_add_property (notmuch_message_t *message,
-                                  const char *key, const char *value);
-    notmuch_status_t
-    notmuch_message_remove_property (notmuch_message_t *message,
-                                     const char *key, const char *value);
-    notmuch_status_t
-    notmuch_message_remove_all_properties (notmuch_message_t *message,
-                                           const char *key);
-    notmuch_message_properties_t *
-    notmuch_message_get_properties (notmuch_message_t *message,
-                                    const char *key, notmuch_bool_t exact);
-    notmuch_bool_t
-    notmuch_message_properties_valid (notmuch_message_properties_t
-                                          *properties);
-    void
-    notmuch_message_properties_move_to_next (notmuch_message_properties_t
-                                                 *properties);
-    const char *
-    notmuch_message_properties_key (notmuch_message_properties_t *properties);
-    const char *
-    notmuch_message_properties_value (notmuch_message_properties_t
-                                          *properties);
-    void
-    notmuch_message_properties_destroy (notmuch_message_properties_t
-                                            *properties);
-    void
-    notmuch_message_destroy (notmuch_message_t *message);
-
-    notmuch_bool_t
-    notmuch_tags_valid (notmuch_tags_t *tags);
-    const char *
-    notmuch_tags_get (notmuch_tags_t *tags);
-    void
-    notmuch_tags_move_to_next (notmuch_tags_t *tags);
-    void
-    notmuch_tags_destroy (notmuch_tags_t *tags);
-
-    notmuch_bool_t
-    notmuch_filenames_valid (notmuch_filenames_t *filenames);
-    const char *
-    notmuch_filenames_get (notmuch_filenames_t *filenames);
-    void
-    notmuch_filenames_move_to_next (notmuch_filenames_t *filenames);
-    void
-    notmuch_filenames_destroy (notmuch_filenames_t *filenames);
-    """
-)
-
-
-if __name__ == '__main__':
-    ffibuilder.compile(verbose=True)
diff --git a/bindings/python-cffi/notdb/_database.py b/bindings/python-cffi/notdb/_database.py
deleted file mode 100644 (file)
index d414082..0000000
+++ /dev/null
@@ -1,705 +0,0 @@
-import collections
-import configparser
-import enum
-import functools
-import os
-import pathlib
-import weakref
-
-import notdb._base as base
-import notdb._capi as capi
-import notdb._errors as errors
-import notdb._message as message
-import notdb._query as querymod
-import notdb._tags as tags
-
-
-__all__ = ['Database', 'AtomicContext', 'DbRevision']
-
-
-def _config_pathname():
-    """Return the path of the configuration file.
-
-    :rtype: pathlib.Path
-    """
-    cfgfname = os.getenv('NOTMUCH_CONFIG', '~/.notmuch-config')
-    return pathlib.Path(os.path.expanduser(cfgfname))
-
-
-class Mode(enum.Enum):
-    READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY
-    READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE
-
-
-class QuerySortOrder(enum.Enum):
-    OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST
-    NEWEST_FIRST = capi.lib.NOTMUCH_SORT_NEWEST_FIRST
-    MESSAGE_ID = capi.lib.NOTMUCH_SORT_MESSAGE_ID
-    UNSORTED = capi.lib.NOTMUCH_SORT_UNSORTED
-
-
-class QueryExclude(enum.Enum):
-    TRUE = capi.lib.NOTMUCH_EXCLUDE_TRUE
-    FLAG = capi.lib.NOTMUCH_EXCLUDE_FLAG
-    FALSE = capi.lib.NOTMUCH_EXCLUDE_FALSE
-    ALL = capi.lib.NOTMUCH_EXCLUDE_ALL
-
-
-class Database(base.NotmuchObject):
-    """Toplevel access to notmuch.
-
-    A :class:`Database` can be opened read-only or read-write.
-    Modifications are not atomic by default, use :meth:`begin_atomic`
-    for atomic updates.  If the underlying database has been modified
-    outside of this class a :exc:`XapianError` will be raised and the
-    instance must be closed and a new one created.
-
-    You can use an instance of this class as a context-manager.
-
-    :cvar MODE: The mode a database can be opened with, an enumeration
-       of ``READ_ONLY`` and ``READ_WRITE``
-    :cvar SORT: The sort order for search results, ``OLDEST_FIRST``,
-       ``NEWEST_FIRST``, ``MESSAGE_ID`` or ``UNSORTED``.
-    :cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``,
-       ``FLAG``, ``FALSE`` or ``ALL``.  See the query documentation
-       for details.
-    :cvar AddedMessage: A namedtuple ``(msg, dup)`` used by
-       :meth:`add` as return value.
-    :cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items.
-       This is used to implement the ``ro`` and ``rw`` string
-       variants.
-
-    :ivar closed: Boolean indicating if the database is closed or
-       still open.
-
-    :param path: The directory of where the database is stored.  If
-       ``None`` the location will be read from the user's
-       configuration file, respecting the ``NOTMUCH_CONFIG``
-       environment variable if set.
-    :type path: str, bytes, os.PathLike or pathlib.Path
-    :param mode: The mode to open the database in.  One of
-       :attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`.  For
-       convenience you can also use the strings ``ro`` for
-       :attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`.
-    :type mode: :attr:`MODE` or str.
-
-    :raises KeyError: if an unknown mode string is used.
-    :raises OSError: or subclasses if the configuration file can not
-       be opened.
-    :raises configparser.Error: or subclasses if the configuration
-       file can not be parsed.
-    :raises NotmuchError: or subclasses for other failures.
-    """
-
-    MODE = Mode
-    SORT = QuerySortOrder
-    EXCLUDE = QueryExclude
-    AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup'])
-    _db_p = base.MemoryPointer()
-    STR_MODE_MAP = {
-        'ro': MODE.READ_ONLY,
-        'rw': MODE.READ_WRITE,
-    }
-
-    def __init__(self, path=None, mode=MODE.READ_ONLY):
-        if isinstance(mode, str):
-            mode = self.STR_MODE_MAP[mode]
-        self.mode = mode
-        if path is None:
-            path = self.default_path()
-        if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
-            path = bytes(path)
-        db_pp = capi.ffi.new('notmuch_database_t **')
-        cmsg = capi.ffi.new('char**')
-        ret = capi.lib.notmuch_database_open_verbose(os.fsencode(path),
-                                                     mode.value, db_pp, cmsg)
-        if cmsg[0]:
-            msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
-            capi.lib.free(cmsg[0])
-        else:
-            msg = None
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret, msg)
-        self._db_p = db_pp[0]
-        self.closed = False
-
-    @classmethod
-    def create(cls, path=None):
-        """Create and open database in READ_WRITE mode.
-
-        This is creates a new notmuch database and returns an opened
-        instance in :attr:`MODE.READ_WRITE` mode.
-
-        :param path: The directory of where the database is stored.  If
-           ``None`` the location will be read from the user's
-           configuration file, respecting the ``NOTMUCH_CONFIG``
-           environment variable if set.
-        :type path: str, bytes or os.PathLike
-
-        :raises OSError: or subclasses if the configuration file can not
-           be opened.
-        :raises configparser.Error: or subclasses if the configuration
-           file can not be parsed.
-        :raises NotmuchError: if the config file does not have the
-           database.path setting.
-        :raises FileError: if the database already exists.
-
-        :returns: The newly created instance.
-        """
-        if path is None:
-            path = cls.default_path()
-        if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
-            path = bytes(path)
-        db_pp = capi.ffi.new('notmuch_database_t **')
-        cmsg = capi.ffi.new('char**')
-        ret = capi.lib.notmuch_database_create_verbose(os.fsencode(path),
-                                                       db_pp, cmsg)
-        if cmsg[0]:
-            msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
-            capi.lib.free(cmsg[0])
-        else:
-            msg = None
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret, msg)
-
-        # Now close the db and let __init__ open it.  Inefficient but
-        # creating is not a hot loop while this allows us to have a
-        # clean API.
-        ret = capi.lib.notmuch_database_destroy(db_pp[0])
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        return cls(path, cls.MODE.READ_WRITE)
-
-    @staticmethod
-    def default_path(cfg_path=None):
-        """Return the path of the user's default database.
-
-        This reads the user's configuration file and returns the
-        default path of the database.
-
-        :param cfg_path: The pathname of the notmuch configuration file.
-           If not specified tries to use the pathname provided in the
-           :env:`NOTMUCH_CONFIG` environment variable and falls back
-           to :file:`~/.notmuch-config.
-        :type cfg_path: str, bytes, os.PathLike or pathlib.Path.
-
-        :returns: The path of the database, which does not necessarily
-           exists.
-        :rtype: pathlib.Path
-        :raises OSError: or subclasses if the configuration file can not
-           be opened.
-        :raises configparser.Error: or subclasses if the configuration
-           file can not be parsed.
-        :raises NotmuchError if the config file does not have the
-           database.path setting.
-        """
-        if not cfg_path:
-            cfg_path = _config_pathname()
-        if not hasattr(os, 'PathLike') and isinstance(cfg_path, pathlib.Path):
-            cfg_path = bytes(cfg_path)
-        parser = configparser.ConfigParser()
-        with open(cfg_path) as fp:
-            parser.read_file(fp)
-        try:
-            return pathlib.Path(parser.get('database', 'path'))
-        except configparser.Error:
-            raise errors.NotmuchError(
-                'No database.path setting in {}'.format(cfg_path))
-
-    def __del__(self):
-        self._destroy()
-
-    @property
-    def alive(self):
-        try:
-            self._db_p
-        except errors.ObjectDestroyedError:
-            return False
-        else:
-            return True
-
-    def _destroy(self):
-        try:
-            ret = capi.lib.notmuch_database_destroy(self._db_p)
-        except errors.ObjectDestroyedError:
-            ret = capi.lib.NOTMUCH_STATUS_SUCCESS
-        else:
-            self._db_p = None
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def close(self):
-        """Close the notmuch database.
-
-        Once closed most operations will fail.  This can still be
-        useful however to explicitly close a database which is opened
-        read-write as this would otherwise stop other processes from
-        reading the database while it is open.
-
-        :raises ObjectDestroyedError: if used after destroyed.
-        """
-        ret = capi.lib.notmuch_database_close(self._db_p)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        self.closed = True
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        self.close()
-
-    @property
-    def path(self):
-        """The pathname of the notmuch database.
-
-        This is returned as a :class:`pathlib.Path` instance.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        try:
-            return self._cache_path
-        except AttributeError:
-            ret = capi.lib.notmuch_database_get_path(self._db_p)
-            self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
-            return self._cache_path
-
-    @property
-    def version(self):
-        """The database format version.
-
-        This is a positive integer.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        try:
-            return self._cache_version
-        except AttributeError:
-            ret = capi.lib.notmuch_database_get_version(self._db_p)
-            self._cache_version = ret
-            return ret
-
-    @property
-    def needs_upgrade(self):
-        """Whether the database should be upgraded.
-
-        If *True* the database can be upgraded using :meth:`upgrade`.
-        Not doing so may result in some operations raising
-        :exc:`UpgradeRequiredError`.
-
-        A read-only database will never be upgradable.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_database_needs_upgrade(self._db_p)
-        return bool(ret)
-
-    def upgrade(self, progress_cb=None):
-        """Upgrade the database to the latest version.
-
-        Upgrade the database, optionally with a progress callback
-        which should be a callable which will be called with a
-        floating point number in the range of [0.0 .. 1.0].
-        """
-        raise NotImplementedError
-
-    def atomic(self):
-        """Return a context manager to perform atomic operations.
-
-        The returned context manager can be used to perform atomic
-        operations on the database.
-
-        .. note:: Unlinke a traditional RDBMS transaction this does
-           not imply durability, it only ensures the changes are
-           performed atomically.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ctx = AtomicContext(self, '_db_p')
-        return ctx
-
-    def revision(self):
-        """The currently committed revision in the database.
-
-        Returned as a ``(revision, uuid)`` namedtuple.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        raw_uuid = capi.ffi.new('char**')
-        rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
-        return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
-
-    def get_directory(self, path):
-        raise NotImplementedError
-
-    def add(self, filename, *, sync_flags=False):
-        """Add a message to the database.
-
-        Add a new message to the notmuch database.  The message is
-        referred to by the pathname of the maildir file.  If the
-        message ID of the new message already exists in the database,
-        this adds ``pathname`` to the list of list of files for the
-        existing message.
-
-        :param filename: The path of the file containing the message.
-        :type filename: str, bytes, os.PathLike or pathlib.Path.
-        :param sync_flags: Whether to sync the known maildir flags to
-           notmuch tags.  See :meth:`Message.flags_to_tags` for
-           details.
-
-        :returns: A tuple where the first item is the newly inserted
-           messages as a :class:`Message` instance, and the second
-           item is a boolean indicating if the message inserted was a
-           duplicate.  This is the namedtuple ``AddedMessage(msg,
-           dup)``.
-        :rtype: Database.AddedMessage
-
-        If an exception is raised, no message was added.
-
-        :raises XapianError: A Xapian exception occurred.
-        :raises FileError: The file referred to by ``pathname`` could
-           not be opened.
-        :raises FileNotEmailError: The file referreed to by
-           ``pathname`` is not recognised as an email message.
-        :raises ReadOnlyDatabaseError: The database is opened in
-           READ_ONLY mode.
-        :raises UpgradeRequiredError: The database must be upgraded
-           first.
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
-            filename = bytes(filename)
-        msg_pp = capi.ffi.new('notmuch_message_t **')
-        ret = capi.lib.notmuch_database_add_message(self._db_p,
-                                                    os.fsencode(filename),
-                                                    msg_pp)
-        ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
-              capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
-        if ret not in ok:
-            raise errors.NotmuchError(ret)
-        msg = message.Message(self, msg_pp[0], db=self)
-        if sync_flags:
-            msg.tags.from_maildir_flags()
-        return self.AddedMessage(
-            msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
-
-    def remove(self, filename):
-        """Remove a message from the notmuch database.
-
-        Removing a message which is not in the database is just a
-        silent nop-operation.
-
-        :param filename: The pathname of the file containing the
-           message to be removed.
-        :type filename: str, bytes, os.PathLike or pathlib.Path.
-
-        :returns: True if the message is still in the database.  This
-           can happen when multiple files contain the same message ID.
-           The true/false distinction is fairly arbitrary, but think
-           of it as ``dup = db.remove_message(name); if dup: ...``.
-        :rtype: bool
-
-        :raises XapianError: A Xapian exception ocurred.
-        :raises ReadOnlyDatabaseError: The database is opened in
-           READ_ONLY mode.
-        :raises UpgradeRequiredError: The database must be upgraded
-           first.
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
-            filename = bytes(filename)
-        ret = capi.lib.notmuch_database_remove_message(self._db_p,
-                                                       os.fsencode(filename))
-        ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
-              capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
-        if ret not in ok:
-            raise errors.NotmuchError(ret)
-        if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
-            return True
-        else:
-            return False
-
-    def find(self, msgid):
-        """Return the message matching the given message ID.
-
-        If a message with the given message ID is found a
-        :class:`Message` instance is returned.  Otherwise a
-        :exc:`LookupError` is raised.
-
-        :param msgid: The message ID to look for.
-        :type msgid: str
-
-        :returns: The message instance.
-        :rtype: Message
-
-        :raises LookupError: If no message was found.
-        :raises OutOfMemoryError: When there is no memory to allocate
-           the message instance.
-        :raises XapianError: A Xapian exception ocurred.
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        msg_pp = capi.ffi.new('notmuch_message_t **')
-        ret = capi.lib.notmuch_database_find_message(self._db_p,
-                                                     msgid.encode(), msg_pp)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        msg_p = msg_pp[0]
-        if msg_p == capi.ffi.NULL:
-            raise LookupError
-        msg = message.Message(self, msg_p, db=self)
-        return msg
-
-    def get(self, filename):
-        """Return the :class:`Message` given a pathname.
-
-        If a message with the given pathname exists in the database
-        return the :class:`Message` instance for the message.
-        Otherwise raise a :exc:`LookupError` exception.
-
-        :param filename: The pathname of the message.
-        :type filename: str, bytes, os.PathLike or pathlib.Path
-
-        :returns: The message instance.
-        :rtype: Message
-
-        :raises LookupError: If no message was found.  This is also
-           a subclass of :exc:`KeyError`.
-        :raises OutOfMemoryError: When there is no memory to allocate
-           the message instance.
-        :raises XapianError: A Xapian exception ocurred.
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
-            filename = bytes(filename)
-        msg_pp = capi.ffi.new('notmuch_message_t **')
-        ret = capi.lib.notmuch_database_find_message_by_filename(
-            self._db_p, os.fsencode(filename), msg_pp)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        msg_p = msg_pp[0]
-        if msg_p == capi.ffi.NULL:
-            raise LookupError
-        msg = message.Message(self, msg_p, db=self)
-        return msg
-
-    @property
-    def tags(self):
-        """Return an immutable set with all tags used in this database.
-
-        This returns an immutable set-like object implementing the
-        collections.abc.Set Abstract Base Class.  Due to the
-        underlying libnotmuch implementation some operations have
-        different performance characteristics then plain set objects.
-        Mainly any lookup operation is O(n) rather then O(1).
-
-        Normal usage treats tags as UTF-8 encoded unicode strings so
-        they are exposed to Python as normal unicode string objects.
-        If you need to handle tags stored in libnotmuch which are not
-        valid unicode do check the :class:`ImmutableTagSet` docs for
-        how to handle this.
-
-        :rtype: ImmutableTagSet
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        try:
-            ref = self._cached_tagset
-        except AttributeError:
-            tagset = None
-        else:
-            tagset = ref()
-        if tagset is None:
-            tagset = tags.ImmutableTagSet(
-                self, '_db_p', capi.lib.notmuch_database_get_all_tags)
-            self._cached_tagset = weakref.ref(tagset)
-        return tagset
-
-    def _create_query(self, query, *,
-                      omit_excluded=EXCLUDE.TRUE,
-                      sort=SORT.UNSORTED,  # Check this default
-                      exclude_tags=None):
-        """Create an internal query object.
-
-        :raises OutOfMemoryError: if no memory is available to
-           allocate the query.
-        """
-        if isinstance(query, str):
-            query = query.encode('utf-8')
-        query_p = capi.lib.notmuch_query_create(self._db_p, query)
-        if query_p == capi.ffi.NULL:
-            raise errors.OutOfMemoryError()
-        capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value)
-        capi.lib.notmuch_query_set_sort(query_p, sort.value)
-        if exclude_tags is not None:
-            for tag in exclude_tags:
-                if isinstance(tag, str):
-                    tag = str.encode('utf-8')
-                capi.lib.notmuch_query_add_tag_exclude(query_p, tag)
-        return querymod.Query(self, query_p)
-
-    def messages(self, query, *,
-                 omit_excluded=EXCLUDE.TRUE,
-                 sort=SORT.UNSORTED,  # Check this default
-                 exclude_tags=None):
-        """Search the database for messages.
-
-        :returns: An iterator over the messages found.
-        :rtype: MessageIter
-
-        :raises OutOfMemoryError: if no memory is available to
-           allocate the query.
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        query = self._create_query(query,
-                                   omit_excluded=omit_excluded,
-                                   sort=sort,
-                                   exclude_tags=exclude_tags)
-        return query.messages()
-
-    def count_messages(self, query, *,
-                       omit_excluded=EXCLUDE.TRUE,
-                       sort=SORT.UNSORTED,  # Check this default
-                       exclude_tags=None):
-        """Search the database for messages.
-
-        :returns: An iterator over the messages found.
-        :rtype: MessageIter
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        query = self._create_query(query,
-                                   omit_excluded=omit_excluded,
-                                   sort=sort,
-                                   exclude_tags=exclude_tags)
-        return query.count_messages()
-
-    def threads(self,  query, *,
-                omit_excluded=EXCLUDE.TRUE,
-                sort=SORT.UNSORTED,  # Check this default
-                exclude_tags=None):
-        query = self._create_query(query,
-                                   omit_excluded=omit_excluded,
-                                   sort=sort,
-                                   exclude_tags=exclude_tags)
-        return query.threads()
-
-    def count_threads(self, query, *,
-                      omit_excluded=EXCLUDE.TRUE,
-                      sort=SORT.UNSORTED,  # Check this default
-                      exclude_tags=None):
-        query = self._create_query(query,
-                                   omit_excluded=omit_excluded,
-                                   sort=sort,
-                                   exclude_tags=exclude_tags)
-        return query.count_threads()
-
-    def status_string(self):
-        raise NotImplementedError
-
-    def __repr__(self):
-        return 'Database(path={self.path}, mode={self.mode})'.format(self=self)
-
-
-class AtomicContext:
-    """Context manager for atomic support.
-
-    This supports the notmuch_database_begin_atomic and
-    notmuch_database_end_atomic API calls.  The object can not be
-    directly instantiated by the user, only via ``Database.atomic``.
-    It does keep a reference to the :class:`Database` instance to keep
-    the C memory alive.
-
-    :raises XapianError: When this is raised at enter time the atomic
-       section is not active.  When it is raised at exit time the
-       atomic section is still active and you may need to try using
-       :meth:`force_end`.
-    :raises ObjectDestroyedError: if used after destoryed.
-    """
-
-    def __init__(self, db, ptr_name):
-        self._db = db
-        self._ptr = lambda: getattr(db, ptr_name)
-
-    def __del__(self):
-        self._destroy()
-
-    @property
-    def alive(self):
-        return self.parent.alive
-
-    def _destroy(self):
-        pass
-
-    def __enter__(self):
-        ret = capi.lib.notmuch_database_begin_atomic(self._ptr())
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        return self
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        ret = capi.lib.notmuch_database_end_atomic(self._ptr())
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def force_end(self):
-        """Force ending the atomic section.
-
-        This can only be called once __exit__ has been called.  It
-        will attept to close the atomic section (again).  This is
-        useful if the original exit raised an exception and the atomic
-        section is still open.  But things are pretty ugly by now.
-
-        :raises XapianError: If exiting fails, the atomic section is
-           not ended.
-        :raises UnbalancedAtomicError: If the database was currently
-           not in an atomic section.
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_database_end_atomic(self._ptr())
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-
-@functools.total_ordering
-class DbRevision:
-    """A database revision.
-
-    The database revision number increases monotonically with each
-    commit to the database.  Which means user-visible changes can be
-    ordered.  This object is sortable with other revisions.  It
-    carries the UUID of the database to ensure it is only ever
-    compared with revisions from the same database.
-    """
-
-    def __init__(self, rev, uuid):
-        self._rev = rev
-        self._uuid = uuid
-
-    @property
-    def rev(self):
-        """The revision number, a positive integer."""
-        return self._rev
-
-    @property
-    def uuid(self):
-        """The UUID of the database, consider this opaque."""
-        return self._uuid
-
-    def __eq__(self, other):
-        if isinstance(other, self.__class__):
-            if self.uuid != other.uuid:
-                return False
-            return self.rev == other.rev
-        else:
-            return NotImplemented
-
-    def __lt__(self, other):
-        if self.__class__ is other.__class__:
-            if self.uuid != other.uuid:
-                return False
-            return self.rev < other.rev
-        else:
-            return NotImplemented
-
-    def __repr__(self):
-        return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self)
diff --git a/bindings/python-cffi/notdb/_errors.py b/bindings/python-cffi/notdb/_errors.py
deleted file mode 100644 (file)
index 924e722..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-from notdb import _capi as capi
-
-
-class NotmuchError(Exception):
-    """Base exception for errors originating from the notmuch library.
-
-    Usually this will have two attributes:
-
-    :status: This is a numeric status code corresponding to the error
-       code in the notmuch library.  This is normally fairly
-       meaningless, it can also often be ``None``.  This exists mostly
-       to easily create new errors from notmuch status codes and
-       should not normally be used by users.
-
-    :message: A user-facing message for the error.  This can
-       occasionally also be ``None``.  Usually you'll want to call
-       ``str()`` on the error object instead to get a sensible
-       message.
-    """
-
-    @classmethod
-    def exc_type(cls, status):
-        """Return correct exception type for notmuch status."""
-        types = {
-            capi.lib.NOTMUCH_STATUS_OUT_OF_MEMORY:
-                OutOfMemoryError,
-            capi.lib.NOTMUCH_STATUS_READ_ONLY_DATABASE:
-                ReadOnlyDatabaseError,
-            capi.lib.NOTMUCH_STATUS_XAPIAN_EXCEPTION:
-                XapianError,
-            capi.lib.NOTMUCH_STATUS_FILE_ERROR:
-                FileError,
-            capi.lib.NOTMUCH_STATUS_FILE_NOT_EMAIL:
-                FileNotEmailError,
-            capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
-                DuplicateMessageIdError,
-            capi.lib.NOTMUCH_STATUS_NULL_POINTER:
-                NullPointerError,
-            capi.lib.NOTMUCH_STATUS_TAG_TOO_LONG:
-                TagTooLongError,
-            capi.lib.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
-                UnbalancedFreezeThawError,
-            capi.lib.NOTMUCH_STATUS_UNBALANCED_ATOMIC:
-                UnbalancedAtomicError,
-            capi.lib.NOTMUCH_STATUS_UNSUPPORTED_OPERATION:
-                UnsupportedOperationError,
-            capi.lib.NOTMUCH_STATUS_UPGRADE_REQUIRED:
-                UpgradeRequiredError,
-            capi.lib.NOTMUCH_STATUS_PATH_ERROR:
-                PathError,
-            capi.lib.NOTMUCH_STATUS_ILLEGAL_ARGUMENT:
-                IllegalArgumentError,
-        }
-        return types[status]
-
-    def __new__(cls, *args, **kwargs):
-        """Return the correct subclass based on status."""
-        # This is simplistic, but the actual __init__ will fail if the
-        # signature is wrong anyway.
-        if args:
-            status = args[0]
-        else:
-            status = kwargs.get('status', None)
-        if status and cls == NotmuchError:
-            exc = cls.exc_type(status)
-            return exc.__new__(exc, *args, **kwargs)
-        else:
-            return super().__new__(cls)
-
-    def __init__(self, status=None, message=None):
-        self.status = status
-        self.message = message
-
-    def __str__(self):
-        if self.message:
-            return self.message
-        elif self.status:
-            return capi.lib.notmuch_status_to_string(self.status)
-        else:
-            return 'Unknown error'
-
-
-class OutOfMemoryError(NotmuchError): pass
-class ReadOnlyDatabaseError(NotmuchError): pass
-class XapianError(NotmuchError): pass
-class FileError(NotmuchError): pass
-class FileNotEmailError(NotmuchError): pass
-class DuplicateMessageIdError(NotmuchError): pass
-class NullPointerError(NotmuchError): pass
-class TagTooLongError(NotmuchError): pass
-class UnbalancedFreezeThawError(NotmuchError): pass
-class UnbalancedAtomicError(NotmuchError): pass
-class UnsupportedOperationError(NotmuchError): pass
-class UpgradeRequiredError(NotmuchError): pass
-class PathError(NotmuchError): pass
-class IllegalArgumentError(NotmuchError): pass
-
-
-class ObjectDestroyedError(NotmuchError):
-    """The object has already been destoryed and it's memory freed.
-
-    This occurs when :meth:`destroy` has been called on the object but
-    you still happen to have access to the object.  This should not
-    normally occur since you should never call :meth:`destroy` by
-    hand.
-    """
-
-    def __str__(self):
-        if self.message:
-            return self.message
-        else:
-            return 'Memory already freed'
diff --git a/bindings/python-cffi/notdb/_message.py b/bindings/python-cffi/notdb/_message.py
deleted file mode 100644 (file)
index 9b2b037..0000000
+++ /dev/null
@@ -1,691 +0,0 @@
-import collections
-import contextlib
-import os
-import pathlib
-import weakref
-
-import notdb._base as base
-import notdb._capi as capi
-import notdb._errors as errors
-import notdb._tags as tags
-
-
-__all__ = ['Message']
-
-
-class Message(base.NotmuchObject):
-    """An email message stored in the notmuch database.
-
-    This should not be directly created, instead it will be returned
-    by calling methods on :class:`Database`.  A message keeps a
-    reference to the database object since the database object can not
-    be released while the message is in use.
-
-    Note that this represents a message in the notmuch database.  For
-    full email functionality you may want to use the :mod:`email`
-    package from Python's standard library.  You could e.g. create
-    this as such::
-
-       notmuch_msg = db.get_message(msgid)  # or from a query
-       parser = email.parser.BytesParser(policy=email.policy.default)
-       with notmuch_msg.path.open('rb) as fp:
-           email_msg = parser.parse(fp)
-
-    Most commonly the functionality provided by notmuch is sufficient
-    to read email however.
-
-    Messages are considered equal when they have the same message ID.
-    This is how libnotmuch treats messages as well, the
-    :meth:`pathnames` function returns multiple results for
-    duplicates.
-
-    :param parent: The parent object.  This is probably one off a
-       :class:`Database`, :class:`Thread` or :class:`Query`.
-    :type parent: NotmuchObject
-    :param db: The database instance this message is associated with.
-       This could be the same as the parent.
-    :type db: Database
-    :param msg_p: The C pointer to the ``notmuch_message_t``.
-    :type msg_p: <cdata>
-
-    :param dup: Whether the message was a duplicate on insertion.
-
-    :type dup: None or bool
-    """
-    _msg_p = base.MemoryPointer()
-
-    def __init__(self, parent, msg_p, *, db):
-        self._parent = parent
-        self._msg_p = msg_p
-        self._db = db
-
-    @property
-    def alive(self):
-        if not self._parent.alive:
-            return False
-        try:
-            self._msg_p
-        except errors.ObjectDestroyedError:
-            return False
-        else:
-            return True
-
-    def __del__(self):
-        self._destroy()
-
-    def _destroy(self):
-        if self.alive:
-            capi.lib.notmuch_message_destroy(self._msg_p)
-        self._msg_p = None
-
-    @property
-    def messageid(self):
-        """The message ID as a string.
-
-        The message ID is decoded with the ignore error handler.  This
-        is fine as long as the message ID is well formed.  If it is
-        not valid ASCII then this will be lossy.  So if you need to be
-        able to write the exact same message ID back you should use
-        :attr:`messageidb`.
-
-        Note that notmuch will decode the message ID value and thus
-        strip off the surrounding ``<`` and ``>`` characters.  This is
-        different from Python's :mod:`email` package behaviour which
-        leaves these characters in place.
-
-        :returns: The message ID.
-        :rtype: :class:`BinString`, this is a normal str but calling
-           bytes() on it will return the original bytes used to create
-           it.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
-        return base.BinString(capi.ffi.string(ret))
-
-    @property
-    def threadid(self):
-        """The thread ID.
-
-        The thread ID is decoded with the surrogateescape error
-        handler so that it is possible to reconstruct the original
-        thread ID if it is not valid UTF-8.
-
-        :returns: The thread ID.
-        :rtype: :class:`BinString`, this is a normal str but calling
-           bytes() on it will return the original bytes used to create
-           it.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
-        return base.BinString(capi.ffi.string(ret))
-
-    @property
-    def path(self):
-        """A pathname of the message as a pathlib.Path instance.
-
-        If multiple files in the database contain the same message ID
-        this will be just one of the files, chosen at random.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_filename(self._msg_p)
-        return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
-
-    @property
-    def pathb(self):
-        """A pathname of the message as a bytes object.
-
-        See :attr:`path` for details, this is the same but does return
-        the path as a bytes object which is faster but less convenient.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_filename(self._msg_p)
-        return capi.ffi.string(ret)
-
-    def filenames(self):
-        """Return an iterator of all files for this message.
-
-        If multiple files contained the same message ID they will all
-        be returned here.  The files are returned as intances of
-        :class:`pathlib.Path`.
-
-        :returns: Iterator yielding :class:`pathlib.Path` instances.
-        :rtype: iter
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
-        return PathIter(self, fnames_p)
-
-    def filenamesb(self):
-        """Return an iterator of all files for this message.
-
-        This is like :meth:`pathnames` but the files are returned as
-        byte objects instead.
-
-        :returns: Iterator yielding :class:`bytes` instances.
-        :rtype: iter
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
-        return FilenamesIter(self, fnames_p)
-
-    @property
-    def ghost(self):
-        """Indicates whether this message is a ghost message.
-
-        A ghost message if a message which we know exists, but it has
-        no files or content associated with it.  This can happen if
-        it was referenced by some other message.  Only the
-        :attr:`messageid` and :attr:`threadid` attributes are valid
-        for it.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_flag(
-            self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
-        return bool(ret)
-
-    @property
-    def excluded(self):
-        """Indicates whether this message was excluded from the query.
-
-        When a message is created from a search, sometimes messages
-        that where excluded by the search query could still be
-        returned by it, e.g. because they are part of a thread
-        matching the query.  the :meth:`Database.query` method allows
-        these messages to be flagged, which results in this property
-        being set to *True*.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_flag(
-            self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
-        return bool(ret)
-
-    @property
-    def date(self):
-        """The message date as an integer.
-
-        The time the message was sent as an integer number of seconds
-        since the *epoch*, 1 Jan 1970.  This is derived from the
-        message's header, you can get the original header value with
-        :meth:`header`.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        return capi.lib.notmuch_message_get_date(self._msg_p)
-
-    def header(self, name):
-        """Return the value of the named header.
-
-        Returns the header from notmuch, some common headers are
-        stored in the database, others are read from the file.
-        Headers are returned with their newlines stripped and
-        collapsed concatenated together if they occur multiple times.
-        You may be better off using the standard library email
-        package's ``email.message_from_file(msg.path.open())`` if that
-        is not sufficient for you.
-
-        :param header: Case-insensitive header name to retrieve.
-        :type header: str or bytes
-
-        :returns: The header value, an empty string if the header is
-           not present.
-        :rtype: str
-
-        :raises LookupError: if the header is not present.
-        :raises NullPointerError: For unexpected notmuch errors.
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        # The returned is supposedly guaranteed to be UTF-8.  Header
-        # names must be ASCII as per RFC x822.
-        if isinstance(name, str):
-            name = name.encode('ascii')
-        ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
-        if ret == capi.ffi.NULL:
-            raise errors.NullPointerError()
-        hdr = capi.ffi.string(ret)
-        if not hdr:
-            raise LookupError
-        return hdr.decode(encoding='utf-8')
-
-    @property
-    def tags(self):
-        """The tags associated with the message.
-
-        This behaves as a set.  But removing and adding items to the
-        set removes and adds them to the message in the database.
-
-        :raises ReadOnlyDatabaseError: When manipulating tags on a
-           database opened in read-only mode.
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        try:
-            ref = self._cached_tagset
-        except AttributeError:
-            tagset = None
-        else:
-            tagset = ref()
-        if tagset is None:
-            tagset = tags.MutableTagSet(
-                self, '_msg_p', capi.lib.notmuch_message_get_tags)
-            self._cached_tagset = weakref.ref(tagset)
-        return tagset
-
-    @contextlib.contextmanager
-    def frozen(self):
-        """Context manager to freeze the message state.
-
-        This allows you to perform atomic tag updates::
-
-           with msg.frozen():
-               msg.tags.clear()
-               msg.tags.add('foo')
-
-        Using This would ensure the message never ends up with no tags
-        applied at all.
-
-        It is safe to nest calls to this context manager.
-
-        :raises ReadOnlyDatabaseError: if the database is opened in
-           read-only mode.
-        :raises UnbalancedFreezeThawError: if you somehow managed to
-           call __exit__ of this context manager more than once.  Why
-           did you do that?
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_freeze(self._msg_p)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        self._frozen = True
-        try:
-            yield
-        except Exception:
-            # Only way to "rollback" these changes is to destroy
-            # ourselves and re-create.  Behold.
-            msgid = self.messageid
-            self._destroy()
-            with contextlib.suppress(Exception):
-                new = self._db.find(msgid)
-                self._msg_p = new._msg_p
-                new._msg_p = None
-                del new
-            raise
-        else:
-            ret = capi.lib.notmuch_message_thaw(self._msg_p)
-            if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-                raise errors.NotmuchError(ret)
-            self._frozen = False
-
-    @property
-    def properties(self):
-        """A map of arbitrary key-value pairs associated with the message.
-
-        Be aware that properties may be used by other extensions to
-        store state in.  So delete or modify with care.
-
-        The properties map is somewhat special.  It is essentially a
-        multimap-like structure where each key can have multiple
-        values.  Therefore accessing a single item using
-        :meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
-        will only return you the *first* item if there are multiple
-        and thus are only recommended if you know there to be only one
-        value.
-
-        Instead the map has an additional :meth:`PropertiesMap.all`
-        method which can be used to retrieve all properties of a given
-        key.  This method also allows iterating of a a subset of the
-        keys starting with a given prefix.
-        """
-        try:
-            ref = self._cached_props
-        except AttributeError:
-            props = None
-        else:
-            props = ref()
-        if props is None:
-            props = PropertiesMap(self, '_msg_p')
-            self._cached_props = weakref.ref(props)
-        return props
-
-    def replies(self):
-        """Return an iterator of all replies to this message.
-
-        This method will only work if the message was created from a
-        thread.  Otherwise it will yield no results.
-
-        :returns: An iterator yielding :class:`Message` instances.
-        :rtype: MessageIter
-        """
-        # The notmuch_messages_valid call accepts NULL and this will
-        # become an empty iterator, raising StopIteration immediately.
-        # Hence no return value checking here.
-        msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p)
-        return MessageIter(self, msgs_p, db=self._db)
-
-    def __hash__(self):
-        return hash(self.messageid)
-
-    def __eq__(self, other):
-        if isinstance(other, self.__class__):
-            return self.messageid == other.messageid
-
-
-class FilenamesIter(base.NotmuchIter):
-    """Iterator for binary filenames objects."""
-
-    def __init__(self, parent, iter_p):
-        super().__init__(parent, iter_p,
-                         fn_destroy=capi.lib.notmuch_filenames_destroy,
-                         fn_valid=capi.lib.notmuch_filenames_valid,
-                         fn_get=capi.lib.notmuch_filenames_get,
-                         fn_next=capi.lib.notmuch_filenames_move_to_next)
-
-    def __next__(self):
-        fname = super().__next__()
-        return capi.ffi.string(fname)
-
-
-class PathIter(FilenamesIter):
-    """Iterator for pathlib.Path objects."""
-
-    def __next__(self):
-        fname = super().__next__()
-        return pathlib.Path(os.fsdecode(fname))
-
-
-class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
-    """A mutable mapping to manage properties.
-
-    Both keys and values of properties are supposed to be UTF-8
-    strings in libnotmuch.  However since the uderlying API uses
-    bytestrings you can use either str or bytes to represent keys and
-    all returned keys and values use :class:`BinString`.
-
-    Also be aware that ``iter(this_map)`` will return duplicate keys,
-    while the :class:`collections.abc.KeysView` returned by
-    :meth:`keys` is a :class:`collections.abc.Set` subclass.  This
-    means the former will yield duplicate keys while the latter won't.
-    It also means ``len(list(iter(this_map)))`` could be different
-    than ``len(this_map.keys())``.  ``len(this_map)`` will correspond
-    with the lenght of the default iterator.
-
-    Be aware that libnotmuch exposes all of this as iterators, so
-    quite a few operations have O(n) performance instead of the usual
-    O(1).
-    """
-    Property = collections.namedtuple('Property', ['key', 'value'])
-    _marker = object()
-
-    def __init__(self, msg, ptr_name):
-        self._msg = msg
-        self._ptr = lambda: getattr(msg, ptr_name)
-
-    @property
-    def alive(self):
-        if not self._msg.alive:
-            return False
-        try:
-            self._ptr
-        except errors.ObjectDestroyedError:
-            return False
-        else:
-            return True
-
-    def _destroy(self):
-        pass
-
-    def __iter__(self):
-        """Return an iterator which iterates over the keys.
-
-        Be aware that a single key may have multiple values associated
-        with it, if so it will appear multiple times here.
-        """
-        iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
-        return PropertiesKeyIter(self, iter_p)
-
-    def __len__(self):
-        iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
-        it = base.NotmuchIter(
-            self, iter_p,
-            fn_destroy=capi.lib.notmuch_message_properties_destroy,
-            fn_valid=capi.lib.notmuch_message_properties_valid,
-            fn_get=capi.lib.notmuch_message_properties_key,
-            fn_next=capi.lib.notmuch_message_properties_move_to_next,
-        )
-        return len(list(it))
-
-    def __getitem__(self, key):
-        """Return **the first** peroperty associated with a key."""
-        if isinstance(key, str):
-            key = key.encode('utf-8')
-        value_pp = capi.ffi.new('char**')
-        ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        if value_pp[0] == capi.ffi.NULL:
-            raise KeyError
-        return base.BinString.from_cffi(value_pp[0])
-
-    def keys(self):
-        """Return a :class:`collections.abc.KeysView` for this map.
-
-        Even when keys occur multiple times this is a subset of set()
-        so will only contain them once.
-        """
-        return collections.abc.KeysView({k: None for k in self})
-
-    def items(self):
-        """Return a :class:`collections.abc.ItemsView` for this map.
-
-        The ItemsView treats a ``(key, value)`` pair as unique, so
-        dupcliate ``(key, value)`` pairs will be merged together.
-        However duplicate keys with different values will be returned.
-        """
-        items = set()
-        props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
-        while capi.lib.notmuch_message_properties_valid(props_p):
-            key = capi.lib.notmuch_message_properties_key(props_p)
-            value = capi.lib.notmuch_message_properties_value(props_p)
-            items.add((base.BinString.from_cffi(key),
-                       base.BinString.from_cffi(value)))
-            capi.lib.notmuch_message_properties_move_to_next(props_p)
-        capi.lib.notmuch_message_properties_destroy(props_p)
-        return PropertiesItemsView(items)
-
-    def values(self):
-        """Return a :class:`collecions.abc.ValuesView` for this map.
-
-        All unique property values are included in the view.
-        """
-        values = set()
-        props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
-        while capi.lib.notmuch_message_properties_valid(props_p):
-            value = capi.lib.notmuch_message_properties_value(props_p)
-            values.add(base.BinString.from_cffi(value))
-            capi.lib.notmuch_message_properties_move_to_next(props_p)
-        capi.lib.notmuch_message_properties_destroy(props_p)
-        return PropertiesValuesView(values)
-
-    def __setitem__(self, key, value):
-        """Add a key-value pair to the properties.
-
-        You may prefer to use :meth:`add` for clarity since this
-        method usually implies implicit overwriting of an existing key
-        if it exists, while for properties this is not the case.
-        """
-        self.add(key, value)
-
-    def add(self, key, value):
-        """Add a key-value pair to the properties."""
-        if isinstance(key, str):
-            key = key.encode('utf-8')
-        if isinstance(value, str):
-            value = value.encode('utf-8')
-        ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def __delitem__(self, key):
-        """Remove all properties with this key."""
-        if isinstance(key, str):
-            key = key.encode('utf-8')
-        ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def remove(self, key, value):
-        """Remove a key-value pair from the properties."""
-        if isinstance(key, str):
-            key = key.encode('utf-8')
-        if isinstance(value, str):
-            value = value.encode('utf-8')
-        ret = capi.lib.notmuch_message_remove_property(self._ptr(), key, value)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def pop(self, key, default=_marker):
-        try:
-            value = self[key]
-        except KeyError:
-            if default is self._marker:
-                raise
-            else:
-                return default
-        else:
-            self.remove(key, value)
-            return value
-
-    def popitem(self):
-        try:
-            key = next(iter(self))
-        except StopIteration:
-            raise KeyError
-        value = self.pop(key)
-        return (key, value)
-
-    def clear(self):
-        ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
-                                                             capi.ffi.NULL)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def getall(self, prefix='', *, exact=False):
-        """Return an iterator yielding all properties for a given key prefix.
-
-        The returned iterator yields all peroperties which start with
-        a given key prefix as ``(key, value)`` namedtuples.  If called
-        with ``exact=True`` then only properties which exactly match
-        the prefix are returned, those a key longer than the prefix
-        will not be included.
-
-        :param prefix: The prefix of the key.
-        """
-        if isinstance(prefix, str):
-            prefix = prefix.encode('utf-8')
-        props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
-                                                          prefix, exact)
-        return PropertiesIter(self, props_p)
-
-
-class PropertiesKeyIter(base.NotmuchIter):
-
-    def __init__(self, parent, iter_p):
-        super().__init__(
-            parent,
-            iter_p,
-            fn_destroy=capi.lib.notmuch_message_properties_destroy,
-            fn_valid=capi.lib.notmuch_message_properties_valid,
-            fn_get=capi.lib.notmuch_message_properties_key,
-            fn_next=capi.lib.notmuch_message_properties_move_to_next)
-
-    def __next__(self):
-        item = super().__next__()
-        return base.BinString.from_cffi(item)
-
-
-class PropertiesIter(base.NotmuchIter):
-
-    def __init__(self, parent, iter_p):
-        super().__init__(
-            parent,
-            iter_p,
-            fn_destroy=capi.lib.notmuch_message_properties_destroy,
-            fn_valid=capi.lib.notmuch_message_properties_valid,
-            fn_get=capi.lib.notmuch_message_properties_key,
-            fn_next=capi.lib.notmuch_message_properties_move_to_next,
-        )
-
-    def __next__(self):
-        if not self._fn_valid(self._iter_p):
-            self._destroy()
-            raise StopIteration
-        key = capi.lib.notmuch_message_properties_key(self._iter_p)
-        value = capi.lib.notmuch_message_properties_value(self._iter_p)
-        capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
-        return PropertiesMap.Property(base.BinString.from_cffi(key),
-                                      base.BinString.from_cffi(value))
-
-
-class PropertiesItemsView(collections.abc.Set):
-
-    __slots__ = ('_items',)
-
-    def __init__(self, items):
-        self._items = items
-
-    @classmethod
-    def _from_iterable(self, it):
-        return set(it)
-
-    def __len__(self):
-        return len(self._items)
-
-    def __contains__(self, item):
-        return item in self._items
-
-    def __iter__(self):
-        yield from self._items
-
-
-collections.abc.ItemsView.register(PropertiesItemsView)
-
-
-class PropertiesValuesView(collections.abc.Set):
-
-    __slots__ = ('_values',)
-
-    def __init__(self, values):
-        self._values = values
-
-    def __len__(self):
-        return len(self._values)
-
-    def __contains__(self, value):
-        return value in self._values
-
-    def __iter__(self):
-        yield from self._values
-
-
-collections.abc.ValuesView.register(PropertiesValuesView)
-
-
-class MessageIter(base.NotmuchIter):
-
-    def __init__(self, parent, msgs_p, *, db):
-        self._db = db
-        super().__init__(parent, msgs_p,
-                         fn_destroy=capi.lib.notmuch_messages_destroy,
-                         fn_valid=capi.lib.notmuch_messages_valid,
-                         fn_get=capi.lib.notmuch_messages_get,
-                         fn_next=capi.lib.notmuch_messages_move_to_next)
-
-    def __next__(self):
-        msg_p = super().__next__()
-        return Message(self, msg_p, db=self._db)
diff --git a/bindings/python-cffi/notdb/_query.py b/bindings/python-cffi/notdb/_query.py
deleted file mode 100644 (file)
index 613aaf1..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-from notdb import _base as base
-from notdb import _capi as capi
-from notdb import _errors as errors
-from notdb import _message as message
-from notdb import _thread as thread
-
-
-__all__ = []
-
-
-class Query(base.NotmuchObject):
-    """Private, minimal query object.
-
-    This is not meant for users and is not a full implementation of
-    the query API.  It is only an intermediate used internally to
-    match libnotmuch's memory management.
-    """
-    _query_p = base.MemoryPointer()
-
-    def __init__(self, db, query_p):
-        self._db = db
-        self._query_p = query_p
-
-    @property
-    def alive(self):
-        if not self._db.alive:
-            return False
-        try:
-            self._query_p
-        except errors.ObjectDestroyedError:
-            return False
-        else:
-            return True
-
-    def __del__(self):
-        self._destroy()
-
-    def _destroy(self):
-        if self.alive:
-            capi.lib.notmuch_query_destroy(self._query_p)
-        self._query_p = None
-
-    @property
-    def query(self):
-        """The query string as seen by libnotmuch."""
-        q = capi.lib.notmuch_query_get_query_string(self._query_p)
-        return base.BinString.from_cffi(q)
-
-    def messages(self):
-        """Return an iterator over all the messages found by the query.
-
-        This executes the query and returns an iterator over the
-        :class:`Message` objects found.
-        """
-        msgs_pp = capi.ffi.new('notmuch_messages_t**')
-        ret = capi.lib.notmuch_query_search_messages(self._query_p, msgs_pp)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        return message.MessageIter(self, msgs_pp[0], db=self._db)
-
-    def count_messages(self):
-        """Return the number of messages matching this query."""
-        count_p = capi.ffi.new('unsigned int *')
-        ret = capi.lib.notmuch_query_count_messages(self._query_p, count_p)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        return count_p[0]
-
-    def threads(self):
-        """Return an iterator over all the threads found by the query."""
-        threads_pp = capi.ffi.new('notmuch_threads_t **')
-        ret = capi.lib.notmuch_query_search_threads(self._query_p, threads_pp)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        return thread.ThreadIter(self, threads_pp[0], db=self._db)
-
-    def count_threads(self):
-        """Return the number of threads matching this query."""
-        count_p = capi.ffi.new('unsigned int *')
-        ret = capi.lib.notmuch_query_count_threads(self._query_p, count_p)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        return count_p[0]
diff --git a/bindings/python-cffi/notdb/_tags.py b/bindings/python-cffi/notdb/_tags.py
deleted file mode 100644 (file)
index a25a226..0000000
+++ /dev/null
@@ -1,338 +0,0 @@
-import collections.abc
-
-import notdb._base as base
-import notdb._capi as capi
-import notdb._errors as errors
-
-
-__all__ = ['ImmutableTagSet', 'MutableTagSet', 'TagsIter']
-
-
-class ImmutableTagSet(base.NotmuchObject, collections.abc.Set):
-    """The tags associated with a message thread or whole database.
-
-    Both a thread as well as the database expose the union of all tags
-    in messages associated with them.  This exposes these as a
-    :class:`collections.abc.Set` object.
-
-    Note that due to the underlying notmuch API the performance of the
-    implementation is not the same as you would expect from normal
-    sets.  E.g. the :meth:`__contains__` and :meth:`__len__` are O(n)
-    rather then O(1).
-
-    Tags are internally stored as bytestrings but normally exposed as
-    unicode strings using the UTF-8 encoding and the *ignore* decoder
-    error handler.  However the :meth:`iter` method can be used to
-    return tags as bytestrings or using a different error handler.
-
-    Note that when doing arithmetic operations on tags, this class
-    will return a plain normal set as it is no longer associated with
-    the message.
-
-    :param parent: the parent object
-    :param ptr_name: the name of the attribute on the parent which will
-       return the memory pointer.  This allows this object to
-       access the pointer via the parent's descriptor and thus
-       trigger :class:`MemoryPointer`'s memory safety.
-    :param cffi_fn: the callable CFFI wrapper to retrieve the tags
-       iter.  This can be one of notmuch_database_get_all_tags,
-       notmuch_thread_get_tags or notmuch_message_get_tags.
-    """
-
-    def __init__(self, parent, ptr_name, cffi_fn):
-        self._parent = parent
-        self._ptr = lambda: getattr(parent, ptr_name)
-        self._cffi_fn = cffi_fn
-
-    def __del__(self):
-        self._destroy()
-
-    @property
-    def alive(self):
-        return self._parent.alive
-
-    def _destroy(self):
-        pass
-
-    @classmethod
-    def _from_iterable(cls, it):
-        return set(it)
-
-    def __iter__(self):
-        """Return an iterator over the tags.
-
-        Tags are yielded as unicode strings, decoded using the
-        "ignore" error handler.
-
-        :raises NullPointerError: If the iterator can not be created.
-        """
-        return self.iter(encoding='utf-8', errors='ignore')
-
-    def iter(self, *, encoding=None, errors='strict'):
-        """Aternate iterator constructor controlling string decoding.
-
-        Tags are stored as bytes in the notmuch database, in Python
-        it's easier to work with unicode strings and thus is what the
-        normal iterator returns.  However this method allows you to
-        specify how you would like to get the tags, defaulting to the
-        bytestring representation instead of unicode strings.
-
-        :param encoding: Which codec to use.  The default *None* does not
-           decode at all and will return the unmodified bytes.
-           Otherwise this is passed on to :func:`str.decode`.
-        :param errors: If using a codec, this is the error handler.
-           See :func:`str.decode` to which this is passed on.
-
-        :raises NullPointerError: When things do not go as planned.
-        """
-        # self._cffi_fn should point either to
-        # notmuch_database_get_all_tags, notmuch_thread_get_tags or
-        # notmuch_message_get_tags.  nothmuch.h suggests these never
-        # fail, let's handle NULL anyway.
-        tags_p = self._cffi_fn(self._ptr())
-        if tags_p == capi.ffi.NULL:
-            raise errors.NullPointerError()
-        tags = TagsIter(self, tags_p, encoding=encoding, errors=errors)
-        return tags
-
-    def __len__(self):
-        return sum(1 for t in self)
-
-    def __contains__(self, tag):
-        if isinstance(tag, str):
-            tag = tag.encode()
-        for msg_tag in self.iter():
-            if tag == msg_tag:
-                return True
-        else:
-            return False
-
-    def __eq__(self, other):
-        return tuple(sorted(self.iter())) == tuple(sorted(other.iter()))
-
-    def __hash__(self):
-        return hash(tuple(self.iter()))
-
-    def __repr__(self):
-        return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
-            name=self.__class__.__name__,
-            addr=id(self),
-            tags=', '.join(repr(t) for t in self))
-
-
-class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
-    """The tags associated with a message.
-
-    This is a :class:`collections.abc.MutableSet` object which can be
-    used to manipulate the tags of a message.
-
-    Note that due to the underlying notmuch API the performance of the
-    implementation is not the same as you would expect from normal
-    sets.  E.g. the ``in`` operator and variants are O(n) rather then
-    O(1).
-
-    Tags are bytestrings and calling ``iter()`` will return an
-    iterator yielding bytestrings.  However the :meth:`iter` method
-    can be used to return tags as unicode strings, while all other
-    operations accept either byestrings or unicode strings.  In case
-    unicode strings are used they will be encoded using utf-8 before
-    being passed to notmuch.
-    """
-
-    # Since we subclass ImmutableTagSet we inherit a __hash__.  But we
-    # are mutable, setting it to None will make the Python machinary
-    # recognise us as unhashable.
-    __hash__ = None
-
-    def add(self, tag):
-        """Add a tag to the message.
-
-        :param tag: The tag to add.
-        :type tag: str or bytes.  A str will be encoded using UTF-8.
-
-        :param sync_flags: Whether to sync the maildir flags with the
-           new set of tags.  Leaving this as *None* respects the
-           configuration set in the database, while *True* will always
-           sync and *False* will never sync.
-        :param sync_flags: NoneType or bool
-
-        :raises TypeError: If the tag is not a valid type.
-        :raises TagTooLongError: If the added tag exceeds the maximum
-           lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
-        :raises ReadOnlyDatabaseError: If the database is opened in
-           read-only mode.
-        """
-        if isinstance(tag, str):
-            tag = tag.encode()
-        if not isinstance(tag, bytes):
-            raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
-        ret = capi.lib.notmuch_message_add_tag(self._ptr(), tag)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def discard(self, tag):
-        """Remove a tag from the message.
-
-        :param tag: The tag to remove.
-        :type tag: str of bytes.  A str will be encoded using UTF-8.
-        :param sync_flags: Whether to sync the maildir flags with the
-           new set of tags.  Leaving this as *None* respects the
-           configuration set in the database, while *True* will always
-           sync and *False* will never sync.
-        :param sync_flags: NoneType or bool
-
-        :raises TypeError: If the tag is not a valid type.
-        :raises TagTooLongError: If the tag exceeds the maximum
-           lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
-        :raises ReadOnlyDatabaseError: If the database is opened in
-           read-only mode.
-        """
-        if isinstance(tag, str):
-            tag = tag.encode()
-        if not isinstance(tag, bytes):
-            raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
-        ret = capi.lib.notmuch_message_remove_tag(self._ptr(), tag)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def clear(self):
-        """Remove all tags from the message.
-
-        :raises ReadOnlyDatabaseError: If the database is opened in
-           read-only mode.
-        """
-        ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def from_maildir_flags(self):
-        """Update the tags based on the state in the message's maildir flags.
-
-        This function examines the filenames of 'message' for maildir
-        flags, and adds or removes tags on 'message' as follows when
-        these flags are present:
-
-        Flag    Action if present
-        ----    -----------------
-        'D'     Adds the "draft" tag to the message
-        'F'     Adds the "flagged" tag to the message
-        'P'     Adds the "passed" tag to the message
-        'R'     Adds the "replied" tag to the message
-        'S'     Removes the "unread" tag from the message
-
-        For each flag that is not present, the opposite action
-        (add/remove) is performed for the corresponding tags.
-
-        Flags are identified as trailing components of the filename
-        after a sequence of ":2,".
-
-        If there are multiple filenames associated with this message,
-        the flag is considered present if it appears in one or more
-        filenames. (That is, the flags from the multiple filenames are
-        combined with the logical OR operator.)
-        """
-        ret = capi.lib.notmuch_message_maildir_flags_to_tags(self._ptr())
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def to_maildir_flags(self):
-        """Update the message's maildir flags based on the notmuch tags.
-
-        If the message's filename is in a maildir directory, that is a
-        directory named ``new`` or ``cur``, and has a valid maildir
-        filename then the flags will be added as such:
-
-        'D' if the message has the "draft" tag
-        'F' if the message has the "flagged" tag
-        'P' if the message has the "passed" tag
-        'R' if the message has the "replied" tag
-        'S' if the message does not have the "unread" tag
-
-        Any existing flags unmentioned in the list above will be
-        preserved in the renaming.
-
-        Also, if this filename is in a directory named "new", rename it to
-        be within the neighboring directory named "cur".
-
-        In case there are multiple files associated with the message
-        all filenames will get the same logic applied.
-        """
-        ret = capi.lib.notmuch_message_tags_to_maildir_flags(self._ptr())
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-
-class TagsIter(base.NotmuchObject, collections.abc.Iterator):
-    """Iterator over tags.
-
-    This is only an interator, not a container so calling
-    :meth:`__iter__` does not return a new, replenished iterator but
-    only itself.
-
-    :param parent: The parent object to keep alive.
-    :param tags_p: The CFFI pointer to the C-level tags iterator.
-    :param encoding: Which codec to use.  The default *None* does not
-       decode at all and will return the unmodified bytes.
-       Otherwise this is passed on to :func:`str.decode`.
-    :param errors: If using a codec, this is the error handler.
-       See :func:`str.decode` to which this is passed on.
-
-    :raises ObjectDestoryedError: if used after destroyed.
-    """
-    _tags_p = base.MemoryPointer()
-
-    def __init__(self, parent, tags_p, *, encoding=None, errors='strict'):
-        self._parent = parent
-        self._tags_p = tags_p
-        self._encoding = encoding
-        self._errors = errors
-
-    def __del__(self):
-        self._destroy()
-
-    @property
-    def alive(self):
-        if not self._parent.alive:
-            return False
-        try:
-            self._tags_p
-        except errors.ObjectDestroyedError:
-            return False
-        else:
-            return True
-
-    def _destroy(self):
-        if self.alive:
-            try:
-                capi.lib.notmuch_tags_destroy(self._tags_p)
-            except errors.ObjectDestroyedError:
-                pass
-        self._tags_p = None
-
-    def __iter__(self):
-        """Return the iterator itself.
-
-        Note that as this is an iterator and not a container this will
-        not return a new iterator.  Thus any elements already consumed
-        will not be yielded by the :meth:`__next__` method anymore.
-        """
-        return self
-
-    def __next__(self):
-        if not capi.lib.notmuch_tags_valid(self._tags_p):
-            self._destroy()
-            raise StopIteration()
-        tag_p = capi.lib.notmuch_tags_get(self._tags_p)
-        tag = capi.ffi.string(tag_p)
-        if self._encoding:
-            tag = tag.decode(encoding=self._encoding, errors=self._errors)
-        capi.lib.notmuch_tags_move_to_next(self._tags_p)
-        return tag
-
-    def __repr__(self):
-        try:
-            self._tags_p
-        except errors.ObjectDestroyedError:
-            return '<TagsIter (exhausted)>'
-        else:
-            return '<TagsIter>'
diff --git a/bindings/python-cffi/notdb/_thread.py b/bindings/python-cffi/notdb/_thread.py
deleted file mode 100644 (file)
index e1ef6b0..0000000
+++ /dev/null
@@ -1,190 +0,0 @@
-import collections.abc
-import weakref
-
-from notdb import _base as base
-from notdb import _capi as capi
-from notdb import _errors as errors
-from notdb import _message as message
-from notdb import _tags as tags
-
-
-__all__ = ['Thread']
-
-
-class Thread(base.NotmuchObject, collections.abc.Iterable):
-    _thread_p = base.MemoryPointer()
-
-    def __init__(self, parent, thread_p, *, db):
-        self._parent = parent
-        self._thread_p = thread_p
-        self._db = db
-
-    @property
-    def alive(self):
-        if not self._parent.alive:
-            return False
-        try:
-            self._thread_p
-        except errors.ObjectDestroyedError:
-            return False
-        else:
-            return True
-
-    def __del__(self):
-        self._destroy()
-
-    def _destroy(self):
-        if self.alive:
-            capi.lib.notmuch_thread_destroy(self._thread_p)
-        self._thread_p = None
-
-    @property
-    def threadid(self):
-        """The thread ID as a :class:`BinString`.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_thread_get_thread_id(self._thread_p)
-        return base.BinString.from_cffi(ret)
-
-    def __len__(self):
-        """Return the number of messages in the thread.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        return capi.lib.notmuch_thread_get_total_messages(self._thread_p)
-
-    def toplevel(self):
-        """Return an iterator of the toplevel messages.
-
-        :returns: An iterator yielding :class:`Message` instances.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        msgs_p = capi.lib.notmuch_thread_get_toplevel_messages(self._thread_p)
-        return message.MessageIter(self, msgs_p, db=self._db)
-
-    def __iter__(self):
-        """Return an iterator over all the messages in the thread.
-
-        :returns: An iterator yielding :class:`Message` instances.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        msgs_p = capi.lib.notmuch_thread_get_messages(self._thread_p)
-        return message.MessageIter(self, msgs_p, db=self._db)
-
-    @property
-    def matched(self):
-        """The number of messages in this thread which matched the query.
-
-        Of the messages in the thread this gives the count of messages
-        which did directly match the search query which this thread
-        originates from.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        return capi.lib.notmuch_thread_get_matched_messages(self._thread_p)
-
-    @property
-    def authors(self):
-        """A comma-separated string of all authors in the thread.
-
-        Authors of messages which matched the query the thread was
-        retrieved from will be at the head of the string, ordered by
-        date of their messages.  Following this will be the authors of
-        the other messages in the thread, also ordered by date of
-        their messages.  Both groups of authors are separated by the
-        ``|`` character.
-
-        :returns: The stringified list of authors.
-        :rtype: BinString
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_thread_get_authors(self._thread_p)
-        return base.BinString.from_cffi(ret)
-
-    @property
-    def subject(self):
-        """The subject of the thread, taken from the first message.
-
-        The thread's subject is taken to be the subject of the first
-        message according to query sort order.
-
-        :returns: The thread's subject.
-        :rtype: BinString
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_thread_get_subject(self._thread_p)
-        return base.BinString.from_cffi(ret)
-
-    @property
-    def first(self):
-        """Return the date of the oldest message in the thread.
-
-        The time the first message was sent as an integer number of
-        seconds since the *epoch*, 1 Jan 1970.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        return capi.lib.notmuch_thread_get_oldest_date(self._thread_p)
-
-    @property
-    def last(self):
-        """Return the date of the newest message in the thread.
-
-        The time the last message was sent as an integer number of
-        seconds since the *epoch*, 1 Jan 1970.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        return capi.lib.notmuch_thread_get_newest_date(self._thread_p)
-
-    @property
-    def tags(self):
-        """Return an immutable set with all tags used in this thread.
-
-        This returns an immutable set-like object implementing the
-        collections.abc.Set Abstract Base Class.  Due to the
-        underlying libnotmuch implementation some operations have
-        different performance characteristics then plain set objects.
-        Mainly any lookup operation is O(n) rather then O(1).
-
-        Normal usage treats tags as UTF-8 encoded unicode strings so
-        they are exposed to Python as normal unicode string objects.
-        If you need to handle tags stored in libnotmuch which are not
-        valid unicode do check the :class:`ImmutableTagSet` docs for
-        how to handle this.
-
-        :rtype: ImmutableTagSet
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        try:
-            ref = self._cached_tagset
-        except AttributeError:
-            tagset = None
-        else:
-            tagset = ref()
-        if tagset is None:
-            tagset = tags.ImmutableTagSet(
-                self, '_thread_p', capi.lib.notmuch_thread_get_tags)
-            self._cached_tagset = weakref.ref(tagset)
-        return tagset
-
-
-class ThreadIter(base.NotmuchIter):
-
-    def __init__(self, parent, threads_p, *, db):
-        self._db = db
-        super().__init__(parent, threads_p,
-                         fn_destroy=capi.lib.notmuch_threads_destroy,
-                         fn_valid=capi.lib.notmuch_threads_valid,
-                         fn_get=capi.lib.notmuch_threads_get,
-                         fn_next=capi.lib.notmuch_threads_move_to_next)
-
-    def __next__(self):
-        thread_p = super().__next__()
-        return Thread(self, thread_p, db=self._db)
diff --git a/bindings/python-cffi/notmuch2/__init__.py b/bindings/python-cffi/notmuch2/__init__.py
new file mode 100644 (file)
index 0000000..4d76ec1
--- /dev/null
@@ -0,0 +1,62 @@
+"""Pythonic API to the notmuch database.
+
+Creating Objects
+================
+
+Only the :class:`Database` object is meant to be created by the user.
+All other objects should be created from this initial object.  Users
+should consider their signatures implementation details.
+
+Errors
+======
+
+All errors occuring due to errors from the underlying notmuch database
+are subclasses of the :exc:`NotmuchError`.  Due to memory management
+it is possible to try and use an object after it has been freed.  In
+this case a :exc:`ObjectDestoryedError` will be raised.
+
+Memory Management
+=================
+
+Libnotmuch uses a hierarchical memory allocator, this means all
+objects have a strict parent-child relationship and when the parent is
+freed all the children are freed as well.  This has some implications
+for these Python bindings as parent objects need to be kept alive.
+This is normally schielded entirely from the user however and the
+Python objects automatically make sure the right references are kept
+alive.  It is however the reason the :class:`BaseObject` exists as it
+defines the API all Python objects need to implement to work
+correctly.
+
+Collections and Containers
+==========================
+
+Libnotmuch exposes nearly all collections of things as iterators only.
+In these python bindings they have sometimes been exposed as
+:class:`collections.abc.Container` instances or subclasses of this
+like :class:`collections.abc.Set` or :class:`collections.abc.Mapping`
+etc.  This gives a more natural API to work with, e.g. being able to
+treat tags as sets.  However it does mean that the
+:meth:`__contains__`, :meth:`__len__` and frieds methods on these are
+usually more and essentially O(n) rather than O(1) as you might
+usually expect from Python containers.
+"""
+
+from notmuch2 import _capi
+from notmuch2._base import *
+from notmuch2._database import *
+from notmuch2._errors import *
+from notmuch2._message import *
+from notmuch2._tags import *
+from notmuch2._thread import *
+
+
+NOTMUCH_TAG_MAX = _capi.lib.NOTMUCH_TAG_MAX
+del _capi
+
+
+# Re-home all the objects to the package.  This leaves __qualname__ intact.
+for x in locals().copy().values():
+    if hasattr(x, '__module__'):
+        x.__module__ = __name__
+del x
diff --git a/bindings/python-cffi/notmuch2/_base.py b/bindings/python-cffi/notmuch2/_base.py
new file mode 100644 (file)
index 0000000..3125814
--- /dev/null
@@ -0,0 +1,238 @@
+import abc
+import collections.abc
+
+from notmuch2 import _capi as capi
+from notmuch2 import _errors as errors
+
+
+__all__ = ['NotmuchObject', 'BinString']
+
+
+class NotmuchObject(metaclass=abc.ABCMeta):
+    """Base notmuch object syntax.
+
+    This base class exists to define the memory management handling
+    required to use the notmuch library.  It is meant as an interface
+    definition rather than a base class, though you can use it as a
+    base class to ensure you don't forget part of the interface.  It
+    only concerns you if you are implementing this package itself
+    rather then using it.
+
+    libnotmuch uses a hierarchical memory allocator, where freeing the
+    memory of a parent object also frees the memory of all child
+    objects.  To make this work seamlessly in Python this package
+    keeps references to parent objects which makes them stay alive
+    correctly under normal circumstances.  When an object finally gets
+    deleted the :meth:`__del__` method will be called to free the
+    memory.
+
+    However during some peculiar situations, e.g. interpreter
+    shutdown, it is possible for the :meth:`__del__` method to have
+    been called, whele there are still references to an object.  This
+    could result in child objects asking their memeory to be freed
+    after the parent has already freed the memory, making things
+    rather unhappy as double frees are not taken lightly in C.  To
+    handle this case all objects need to follow the same protocol to
+    destroy themselves, see :meth:`destroy`.
+
+    Once an object has been destroyed trying to use it should raise
+    the :exc:`ObjectDestroyedError` exception.  For this see also the
+    convenience :class:`MemoryPointer` descriptor in this module which
+    can be used as a pointer to libnotmuch memory.
+    """
+
+    @abc.abstractmethod
+    def __init__(self, parent, *args, **kwargs):
+        """Create a new object.
+
+        Other then for the toplevel :class:`Database` object
+        constructors are only ever called by internal code and not by
+        the user.  Per convention their signature always takes the
+        parent object as first argument.  Feel free to make the rest
+        of the signature match the object's requirement.  The object
+        needs to keep a reference to the parent, so it can check the
+        parent is still alive.
+        """
+
+    @property
+    @abc.abstractmethod
+    def alive(self):
+        """Whether the object is still alive.
+
+        This indicates whether the object is still alive.  The first
+        thing this needs to check is whether the parent object is
+        still alive, if it is not then this object can not be alive
+        either.  If the parent is alive then it depends on whether the
+        memory for this object has been freed yet or not.
+        """
+
+    def __del__(self):
+        self._destroy()
+
+    @abc.abstractmethod
+    def _destroy(self):
+        """Destroy the object, freeing all memory.
+
+        This method needs to destory the object on the
+        libnotmuch-level.  It must ensure it's not been destroyed by
+        it's parent object yet before doing so.  It also must be
+        idempotent.
+        """
+
+
+class MemoryPointer:
+    """Data Descriptor to handle accessing libnotmuch pointers.
+
+    Most :class:`NotmuchObject` instances will have one or more CFFI
+    pointers to C-objects.  Once an object is destroyed this pointer
+    should no longer be used and a :exc:`ObjectDestroyedError`
+    exception should be raised on trying to access it.  This
+    descriptor simplifies implementing this, allowing the creation of
+    an attribute which can be assigned to, but when accessed when the
+    stored value is *None* it will raise the
+    :exc:`ObjectDestroyedError` exception::
+
+       class SomeOjb:
+           _ptr = MemoryPointer()
+
+           def __init__(self, ptr):
+               self._ptr = ptr
+
+           def destroy(self):
+               somehow_free(self._ptr)
+               self._ptr = None
+
+           def do_something(self):
+               return some_libnotmuch_call(self._ptr)
+    """
+
+    def __get__(self, instance, owner):
+        try:
+            val = getattr(instance, self.attr_name, None)
+        except AttributeError:
+            # We're not on 3.6+ and self.attr_name does not exist
+            self.__set_name__(instance, 'dummy')
+            val = getattr(instance, self.attr_name, None)
+        if val is None:
+            raise errors.ObjectDestroyedError()
+        return val
+
+    def __set__(self, instance, value):
+        try:
+            setattr(instance, self.attr_name, value)
+        except AttributeError:
+            # We're not on 3.6+ and self.attr_name does not exist
+            self.__set_name__(instance, 'dummy')
+            setattr(instance, self.attr_name, value)
+
+    def __set_name__(self, instance, name):
+        self.attr_name = '_memptr_{}_{:x}'.format(name, id(instance))
+
+
+class BinString(str):
+    """A str subclass with binary data.
+
+    Most data in libnotmuch should be valid ASCII or valid UTF-8.
+    However since it is a C library these are represented as
+    bytestrings intead which means on an API level we can not
+    guarantee that decoding this to UTF-8 will both succeed and be
+    lossless.  This string type converts bytes to unicode in a lossy
+    way, but also makes the raw bytes available.
+
+    This object is a normal unicode string for most intents and
+    purposes, but you can get the original bytestring back by calling
+    ``bytes()`` on it.
+    """
+
+    def __new__(cls, data, encoding='utf-8', errors='ignore'):
+        if not isinstance(data, bytes):
+            data = bytes(data, encoding=encoding)
+        strdata = str(data, encoding=encoding, errors=errors)
+        inst = super().__new__(cls, strdata)
+        inst._bindata = data
+        return inst
+
+    @classmethod
+    def from_cffi(cls, cdata):
+        """Create a new string from a CFFI cdata pointer."""
+        return cls(capi.ffi.string(cdata))
+
+    def __bytes__(self):
+        return self._bindata
+
+
+class NotmuchIter(NotmuchObject, collections.abc.Iterator):
+    """An iterator for libnotmuch iterators.
+
+    It is tempting to use a generator function instead, but this would
+    not correctly respect the :class:`NotmuchObject` memory handling
+    protocol and in some unsuspecting cornercases cause memory
+    trouble.  You probably want to sublcass this in order to wrap the
+    value returned by :meth:`__next__`.
+
+    :param parent: The parent object.
+    :type parent: NotmuchObject
+    :param iter_p: The CFFI pointer to the C iterator.
+    :type iter_p: cffi.cdata
+    :param fn_destory: The CFFI notmuch_*_destroy function.
+    :param fn_valid: The CFFI notmuch_*_valid function.
+    :param fn_get: The CFFI notmuch_*_get function.
+    :param fn_next: The CFFI notmuch_*_move_to_next function.
+    """
+    _iter_p = MemoryPointer()
+
+    def __init__(self, parent, iter_p,
+                 *, fn_destroy, fn_valid, fn_get, fn_next):
+        self._parent = parent
+        self._iter_p = iter_p
+        self._fn_destroy = fn_destroy
+        self._fn_valid = fn_valid
+        self._fn_get = fn_get
+        self._fn_next = fn_next
+
+    def __del__(self):
+        self._destroy()
+
+    @property
+    def alive(self):
+        if not self._parent.alive:
+            return False
+        try:
+            self._iter_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def _destroy(self):
+        if self.alive:
+            try:
+                self._fn_destroy(self._iter_p)
+            except errors.ObjectDestroyedError:
+                pass
+        self._iter_p = None
+
+    def __iter__(self):
+        """Return the iterator itself.
+
+        Note that as this is an iterator and not a container this will
+        not return a new iterator.  Thus any elements already consumed
+        will not be yielded by the :meth:`__next__` method anymore.
+        """
+        return self
+
+    def __next__(self):
+        if not self._fn_valid(self._iter_p):
+            self._destroy()
+            raise StopIteration()
+        obj_p = self._fn_get(self._iter_p)
+        self._fn_next(self._iter_p)
+        return obj_p
+
+    def __repr__(self):
+        try:
+            self._iter_p
+        except errors.ObjectDestroyedError:
+            return '<NotmuchIter (exhausted)>'
+        else:
+            return '<NotmuchIter>'
diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py
new file mode 100644 (file)
index 0000000..3ba3e55
--- /dev/null
@@ -0,0 +1,302 @@
+import cffi
+
+
+ffibuilder = cffi.FFI()
+ffibuilder.set_source(
+    'notmuch2._capi',
+    r"""
+    #include <stdlib.h>
+    #include <time.h>
+    #include <notmuch.h>
+
+    #if LIBNOTMUCH_MAJOR_VERSION < 5
+        #error libnotmuch version not supported by notmuch2 python bindings
+    #endif
+    """,
+    include_dirs=['../../lib'],
+    library_dirs=['../../lib'],
+    libraries=['notmuch'],
+)
+ffibuilder.cdef(
+    r"""
+    void free(void *ptr);
+    typedef int... time_t;
+
+    #define LIBNOTMUCH_MAJOR_VERSION ...
+    #define LIBNOTMUCH_MINOR_VERSION ...
+    #define LIBNOTMUCH_MICRO_VERSION ...
+
+    #define NOTMUCH_TAG_MAX ...
+
+    typedef enum _notmuch_status {
+        NOTMUCH_STATUS_SUCCESS = 0,
+        NOTMUCH_STATUS_OUT_OF_MEMORY,
+        NOTMUCH_STATUS_READ_ONLY_DATABASE,
+        NOTMUCH_STATUS_XAPIAN_EXCEPTION,
+        NOTMUCH_STATUS_FILE_ERROR,
+        NOTMUCH_STATUS_FILE_NOT_EMAIL,
+        NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID,
+        NOTMUCH_STATUS_NULL_POINTER,
+        NOTMUCH_STATUS_TAG_TOO_LONG,
+        NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
+        NOTMUCH_STATUS_UNBALANCED_ATOMIC,
+        NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
+        NOTMUCH_STATUS_UPGRADE_REQUIRED,
+        NOTMUCH_STATUS_PATH_ERROR,
+        NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
+        NOTMUCH_STATUS_LAST_STATUS
+    } notmuch_status_t;
+    typedef enum {
+        NOTMUCH_DATABASE_MODE_READ_ONLY = 0,
+        NOTMUCH_DATABASE_MODE_READ_WRITE
+    } notmuch_database_mode_t;
+    typedef int notmuch_bool_t;
+    typedef enum _notmuch_message_flag {
+        NOTMUCH_MESSAGE_FLAG_MATCH,
+        NOTMUCH_MESSAGE_FLAG_EXCLUDED,
+        NOTMUCH_MESSAGE_FLAG_GHOST,
+    } notmuch_message_flag_t;
+    typedef enum {
+        NOTMUCH_SORT_OLDEST_FIRST,
+        NOTMUCH_SORT_NEWEST_FIRST,
+        NOTMUCH_SORT_MESSAGE_ID,
+        NOTMUCH_SORT_UNSORTED
+    } notmuch_sort_t;
+    typedef enum {
+        NOTMUCH_EXCLUDE_FLAG,
+        NOTMUCH_EXCLUDE_TRUE,
+        NOTMUCH_EXCLUDE_FALSE,
+        NOTMUCH_EXCLUDE_ALL
+    } notmuch_exclude_t;
+
+    // These are fully opaque types for us, we only ever use pointers.
+    typedef struct _notmuch_database notmuch_database_t;
+    typedef struct _notmuch_query notmuch_query_t;
+    typedef struct _notmuch_threads notmuch_threads_t;
+    typedef struct _notmuch_thread notmuch_thread_t;
+    typedef struct _notmuch_messages notmuch_messages_t;
+    typedef struct _notmuch_message notmuch_message_t;
+    typedef struct _notmuch_tags notmuch_tags_t;
+    typedef struct _notmuch_string_map_iterator notmuch_message_properties_t;
+    typedef struct _notmuch_directory notmuch_directory_t;
+    typedef struct _notmuch_filenames notmuch_filenames_t;
+    typedef struct _notmuch_config_list notmuch_config_list_t;
+
+    const char *
+    notmuch_status_to_string (notmuch_status_t status);
+
+    notmuch_status_t
+    notmuch_database_create_verbose (const char *path,
+                                     notmuch_database_t **database,
+                                     char **error_message);
+    notmuch_status_t
+    notmuch_database_create (const char *path, notmuch_database_t **database);
+    notmuch_status_t
+    notmuch_database_open_verbose (const char *path,
+                                   notmuch_database_mode_t mode,
+                                   notmuch_database_t **database,
+                                   char **error_message);
+    notmuch_status_t
+    notmuch_database_open (const char *path,
+                           notmuch_database_mode_t mode,
+                           notmuch_database_t **database);
+    notmuch_status_t
+    notmuch_database_close (notmuch_database_t *database);
+    notmuch_status_t
+    notmuch_database_destroy (notmuch_database_t *database);
+    const char *
+    notmuch_database_get_path (notmuch_database_t *database);
+    unsigned int
+    notmuch_database_get_version (notmuch_database_t *database);
+    notmuch_bool_t
+    notmuch_database_needs_upgrade (notmuch_database_t *database);
+    notmuch_status_t
+    notmuch_database_begin_atomic (notmuch_database_t *notmuch);
+    notmuch_status_t
+    notmuch_database_end_atomic (notmuch_database_t *notmuch);
+    unsigned long
+    notmuch_database_get_revision (notmuch_database_t *notmuch,
+                                   const char **uuid);
+    notmuch_status_t
+    notmuch_database_add_message (notmuch_database_t *database,
+                                  const char *filename,
+                                  notmuch_message_t **message);
+    notmuch_status_t
+    notmuch_database_remove_message (notmuch_database_t *database,
+                                     const char *filename);
+    notmuch_status_t
+    notmuch_database_find_message (notmuch_database_t *database,
+                                   const char *message_id,
+                                   notmuch_message_t **message);
+    notmuch_status_t
+    notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
+                                               const char *filename,
+                                               notmuch_message_t **message);
+    notmuch_tags_t *
+    notmuch_database_get_all_tags (notmuch_database_t *db);
+
+    notmuch_query_t *
+    notmuch_query_create (notmuch_database_t *database,
+                          const char *query_string);
+    const char *
+    notmuch_query_get_query_string (const notmuch_query_t *query);
+    notmuch_database_t *
+    notmuch_query_get_database (const notmuch_query_t *query);
+    void
+    notmuch_query_set_omit_excluded (notmuch_query_t *query,
+                                     notmuch_exclude_t omit_excluded);
+    void
+    notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
+    notmuch_sort_t
+    notmuch_query_get_sort (const notmuch_query_t *query);
+    notmuch_status_t
+    notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag);
+    notmuch_status_t
+    notmuch_query_search_threads (notmuch_query_t *query,
+                                  notmuch_threads_t **out);
+    notmuch_status_t
+    notmuch_query_search_messages (notmuch_query_t *query,
+                                   notmuch_messages_t **out);
+    notmuch_status_t
+    notmuch_query_count_messages (notmuch_query_t *query, unsigned int *count);
+    notmuch_status_t
+    notmuch_query_count_threads (notmuch_query_t *query, unsigned *count);
+    void
+    notmuch_query_destroy (notmuch_query_t *query);
+
+    notmuch_bool_t
+    notmuch_threads_valid (notmuch_threads_t *threads);
+    notmuch_thread_t *
+    notmuch_threads_get (notmuch_threads_t *threads);
+    void
+    notmuch_threads_move_to_next (notmuch_threads_t *threads);
+    void
+    notmuch_threads_destroy (notmuch_threads_t *threads);
+
+    const char *
+    notmuch_thread_get_thread_id (notmuch_thread_t *thread);
+    notmuch_messages_t *
+    notmuch_message_get_replies (notmuch_message_t *message);
+    int
+    notmuch_thread_get_total_messages (notmuch_thread_t *thread);
+    notmuch_messages_t *
+    notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
+    notmuch_messages_t *
+    notmuch_thread_get_messages (notmuch_thread_t *thread);
+    int
+    notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
+    const char *
+    notmuch_thread_get_authors (notmuch_thread_t *thread);
+    const char *
+    notmuch_thread_get_subject (notmuch_thread_t *thread);
+    time_t
+    notmuch_thread_get_oldest_date (notmuch_thread_t *thread);
+    time_t
+    notmuch_thread_get_newest_date (notmuch_thread_t *thread);
+    notmuch_tags_t *
+    notmuch_thread_get_tags (notmuch_thread_t *thread);
+    void
+    notmuch_thread_destroy (notmuch_thread_t *thread);
+
+    notmuch_bool_t
+    notmuch_messages_valid (notmuch_messages_t *messages);
+    notmuch_message_t *
+    notmuch_messages_get (notmuch_messages_t *messages);
+    void
+    notmuch_messages_move_to_next (notmuch_messages_t *messages);
+    void
+    notmuch_messages_destroy (notmuch_messages_t *messages);
+    notmuch_tags_t *
+    notmuch_messages_collect_tags (notmuch_messages_t *messages);
+
+    const char *
+    notmuch_message_get_message_id (notmuch_message_t *message);
+    const char *
+    notmuch_message_get_thread_id (notmuch_message_t *message);
+    const char *
+    notmuch_message_get_filename (notmuch_message_t *message);
+    notmuch_filenames_t *
+    notmuch_message_get_filenames (notmuch_message_t *message);
+    notmuch_bool_t
+    notmuch_message_get_flag (notmuch_message_t *message,
+                              notmuch_message_flag_t flag);
+    void
+    notmuch_message_set_flag (notmuch_message_t *message,
+                              notmuch_message_flag_t flag,
+                              notmuch_bool_t value);
+    time_t
+    notmuch_message_get_date  (notmuch_message_t *message);
+    const char *
+    notmuch_message_get_header (notmuch_message_t *message,
+                                const char *header);
+    notmuch_tags_t *
+    notmuch_message_get_tags (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_add_tag (notmuch_message_t *message, const char *tag);
+    notmuch_status_t
+    notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);
+    notmuch_status_t
+    notmuch_message_remove_all_tags (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_maildir_flags_to_tags (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_tags_to_maildir_flags (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_freeze (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_thaw (notmuch_message_t *message);
+    notmuch_status_t
+    notmuch_message_get_property (notmuch_message_t *message,
+                                  const char *key, const char **value);
+    notmuch_status_t
+    notmuch_message_add_property (notmuch_message_t *message,
+                                  const char *key, const char *value);
+    notmuch_status_t
+    notmuch_message_remove_property (notmuch_message_t *message,
+                                     const char *key, const char *value);
+    notmuch_status_t
+    notmuch_message_remove_all_properties (notmuch_message_t *message,
+                                           const char *key);
+    notmuch_message_properties_t *
+    notmuch_message_get_properties (notmuch_message_t *message,
+                                    const char *key, notmuch_bool_t exact);
+    notmuch_bool_t
+    notmuch_message_properties_valid (notmuch_message_properties_t
+                                          *properties);
+    void
+    notmuch_message_properties_move_to_next (notmuch_message_properties_t
+                                                 *properties);
+    const char *
+    notmuch_message_properties_key (notmuch_message_properties_t *properties);
+    const char *
+    notmuch_message_properties_value (notmuch_message_properties_t
+                                          *properties);
+    void
+    notmuch_message_properties_destroy (notmuch_message_properties_t
+                                            *properties);
+    void
+    notmuch_message_destroy (notmuch_message_t *message);
+
+    notmuch_bool_t
+    notmuch_tags_valid (notmuch_tags_t *tags);
+    const char *
+    notmuch_tags_get (notmuch_tags_t *tags);
+    void
+    notmuch_tags_move_to_next (notmuch_tags_t *tags);
+    void
+    notmuch_tags_destroy (notmuch_tags_t *tags);
+
+    notmuch_bool_t
+    notmuch_filenames_valid (notmuch_filenames_t *filenames);
+    const char *
+    notmuch_filenames_get (notmuch_filenames_t *filenames);
+    void
+    notmuch_filenames_move_to_next (notmuch_filenames_t *filenames);
+    void
+    notmuch_filenames_destroy (notmuch_filenames_t *filenames);
+    """
+)
+
+
+if __name__ == '__main__':
+    ffibuilder.compile(verbose=True)
diff --git a/bindings/python-cffi/notmuch2/_database.py b/bindings/python-cffi/notmuch2/_database.py
new file mode 100644 (file)
index 0000000..a15c4d0
--- /dev/null
@@ -0,0 +1,705 @@
+import collections
+import configparser
+import enum
+import functools
+import os
+import pathlib
+import weakref
+
+import notmuch2._base as base
+import notmuch2._capi as capi
+import notmuch2._errors as errors
+import notmuch2._message as message
+import notmuch2._query as querymod
+import notmuch2._tags as tags
+
+
+__all__ = ['Database', 'AtomicContext', 'DbRevision']
+
+
+def _config_pathname():
+    """Return the path of the configuration file.
+
+    :rtype: pathlib.Path
+    """
+    cfgfname = os.getenv('NOTMUCH_CONFIG', '~/.notmuch-config')
+    return pathlib.Path(os.path.expanduser(cfgfname))
+
+
+class Mode(enum.Enum):
+    READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY
+    READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE
+
+
+class QuerySortOrder(enum.Enum):
+    OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST
+    NEWEST_FIRST = capi.lib.NOTMUCH_SORT_NEWEST_FIRST
+    MESSAGE_ID = capi.lib.NOTMUCH_SORT_MESSAGE_ID
+    UNSORTED = capi.lib.NOTMUCH_SORT_UNSORTED
+
+
+class QueryExclude(enum.Enum):
+    TRUE = capi.lib.NOTMUCH_EXCLUDE_TRUE
+    FLAG = capi.lib.NOTMUCH_EXCLUDE_FLAG
+    FALSE = capi.lib.NOTMUCH_EXCLUDE_FALSE
+    ALL = capi.lib.NOTMUCH_EXCLUDE_ALL
+
+
+class Database(base.NotmuchObject):
+    """Toplevel access to notmuch.
+
+    A :class:`Database` can be opened read-only or read-write.
+    Modifications are not atomic by default, use :meth:`begin_atomic`
+    for atomic updates.  If the underlying database has been modified
+    outside of this class a :exc:`XapianError` will be raised and the
+    instance must be closed and a new one created.
+
+    You can use an instance of this class as a context-manager.
+
+    :cvar MODE: The mode a database can be opened with, an enumeration
+       of ``READ_ONLY`` and ``READ_WRITE``
+    :cvar SORT: The sort order for search results, ``OLDEST_FIRST``,
+       ``NEWEST_FIRST``, ``MESSAGE_ID`` or ``UNSORTED``.
+    :cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``,
+       ``FLAG``, ``FALSE`` or ``ALL``.  See the query documentation
+       for details.
+    :cvar AddedMessage: A namedtuple ``(msg, dup)`` used by
+       :meth:`add` as return value.
+    :cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items.
+       This is used to implement the ``ro`` and ``rw`` string
+       variants.
+
+    :ivar closed: Boolean indicating if the database is closed or
+       still open.
+
+    :param path: The directory of where the database is stored.  If
+       ``None`` the location will be read from the user's
+       configuration file, respecting the ``NOTMUCH_CONFIG``
+       environment variable if set.
+    :type path: str, bytes, os.PathLike or pathlib.Path
+    :param mode: The mode to open the database in.  One of
+       :attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`.  For
+       convenience you can also use the strings ``ro`` for
+       :attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`.
+    :type mode: :attr:`MODE` or str.
+
+    :raises KeyError: if an unknown mode string is used.
+    :raises OSError: or subclasses if the configuration file can not
+       be opened.
+    :raises configparser.Error: or subclasses if the configuration
+       file can not be parsed.
+    :raises NotmuchError: or subclasses for other failures.
+    """
+
+    MODE = Mode
+    SORT = QuerySortOrder
+    EXCLUDE = QueryExclude
+    AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup'])
+    _db_p = base.MemoryPointer()
+    STR_MODE_MAP = {
+        'ro': MODE.READ_ONLY,
+        'rw': MODE.READ_WRITE,
+    }
+
+    def __init__(self, path=None, mode=MODE.READ_ONLY):
+        if isinstance(mode, str):
+            mode = self.STR_MODE_MAP[mode]
+        self.mode = mode
+        if path is None:
+            path = self.default_path()
+        if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
+            path = bytes(path)
+        db_pp = capi.ffi.new('notmuch_database_t **')
+        cmsg = capi.ffi.new('char**')
+        ret = capi.lib.notmuch_database_open_verbose(os.fsencode(path),
+                                                     mode.value, db_pp, cmsg)
+        if cmsg[0]:
+            msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
+            capi.lib.free(cmsg[0])
+        else:
+            msg = None
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret, msg)
+        self._db_p = db_pp[0]
+        self.closed = False
+
+    @classmethod
+    def create(cls, path=None):
+        """Create and open database in READ_WRITE mode.
+
+        This is creates a new notmuch database and returns an opened
+        instance in :attr:`MODE.READ_WRITE` mode.
+
+        :param path: The directory of where the database is stored.  If
+           ``None`` the location will be read from the user's
+           configuration file, respecting the ``NOTMUCH_CONFIG``
+           environment variable if set.
+        :type path: str, bytes or os.PathLike
+
+        :raises OSError: or subclasses if the configuration file can not
+           be opened.
+        :raises configparser.Error: or subclasses if the configuration
+           file can not be parsed.
+        :raises NotmuchError: if the config file does not have the
+           database.path setting.
+        :raises FileError: if the database already exists.
+
+        :returns: The newly created instance.
+        """
+        if path is None:
+            path = cls.default_path()
+        if not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
+            path = bytes(path)
+        db_pp = capi.ffi.new('notmuch_database_t **')
+        cmsg = capi.ffi.new('char**')
+        ret = capi.lib.notmuch_database_create_verbose(os.fsencode(path),
+                                                       db_pp, cmsg)
+        if cmsg[0]:
+            msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
+            capi.lib.free(cmsg[0])
+        else:
+            msg = None
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret, msg)
+
+        # Now close the db and let __init__ open it.  Inefficient but
+        # creating is not a hot loop while this allows us to have a
+        # clean API.
+        ret = capi.lib.notmuch_database_destroy(db_pp[0])
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        return cls(path, cls.MODE.READ_WRITE)
+
+    @staticmethod
+    def default_path(cfg_path=None):
+        """Return the path of the user's default database.
+
+        This reads the user's configuration file and returns the
+        default path of the database.
+
+        :param cfg_path: The pathname of the notmuch configuration file.
+           If not specified tries to use the pathname provided in the
+           :env:`NOTMUCH_CONFIG` environment variable and falls back
+           to :file:`~/.notmuch-config.
+        :type cfg_path: str, bytes, os.PathLike or pathlib.Path.
+
+        :returns: The path of the database, which does not necessarily
+           exists.
+        :rtype: pathlib.Path
+        :raises OSError: or subclasses if the configuration file can not
+           be opened.
+        :raises configparser.Error: or subclasses if the configuration
+           file can not be parsed.
+        :raises NotmuchError if the config file does not have the
+           database.path setting.
+        """
+        if not cfg_path:
+            cfg_path = _config_pathname()
+        if not hasattr(os, 'PathLike') and isinstance(cfg_path, pathlib.Path):
+            cfg_path = bytes(cfg_path)
+        parser = configparser.ConfigParser()
+        with open(cfg_path) as fp:
+            parser.read_file(fp)
+        try:
+            return pathlib.Path(parser.get('database', 'path'))
+        except configparser.Error:
+            raise errors.NotmuchError(
+                'No database.path setting in {}'.format(cfg_path))
+
+    def __del__(self):
+        self._destroy()
+
+    @property
+    def alive(self):
+        try:
+            self._db_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def _destroy(self):
+        try:
+            ret = capi.lib.notmuch_database_destroy(self._db_p)
+        except errors.ObjectDestroyedError:
+            ret = capi.lib.NOTMUCH_STATUS_SUCCESS
+        else:
+            self._db_p = None
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def close(self):
+        """Close the notmuch database.
+
+        Once closed most operations will fail.  This can still be
+        useful however to explicitly close a database which is opened
+        read-write as this would otherwise stop other processes from
+        reading the database while it is open.
+
+        :raises ObjectDestroyedError: if used after destroyed.
+        """
+        ret = capi.lib.notmuch_database_close(self._db_p)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        self.closed = True
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.close()
+
+    @property
+    def path(self):
+        """The pathname of the notmuch database.
+
+        This is returned as a :class:`pathlib.Path` instance.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        try:
+            return self._cache_path
+        except AttributeError:
+            ret = capi.lib.notmuch_database_get_path(self._db_p)
+            self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
+            return self._cache_path
+
+    @property
+    def version(self):
+        """The database format version.
+
+        This is a positive integer.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        try:
+            return self._cache_version
+        except AttributeError:
+            ret = capi.lib.notmuch_database_get_version(self._db_p)
+            self._cache_version = ret
+            return ret
+
+    @property
+    def needs_upgrade(self):
+        """Whether the database should be upgraded.
+
+        If *True* the database can be upgraded using :meth:`upgrade`.
+        Not doing so may result in some operations raising
+        :exc:`UpgradeRequiredError`.
+
+        A read-only database will never be upgradable.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_database_needs_upgrade(self._db_p)
+        return bool(ret)
+
+    def upgrade(self, progress_cb=None):
+        """Upgrade the database to the latest version.
+
+        Upgrade the database, optionally with a progress callback
+        which should be a callable which will be called with a
+        floating point number in the range of [0.0 .. 1.0].
+        """
+        raise NotImplementedError
+
+    def atomic(self):
+        """Return a context manager to perform atomic operations.
+
+        The returned context manager can be used to perform atomic
+        operations on the database.
+
+        .. note:: Unlinke a traditional RDBMS transaction this does
+           not imply durability, it only ensures the changes are
+           performed atomically.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ctx = AtomicContext(self, '_db_p')
+        return ctx
+
+    def revision(self):
+        """The currently committed revision in the database.
+
+        Returned as a ``(revision, uuid)`` namedtuple.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        raw_uuid = capi.ffi.new('char**')
+        rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
+        return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
+
+    def get_directory(self, path):
+        raise NotImplementedError
+
+    def add(self, filename, *, sync_flags=False):
+        """Add a message to the database.
+
+        Add a new message to the notmuch database.  The message is
+        referred to by the pathname of the maildir file.  If the
+        message ID of the new message already exists in the database,
+        this adds ``pathname`` to the list of list of files for the
+        existing message.
+
+        :param filename: The path of the file containing the message.
+        :type filename: str, bytes, os.PathLike or pathlib.Path.
+        :param sync_flags: Whether to sync the known maildir flags to
+           notmuch tags.  See :meth:`Message.flags_to_tags` for
+           details.
+
+        :returns: A tuple where the first item is the newly inserted
+           messages as a :class:`Message` instance, and the second
+           item is a boolean indicating if the message inserted was a
+           duplicate.  This is the namedtuple ``AddedMessage(msg,
+           dup)``.
+        :rtype: Database.AddedMessage
+
+        If an exception is raised, no message was added.
+
+        :raises XapianError: A Xapian exception occurred.
+        :raises FileError: The file referred to by ``pathname`` could
+           not be opened.
+        :raises FileNotEmailError: The file referreed to by
+           ``pathname`` is not recognised as an email message.
+        :raises ReadOnlyDatabaseError: The database is opened in
+           READ_ONLY mode.
+        :raises UpgradeRequiredError: The database must be upgraded
+           first.
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
+            filename = bytes(filename)
+        msg_pp = capi.ffi.new('notmuch_message_t **')
+        ret = capi.lib.notmuch_database_add_message(self._db_p,
+                                                    os.fsencode(filename),
+                                                    msg_pp)
+        ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
+              capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
+        if ret not in ok:
+            raise errors.NotmuchError(ret)
+        msg = message.Message(self, msg_pp[0], db=self)
+        if sync_flags:
+            msg.tags.from_maildir_flags()
+        return self.AddedMessage(
+            msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
+
+    def remove(self, filename):
+        """Remove a message from the notmuch database.
+
+        Removing a message which is not in the database is just a
+        silent nop-operation.
+
+        :param filename: The pathname of the file containing the
+           message to be removed.
+        :type filename: str, bytes, os.PathLike or pathlib.Path.
+
+        :returns: True if the message is still in the database.  This
+           can happen when multiple files contain the same message ID.
+           The true/false distinction is fairly arbitrary, but think
+           of it as ``dup = db.remove_message(name); if dup: ...``.
+        :rtype: bool
+
+        :raises XapianError: A Xapian exception ocurred.
+        :raises ReadOnlyDatabaseError: The database is opened in
+           READ_ONLY mode.
+        :raises UpgradeRequiredError: The database must be upgraded
+           first.
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
+            filename = bytes(filename)
+        ret = capi.lib.notmuch_database_remove_message(self._db_p,
+                                                       os.fsencode(filename))
+        ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
+              capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
+        if ret not in ok:
+            raise errors.NotmuchError(ret)
+        if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+            return True
+        else:
+            return False
+
+    def find(self, msgid):
+        """Return the message matching the given message ID.
+
+        If a message with the given message ID is found a
+        :class:`Message` instance is returned.  Otherwise a
+        :exc:`LookupError` is raised.
+
+        :param msgid: The message ID to look for.
+        :type msgid: str
+
+        :returns: The message instance.
+        :rtype: Message
+
+        :raises LookupError: If no message was found.
+        :raises OutOfMemoryError: When there is no memory to allocate
+           the message instance.
+        :raises XapianError: A Xapian exception ocurred.
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        msg_pp = capi.ffi.new('notmuch_message_t **')
+        ret = capi.lib.notmuch_database_find_message(self._db_p,
+                                                     msgid.encode(), msg_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        msg_p = msg_pp[0]
+        if msg_p == capi.ffi.NULL:
+            raise LookupError
+        msg = message.Message(self, msg_p, db=self)
+        return msg
+
+    def get(self, filename):
+        """Return the :class:`Message` given a pathname.
+
+        If a message with the given pathname exists in the database
+        return the :class:`Message` instance for the message.
+        Otherwise raise a :exc:`LookupError` exception.
+
+        :param filename: The pathname of the message.
+        :type filename: str, bytes, os.PathLike or pathlib.Path
+
+        :returns: The message instance.
+        :rtype: Message
+
+        :raises LookupError: If no message was found.  This is also
+           a subclass of :exc:`KeyError`.
+        :raises OutOfMemoryError: When there is no memory to allocate
+           the message instance.
+        :raises XapianError: A Xapian exception ocurred.
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
+            filename = bytes(filename)
+        msg_pp = capi.ffi.new('notmuch_message_t **')
+        ret = capi.lib.notmuch_database_find_message_by_filename(
+            self._db_p, os.fsencode(filename), msg_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        msg_p = msg_pp[0]
+        if msg_p == capi.ffi.NULL:
+            raise LookupError
+        msg = message.Message(self, msg_p, db=self)
+        return msg
+
+    @property
+    def tags(self):
+        """Return an immutable set with all tags used in this database.
+
+        This returns an immutable set-like object implementing the
+        collections.abc.Set Abstract Base Class.  Due to the
+        underlying libnotmuch implementation some operations have
+        different performance characteristics then plain set objects.
+        Mainly any lookup operation is O(n) rather then O(1).
+
+        Normal usage treats tags as UTF-8 encoded unicode strings so
+        they are exposed to Python as normal unicode string objects.
+        If you need to handle tags stored in libnotmuch which are not
+        valid unicode do check the :class:`ImmutableTagSet` docs for
+        how to handle this.
+
+        :rtype: ImmutableTagSet
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        try:
+            ref = self._cached_tagset
+        except AttributeError:
+            tagset = None
+        else:
+            tagset = ref()
+        if tagset is None:
+            tagset = tags.ImmutableTagSet(
+                self, '_db_p', capi.lib.notmuch_database_get_all_tags)
+            self._cached_tagset = weakref.ref(tagset)
+        return tagset
+
+    def _create_query(self, query, *,
+                      omit_excluded=EXCLUDE.TRUE,
+                      sort=SORT.UNSORTED,  # Check this default
+                      exclude_tags=None):
+        """Create an internal query object.
+
+        :raises OutOfMemoryError: if no memory is available to
+           allocate the query.
+        """
+        if isinstance(query, str):
+            query = query.encode('utf-8')
+        query_p = capi.lib.notmuch_query_create(self._db_p, query)
+        if query_p == capi.ffi.NULL:
+            raise errors.OutOfMemoryError()
+        capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value)
+        capi.lib.notmuch_query_set_sort(query_p, sort.value)
+        if exclude_tags is not None:
+            for tag in exclude_tags:
+                if isinstance(tag, str):
+                    tag = str.encode('utf-8')
+                capi.lib.notmuch_query_add_tag_exclude(query_p, tag)
+        return querymod.Query(self, query_p)
+
+    def messages(self, query, *,
+                 omit_excluded=EXCLUDE.TRUE,
+                 sort=SORT.UNSORTED,  # Check this default
+                 exclude_tags=None):
+        """Search the database for messages.
+
+        :returns: An iterator over the messages found.
+        :rtype: MessageIter
+
+        :raises OutOfMemoryError: if no memory is available to
+           allocate the query.
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        query = self._create_query(query,
+                                   omit_excluded=omit_excluded,
+                                   sort=sort,
+                                   exclude_tags=exclude_tags)
+        return query.messages()
+
+    def count_messages(self, query, *,
+                       omit_excluded=EXCLUDE.TRUE,
+                       sort=SORT.UNSORTED,  # Check this default
+                       exclude_tags=None):
+        """Search the database for messages.
+
+        :returns: An iterator over the messages found.
+        :rtype: MessageIter
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        query = self._create_query(query,
+                                   omit_excluded=omit_excluded,
+                                   sort=sort,
+                                   exclude_tags=exclude_tags)
+        return query.count_messages()
+
+    def threads(self,  query, *,
+                omit_excluded=EXCLUDE.TRUE,
+                sort=SORT.UNSORTED,  # Check this default
+                exclude_tags=None):
+        query = self._create_query(query,
+                                   omit_excluded=omit_excluded,
+                                   sort=sort,
+                                   exclude_tags=exclude_tags)
+        return query.threads()
+
+    def count_threads(self, query, *,
+                      omit_excluded=EXCLUDE.TRUE,
+                      sort=SORT.UNSORTED,  # Check this default
+                      exclude_tags=None):
+        query = self._create_query(query,
+                                   omit_excluded=omit_excluded,
+                                   sort=sort,
+                                   exclude_tags=exclude_tags)
+        return query.count_threads()
+
+    def status_string(self):
+        raise NotImplementedError
+
+    def __repr__(self):
+        return 'Database(path={self.path}, mode={self.mode})'.format(self=self)
+
+
+class AtomicContext:
+    """Context manager for atomic support.
+
+    This supports the notmuch_database_begin_atomic and
+    notmuch_database_end_atomic API calls.  The object can not be
+    directly instantiated by the user, only via ``Database.atomic``.
+    It does keep a reference to the :class:`Database` instance to keep
+    the C memory alive.
+
+    :raises XapianError: When this is raised at enter time the atomic
+       section is not active.  When it is raised at exit time the
+       atomic section is still active and you may need to try using
+       :meth:`force_end`.
+    :raises ObjectDestroyedError: if used after destoryed.
+    """
+
+    def __init__(self, db, ptr_name):
+        self._db = db
+        self._ptr = lambda: getattr(db, ptr_name)
+
+    def __del__(self):
+        self._destroy()
+
+    @property
+    def alive(self):
+        return self.parent.alive
+
+    def _destroy(self):
+        pass
+
+    def __enter__(self):
+        ret = capi.lib.notmuch_database_begin_atomic(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        ret = capi.lib.notmuch_database_end_atomic(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def force_end(self):
+        """Force ending the atomic section.
+
+        This can only be called once __exit__ has been called.  It
+        will attept to close the atomic section (again).  This is
+        useful if the original exit raised an exception and the atomic
+        section is still open.  But things are pretty ugly by now.
+
+        :raises XapianError: If exiting fails, the atomic section is
+           not ended.
+        :raises UnbalancedAtomicError: If the database was currently
+           not in an atomic section.
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_database_end_atomic(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+
+@functools.total_ordering
+class DbRevision:
+    """A database revision.
+
+    The database revision number increases monotonically with each
+    commit to the database.  Which means user-visible changes can be
+    ordered.  This object is sortable with other revisions.  It
+    carries the UUID of the database to ensure it is only ever
+    compared with revisions from the same database.
+    """
+
+    def __init__(self, rev, uuid):
+        self._rev = rev
+        self._uuid = uuid
+
+    @property
+    def rev(self):
+        """The revision number, a positive integer."""
+        return self._rev
+
+    @property
+    def uuid(self):
+        """The UUID of the database, consider this opaque."""
+        return self._uuid
+
+    def __eq__(self, other):
+        if isinstance(other, self.__class__):
+            if self.uuid != other.uuid:
+                return False
+            return self.rev == other.rev
+        else:
+            return NotImplemented
+
+    def __lt__(self, other):
+        if self.__class__ is other.__class__:
+            if self.uuid != other.uuid:
+                return False
+            return self.rev < other.rev
+        else:
+            return NotImplemented
+
+    def __repr__(self):
+        return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self)
diff --git a/bindings/python-cffi/notmuch2/_errors.py b/bindings/python-cffi/notmuch2/_errors.py
new file mode 100644 (file)
index 0000000..1c88763
--- /dev/null
@@ -0,0 +1,112 @@
+from notmuch2 import _capi as capi
+
+
+class NotmuchError(Exception):
+    """Base exception for errors originating from the notmuch library.
+
+    Usually this will have two attributes:
+
+    :status: This is a numeric status code corresponding to the error
+       code in the notmuch library.  This is normally fairly
+       meaningless, it can also often be ``None``.  This exists mostly
+       to easily create new errors from notmuch status codes and
+       should not normally be used by users.
+
+    :message: A user-facing message for the error.  This can
+       occasionally also be ``None``.  Usually you'll want to call
+       ``str()`` on the error object instead to get a sensible
+       message.
+    """
+
+    @classmethod
+    def exc_type(cls, status):
+        """Return correct exception type for notmuch status."""
+        types = {
+            capi.lib.NOTMUCH_STATUS_OUT_OF_MEMORY:
+                OutOfMemoryError,
+            capi.lib.NOTMUCH_STATUS_READ_ONLY_DATABASE:
+                ReadOnlyDatabaseError,
+            capi.lib.NOTMUCH_STATUS_XAPIAN_EXCEPTION:
+                XapianError,
+            capi.lib.NOTMUCH_STATUS_FILE_ERROR:
+                FileError,
+            capi.lib.NOTMUCH_STATUS_FILE_NOT_EMAIL:
+                FileNotEmailError,
+            capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+                DuplicateMessageIdError,
+            capi.lib.NOTMUCH_STATUS_NULL_POINTER:
+                NullPointerError,
+            capi.lib.NOTMUCH_STATUS_TAG_TOO_LONG:
+                TagTooLongError,
+            capi.lib.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
+                UnbalancedFreezeThawError,
+            capi.lib.NOTMUCH_STATUS_UNBALANCED_ATOMIC:
+                UnbalancedAtomicError,
+            capi.lib.NOTMUCH_STATUS_UNSUPPORTED_OPERATION:
+                UnsupportedOperationError,
+            capi.lib.NOTMUCH_STATUS_UPGRADE_REQUIRED:
+                UpgradeRequiredError,
+            capi.lib.NOTMUCH_STATUS_PATH_ERROR:
+                PathError,
+            capi.lib.NOTMUCH_STATUS_ILLEGAL_ARGUMENT:
+                IllegalArgumentError,
+        }
+        return types[status]
+
+    def __new__(cls, *args, **kwargs):
+        """Return the correct subclass based on status."""
+        # This is simplistic, but the actual __init__ will fail if the
+        # signature is wrong anyway.
+        if args:
+            status = args[0]
+        else:
+            status = kwargs.get('status', None)
+        if status and cls == NotmuchError:
+            exc = cls.exc_type(status)
+            return exc.__new__(exc, *args, **kwargs)
+        else:
+            return super().__new__(cls)
+
+    def __init__(self, status=None, message=None):
+        self.status = status
+        self.message = message
+
+    def __str__(self):
+        if self.message:
+            return self.message
+        elif self.status:
+            return capi.lib.notmuch_status_to_string(self.status)
+        else:
+            return 'Unknown error'
+
+
+class OutOfMemoryError(NotmuchError): pass
+class ReadOnlyDatabaseError(NotmuchError): pass
+class XapianError(NotmuchError): pass
+class FileError(NotmuchError): pass
+class FileNotEmailError(NotmuchError): pass
+class DuplicateMessageIdError(NotmuchError): pass
+class NullPointerError(NotmuchError): pass
+class TagTooLongError(NotmuchError): pass
+class UnbalancedFreezeThawError(NotmuchError): pass
+class UnbalancedAtomicError(NotmuchError): pass
+class UnsupportedOperationError(NotmuchError): pass
+class UpgradeRequiredError(NotmuchError): pass
+class PathError(NotmuchError): pass
+class IllegalArgumentError(NotmuchError): pass
+
+
+class ObjectDestroyedError(NotmuchError):
+    """The object has already been destoryed and it's memory freed.
+
+    This occurs when :meth:`destroy` has been called on the object but
+    you still happen to have access to the object.  This should not
+    normally occur since you should never call :meth:`destroy` by
+    hand.
+    """
+
+    def __str__(self):
+        if self.message:
+            return self.message
+        else:
+            return 'Memory already freed'
diff --git a/bindings/python-cffi/notmuch2/_message.py b/bindings/python-cffi/notmuch2/_message.py
new file mode 100644 (file)
index 0000000..bb56142
--- /dev/null
@@ -0,0 +1,691 @@
+import collections
+import contextlib
+import os
+import pathlib
+import weakref
+
+import notmuch2._base as base
+import notmuch2._capi as capi
+import notmuch2._errors as errors
+import notmuch2._tags as tags
+
+
+__all__ = ['Message']
+
+
+class Message(base.NotmuchObject):
+    """An email message stored in the notmuch database.
+
+    This should not be directly created, instead it will be returned
+    by calling methods on :class:`Database`.  A message keeps a
+    reference to the database object since the database object can not
+    be released while the message is in use.
+
+    Note that this represents a message in the notmuch database.  For
+    full email functionality you may want to use the :mod:`email`
+    package from Python's standard library.  You could e.g. create
+    this as such::
+
+       notmuch_msg = db.get_message(msgid)  # or from a query
+       parser = email.parser.BytesParser(policy=email.policy.default)
+       with notmuch_msg.path.open('rb) as fp:
+           email_msg = parser.parse(fp)
+
+    Most commonly the functionality provided by notmuch is sufficient
+    to read email however.
+
+    Messages are considered equal when they have the same message ID.
+    This is how libnotmuch treats messages as well, the
+    :meth:`pathnames` function returns multiple results for
+    duplicates.
+
+    :param parent: The parent object.  This is probably one off a
+       :class:`Database`, :class:`Thread` or :class:`Query`.
+    :type parent: NotmuchObject
+    :param db: The database instance this message is associated with.
+       This could be the same as the parent.
+    :type db: Database
+    :param msg_p: The C pointer to the ``notmuch_message_t``.
+    :type msg_p: <cdata>
+
+    :param dup: Whether the message was a duplicate on insertion.
+
+    :type dup: None or bool
+    """
+    _msg_p = base.MemoryPointer()
+
+    def __init__(self, parent, msg_p, *, db):
+        self._parent = parent
+        self._msg_p = msg_p
+        self._db = db
+
+    @property
+    def alive(self):
+        if not self._parent.alive:
+            return False
+        try:
+            self._msg_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def __del__(self):
+        self._destroy()
+
+    def _destroy(self):
+        if self.alive:
+            capi.lib.notmuch_message_destroy(self._msg_p)
+        self._msg_p = None
+
+    @property
+    def messageid(self):
+        """The message ID as a string.
+
+        The message ID is decoded with the ignore error handler.  This
+        is fine as long as the message ID is well formed.  If it is
+        not valid ASCII then this will be lossy.  So if you need to be
+        able to write the exact same message ID back you should use
+        :attr:`messageidb`.
+
+        Note that notmuch will decode the message ID value and thus
+        strip off the surrounding ``<`` and ``>`` characters.  This is
+        different from Python's :mod:`email` package behaviour which
+        leaves these characters in place.
+
+        :returns: The message ID.
+        :rtype: :class:`BinString`, this is a normal str but calling
+           bytes() on it will return the original bytes used to create
+           it.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
+        return base.BinString(capi.ffi.string(ret))
+
+    @property
+    def threadid(self):
+        """The thread ID.
+
+        The thread ID is decoded with the surrogateescape error
+        handler so that it is possible to reconstruct the original
+        thread ID if it is not valid UTF-8.
+
+        :returns: The thread ID.
+        :rtype: :class:`BinString`, this is a normal str but calling
+           bytes() on it will return the original bytes used to create
+           it.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
+        return base.BinString(capi.ffi.string(ret))
+
+    @property
+    def path(self):
+        """A pathname of the message as a pathlib.Path instance.
+
+        If multiple files in the database contain the same message ID
+        this will be just one of the files, chosen at random.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_message_get_filename(self._msg_p)
+        return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
+
+    @property
+    def pathb(self):
+        """A pathname of the message as a bytes object.
+
+        See :attr:`path` for details, this is the same but does return
+        the path as a bytes object which is faster but less convenient.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_message_get_filename(self._msg_p)
+        return capi.ffi.string(ret)
+
+    def filenames(self):
+        """Return an iterator of all files for this message.
+
+        If multiple files contained the same message ID they will all
+        be returned here.  The files are returned as intances of
+        :class:`pathlib.Path`.
+
+        :returns: Iterator yielding :class:`pathlib.Path` instances.
+        :rtype: iter
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
+        return PathIter(self, fnames_p)
+
+    def filenamesb(self):
+        """Return an iterator of all files for this message.
+
+        This is like :meth:`pathnames` but the files are returned as
+        byte objects instead.
+
+        :returns: Iterator yielding :class:`bytes` instances.
+        :rtype: iter
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
+        return FilenamesIter(self, fnames_p)
+
+    @property
+    def ghost(self):
+        """Indicates whether this message is a ghost message.
+
+        A ghost message if a message which we know exists, but it has
+        no files or content associated with it.  This can happen if
+        it was referenced by some other message.  Only the
+        :attr:`messageid` and :attr:`threadid` attributes are valid
+        for it.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_message_get_flag(
+            self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
+        return bool(ret)
+
+    @property
+    def excluded(self):
+        """Indicates whether this message was excluded from the query.
+
+        When a message is created from a search, sometimes messages
+        that where excluded by the search query could still be
+        returned by it, e.g. because they are part of a thread
+        matching the query.  the :meth:`Database.query` method allows
+        these messages to be flagged, which results in this property
+        being set to *True*.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_message_get_flag(
+            self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
+        return bool(ret)
+
+    @property
+    def date(self):
+        """The message date as an integer.
+
+        The time the message was sent as an integer number of seconds
+        since the *epoch*, 1 Jan 1970.  This is derived from the
+        message's header, you can get the original header value with
+        :meth:`header`.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        return capi.lib.notmuch_message_get_date(self._msg_p)
+
+    def header(self, name):
+        """Return the value of the named header.
+
+        Returns the header from notmuch, some common headers are
+        stored in the database, others are read from the file.
+        Headers are returned with their newlines stripped and
+        collapsed concatenated together if they occur multiple times.
+        You may be better off using the standard library email
+        package's ``email.message_from_file(msg.path.open())`` if that
+        is not sufficient for you.
+
+        :param header: Case-insensitive header name to retrieve.
+        :type header: str or bytes
+
+        :returns: The header value, an empty string if the header is
+           not present.
+        :rtype: str
+
+        :raises LookupError: if the header is not present.
+        :raises NullPointerError: For unexpected notmuch errors.
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        # The returned is supposedly guaranteed to be UTF-8.  Header
+        # names must be ASCII as per RFC x822.
+        if isinstance(name, str):
+            name = name.encode('ascii')
+        ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
+        if ret == capi.ffi.NULL:
+            raise errors.NullPointerError()
+        hdr = capi.ffi.string(ret)
+        if not hdr:
+            raise LookupError
+        return hdr.decode(encoding='utf-8')
+
+    @property
+    def tags(self):
+        """The tags associated with the message.
+
+        This behaves as a set.  But removing and adding items to the
+        set removes and adds them to the message in the database.
+
+        :raises ReadOnlyDatabaseError: When manipulating tags on a
+           database opened in read-only mode.
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        try:
+            ref = self._cached_tagset
+        except AttributeError:
+            tagset = None
+        else:
+            tagset = ref()
+        if tagset is None:
+            tagset = tags.MutableTagSet(
+                self, '_msg_p', capi.lib.notmuch_message_get_tags)
+            self._cached_tagset = weakref.ref(tagset)
+        return tagset
+
+    @contextlib.contextmanager
+    def frozen(self):
+        """Context manager to freeze the message state.
+
+        This allows you to perform atomic tag updates::
+
+           with msg.frozen():
+               msg.tags.clear()
+               msg.tags.add('foo')
+
+        Using This would ensure the message never ends up with no tags
+        applied at all.
+
+        It is safe to nest calls to this context manager.
+
+        :raises ReadOnlyDatabaseError: if the database is opened in
+           read-only mode.
+        :raises UnbalancedFreezeThawError: if you somehow managed to
+           call __exit__ of this context manager more than once.  Why
+           did you do that?
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_message_freeze(self._msg_p)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        self._frozen = True
+        try:
+            yield
+        except Exception:
+            # Only way to "rollback" these changes is to destroy
+            # ourselves and re-create.  Behold.
+            msgid = self.messageid
+            self._destroy()
+            with contextlib.suppress(Exception):
+                new = self._db.find(msgid)
+                self._msg_p = new._msg_p
+                new._msg_p = None
+                del new
+            raise
+        else:
+            ret = capi.lib.notmuch_message_thaw(self._msg_p)
+            if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+                raise errors.NotmuchError(ret)
+            self._frozen = False
+
+    @property
+    def properties(self):
+        """A map of arbitrary key-value pairs associated with the message.
+
+        Be aware that properties may be used by other extensions to
+        store state in.  So delete or modify with care.
+
+        The properties map is somewhat special.  It is essentially a
+        multimap-like structure where each key can have multiple
+        values.  Therefore accessing a single item using
+        :meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
+        will only return you the *first* item if there are multiple
+        and thus are only recommended if you know there to be only one
+        value.
+
+        Instead the map has an additional :meth:`PropertiesMap.all`
+        method which can be used to retrieve all properties of a given
+        key.  This method also allows iterating of a a subset of the
+        keys starting with a given prefix.
+        """
+        try:
+            ref = self._cached_props
+        except AttributeError:
+            props = None
+        else:
+            props = ref()
+        if props is None:
+            props = PropertiesMap(self, '_msg_p')
+            self._cached_props = weakref.ref(props)
+        return props
+
+    def replies(self):
+        """Return an iterator of all replies to this message.
+
+        This method will only work if the message was created from a
+        thread.  Otherwise it will yield no results.
+
+        :returns: An iterator yielding :class:`Message` instances.
+        :rtype: MessageIter
+        """
+        # The notmuch_messages_valid call accepts NULL and this will
+        # become an empty iterator, raising StopIteration immediately.
+        # Hence no return value checking here.
+        msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p)
+        return MessageIter(self, msgs_p, db=self._db)
+
+    def __hash__(self):
+        return hash(self.messageid)
+
+    def __eq__(self, other):
+        if isinstance(other, self.__class__):
+            return self.messageid == other.messageid
+
+
+class FilenamesIter(base.NotmuchIter):
+    """Iterator for binary filenames objects."""
+
+    def __init__(self, parent, iter_p):
+        super().__init__(parent, iter_p,
+                         fn_destroy=capi.lib.notmuch_filenames_destroy,
+                         fn_valid=capi.lib.notmuch_filenames_valid,
+                         fn_get=capi.lib.notmuch_filenames_get,
+                         fn_next=capi.lib.notmuch_filenames_move_to_next)
+
+    def __next__(self):
+        fname = super().__next__()
+        return capi.ffi.string(fname)
+
+
+class PathIter(FilenamesIter):
+    """Iterator for pathlib.Path objects."""
+
+    def __next__(self):
+        fname = super().__next__()
+        return pathlib.Path(os.fsdecode(fname))
+
+
+class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
+    """A mutable mapping to manage properties.
+
+    Both keys and values of properties are supposed to be UTF-8
+    strings in libnotmuch.  However since the uderlying API uses
+    bytestrings you can use either str or bytes to represent keys and
+    all returned keys and values use :class:`BinString`.
+
+    Also be aware that ``iter(this_map)`` will return duplicate keys,
+    while the :class:`collections.abc.KeysView` returned by
+    :meth:`keys` is a :class:`collections.abc.Set` subclass.  This
+    means the former will yield duplicate keys while the latter won't.
+    It also means ``len(list(iter(this_map)))`` could be different
+    than ``len(this_map.keys())``.  ``len(this_map)`` will correspond
+    with the lenght of the default iterator.
+
+    Be aware that libnotmuch exposes all of this as iterators, so
+    quite a few operations have O(n) performance instead of the usual
+    O(1).
+    """
+    Property = collections.namedtuple('Property', ['key', 'value'])
+    _marker = object()
+
+    def __init__(self, msg, ptr_name):
+        self._msg = msg
+        self._ptr = lambda: getattr(msg, ptr_name)
+
+    @property
+    def alive(self):
+        if not self._msg.alive:
+            return False
+        try:
+            self._ptr
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def _destroy(self):
+        pass
+
+    def __iter__(self):
+        """Return an iterator which iterates over the keys.
+
+        Be aware that a single key may have multiple values associated
+        with it, if so it will appear multiple times here.
+        """
+        iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+        return PropertiesKeyIter(self, iter_p)
+
+    def __len__(self):
+        iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+        it = base.NotmuchIter(
+            self, iter_p,
+            fn_destroy=capi.lib.notmuch_message_properties_destroy,
+            fn_valid=capi.lib.notmuch_message_properties_valid,
+            fn_get=capi.lib.notmuch_message_properties_key,
+            fn_next=capi.lib.notmuch_message_properties_move_to_next,
+        )
+        return len(list(it))
+
+    def __getitem__(self, key):
+        """Return **the first** peroperty associated with a key."""
+        if isinstance(key, str):
+            key = key.encode('utf-8')
+        value_pp = capi.ffi.new('char**')
+        ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        if value_pp[0] == capi.ffi.NULL:
+            raise KeyError
+        return base.BinString.from_cffi(value_pp[0])
+
+    def keys(self):
+        """Return a :class:`collections.abc.KeysView` for this map.
+
+        Even when keys occur multiple times this is a subset of set()
+        so will only contain them once.
+        """
+        return collections.abc.KeysView({k: None for k in self})
+
+    def items(self):
+        """Return a :class:`collections.abc.ItemsView` for this map.
+
+        The ItemsView treats a ``(key, value)`` pair as unique, so
+        dupcliate ``(key, value)`` pairs will be merged together.
+        However duplicate keys with different values will be returned.
+        """
+        items = set()
+        props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+        while capi.lib.notmuch_message_properties_valid(props_p):
+            key = capi.lib.notmuch_message_properties_key(props_p)
+            value = capi.lib.notmuch_message_properties_value(props_p)
+            items.add((base.BinString.from_cffi(key),
+                       base.BinString.from_cffi(value)))
+            capi.lib.notmuch_message_properties_move_to_next(props_p)
+        capi.lib.notmuch_message_properties_destroy(props_p)
+        return PropertiesItemsView(items)
+
+    def values(self):
+        """Return a :class:`collecions.abc.ValuesView` for this map.
+
+        All unique property values are included in the view.
+        """
+        values = set()
+        props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
+        while capi.lib.notmuch_message_properties_valid(props_p):
+            value = capi.lib.notmuch_message_properties_value(props_p)
+            values.add(base.BinString.from_cffi(value))
+            capi.lib.notmuch_message_properties_move_to_next(props_p)
+        capi.lib.notmuch_message_properties_destroy(props_p)
+        return PropertiesValuesView(values)
+
+    def __setitem__(self, key, value):
+        """Add a key-value pair to the properties.
+
+        You may prefer to use :meth:`add` for clarity since this
+        method usually implies implicit overwriting of an existing key
+        if it exists, while for properties this is not the case.
+        """
+        self.add(key, value)
+
+    def add(self, key, value):
+        """Add a key-value pair to the properties."""
+        if isinstance(key, str):
+            key = key.encode('utf-8')
+        if isinstance(value, str):
+            value = value.encode('utf-8')
+        ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def __delitem__(self, key):
+        """Remove all properties with this key."""
+        if isinstance(key, str):
+            key = key.encode('utf-8')
+        ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def remove(self, key, value):
+        """Remove a key-value pair from the properties."""
+        if isinstance(key, str):
+            key = key.encode('utf-8')
+        if isinstance(value, str):
+            value = value.encode('utf-8')
+        ret = capi.lib.notmuch_message_remove_property(self._ptr(), key, value)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def pop(self, key, default=_marker):
+        try:
+            value = self[key]
+        except KeyError:
+            if default is self._marker:
+                raise
+            else:
+                return default
+        else:
+            self.remove(key, value)
+            return value
+
+    def popitem(self):
+        try:
+            key = next(iter(self))
+        except StopIteration:
+            raise KeyError
+        value = self.pop(key)
+        return (key, value)
+
+    def clear(self):
+        ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
+                                                             capi.ffi.NULL)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def getall(self, prefix='', *, exact=False):
+        """Return an iterator yielding all properties for a given key prefix.
+
+        The returned iterator yields all peroperties which start with
+        a given key prefix as ``(key, value)`` namedtuples.  If called
+        with ``exact=True`` then only properties which exactly match
+        the prefix are returned, those a key longer than the prefix
+        will not be included.
+
+        :param prefix: The prefix of the key.
+        """
+        if isinstance(prefix, str):
+            prefix = prefix.encode('utf-8')
+        props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
+                                                          prefix, exact)
+        return PropertiesIter(self, props_p)
+
+
+class PropertiesKeyIter(base.NotmuchIter):
+
+    def __init__(self, parent, iter_p):
+        super().__init__(
+            parent,
+            iter_p,
+            fn_destroy=capi.lib.notmuch_message_properties_destroy,
+            fn_valid=capi.lib.notmuch_message_properties_valid,
+            fn_get=capi.lib.notmuch_message_properties_key,
+            fn_next=capi.lib.notmuch_message_properties_move_to_next)
+
+    def __next__(self):
+        item = super().__next__()
+        return base.BinString.from_cffi(item)
+
+
+class PropertiesIter(base.NotmuchIter):
+
+    def __init__(self, parent, iter_p):
+        super().__init__(
+            parent,
+            iter_p,
+            fn_destroy=capi.lib.notmuch_message_properties_destroy,
+            fn_valid=capi.lib.notmuch_message_properties_valid,
+            fn_get=capi.lib.notmuch_message_properties_key,
+            fn_next=capi.lib.notmuch_message_properties_move_to_next,
+        )
+
+    def __next__(self):
+        if not self._fn_valid(self._iter_p):
+            self._destroy()
+            raise StopIteration
+        key = capi.lib.notmuch_message_properties_key(self._iter_p)
+        value = capi.lib.notmuch_message_properties_value(self._iter_p)
+        capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
+        return PropertiesMap.Property(base.BinString.from_cffi(key),
+                                      base.BinString.from_cffi(value))
+
+
+class PropertiesItemsView(collections.abc.Set):
+
+    __slots__ = ('_items',)
+
+    def __init__(self, items):
+        self._items = items
+
+    @classmethod
+    def _from_iterable(self, it):
+        return set(it)
+
+    def __len__(self):
+        return len(self._items)
+
+    def __contains__(self, item):
+        return item in self._items
+
+    def __iter__(self):
+        yield from self._items
+
+
+collections.abc.ItemsView.register(PropertiesItemsView)
+
+
+class PropertiesValuesView(collections.abc.Set):
+
+    __slots__ = ('_values',)
+
+    def __init__(self, values):
+        self._values = values
+
+    def __len__(self):
+        return len(self._values)
+
+    def __contains__(self, value):
+        return value in self._values
+
+    def __iter__(self):
+        yield from self._values
+
+
+collections.abc.ValuesView.register(PropertiesValuesView)
+
+
+class MessageIter(base.NotmuchIter):
+
+    def __init__(self, parent, msgs_p, *, db):
+        self._db = db
+        super().__init__(parent, msgs_p,
+                         fn_destroy=capi.lib.notmuch_messages_destroy,
+                         fn_valid=capi.lib.notmuch_messages_valid,
+                         fn_get=capi.lib.notmuch_messages_get,
+                         fn_next=capi.lib.notmuch_messages_move_to_next)
+
+    def __next__(self):
+        msg_p = super().__next__()
+        return Message(self, msg_p, db=self._db)
diff --git a/bindings/python-cffi/notmuch2/_query.py b/bindings/python-cffi/notmuch2/_query.py
new file mode 100644 (file)
index 0000000..1db6ec9
--- /dev/null
@@ -0,0 +1,83 @@
+from notmuch2 import _base as base
+from notmuch2 import _capi as capi
+from notmuch2 import _errors as errors
+from notmuch2 import _message as message
+from notmuch2 import _thread as thread
+
+
+__all__ = []
+
+
+class Query(base.NotmuchObject):
+    """Private, minimal query object.
+
+    This is not meant for users and is not a full implementation of
+    the query API.  It is only an intermediate used internally to
+    match libnotmuch's memory management.
+    """
+    _query_p = base.MemoryPointer()
+
+    def __init__(self, db, query_p):
+        self._db = db
+        self._query_p = query_p
+
+    @property
+    def alive(self):
+        if not self._db.alive:
+            return False
+        try:
+            self._query_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def __del__(self):
+        self._destroy()
+
+    def _destroy(self):
+        if self.alive:
+            capi.lib.notmuch_query_destroy(self._query_p)
+        self._query_p = None
+
+    @property
+    def query(self):
+        """The query string as seen by libnotmuch."""
+        q = capi.lib.notmuch_query_get_query_string(self._query_p)
+        return base.BinString.from_cffi(q)
+
+    def messages(self):
+        """Return an iterator over all the messages found by the query.
+
+        This executes the query and returns an iterator over the
+        :class:`Message` objects found.
+        """
+        msgs_pp = capi.ffi.new('notmuch_messages_t**')
+        ret = capi.lib.notmuch_query_search_messages(self._query_p, msgs_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        return message.MessageIter(self, msgs_pp[0], db=self._db)
+
+    def count_messages(self):
+        """Return the number of messages matching this query."""
+        count_p = capi.ffi.new('unsigned int *')
+        ret = capi.lib.notmuch_query_count_messages(self._query_p, count_p)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        return count_p[0]
+
+    def threads(self):
+        """Return an iterator over all the threads found by the query."""
+        threads_pp = capi.ffi.new('notmuch_threads_t **')
+        ret = capi.lib.notmuch_query_search_threads(self._query_p, threads_pp)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        return thread.ThreadIter(self, threads_pp[0], db=self._db)
+
+    def count_threads(self):
+        """Return the number of threads matching this query."""
+        count_p = capi.ffi.new('unsigned int *')
+        ret = capi.lib.notmuch_query_count_threads(self._query_p, count_p)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+        return count_p[0]
diff --git a/bindings/python-cffi/notmuch2/_tags.py b/bindings/python-cffi/notmuch2/_tags.py
new file mode 100644 (file)
index 0000000..fe422a7
--- /dev/null
@@ -0,0 +1,338 @@
+import collections.abc
+
+import notmuch2._base as base
+import notmuch2._capi as capi
+import notmuch2._errors as errors
+
+
+__all__ = ['ImmutableTagSet', 'MutableTagSet', 'TagsIter']
+
+
+class ImmutableTagSet(base.NotmuchObject, collections.abc.Set):
+    """The tags associated with a message thread or whole database.
+
+    Both a thread as well as the database expose the union of all tags
+    in messages associated with them.  This exposes these as a
+    :class:`collections.abc.Set` object.
+
+    Note that due to the underlying notmuch API the performance of the
+    implementation is not the same as you would expect from normal
+    sets.  E.g. the :meth:`__contains__` and :meth:`__len__` are O(n)
+    rather then O(1).
+
+    Tags are internally stored as bytestrings but normally exposed as
+    unicode strings using the UTF-8 encoding and the *ignore* decoder
+    error handler.  However the :meth:`iter` method can be used to
+    return tags as bytestrings or using a different error handler.
+
+    Note that when doing arithmetic operations on tags, this class
+    will return a plain normal set as it is no longer associated with
+    the message.
+
+    :param parent: the parent object
+    :param ptr_name: the name of the attribute on the parent which will
+       return the memory pointer.  This allows this object to
+       access the pointer via the parent's descriptor and thus
+       trigger :class:`MemoryPointer`'s memory safety.
+    :param cffi_fn: the callable CFFI wrapper to retrieve the tags
+       iter.  This can be one of notmuch_database_get_all_tags,
+       notmuch_thread_get_tags or notmuch_message_get_tags.
+    """
+
+    def __init__(self, parent, ptr_name, cffi_fn):
+        self._parent = parent
+        self._ptr = lambda: getattr(parent, ptr_name)
+        self._cffi_fn = cffi_fn
+
+    def __del__(self):
+        self._destroy()
+
+    @property
+    def alive(self):
+        return self._parent.alive
+
+    def _destroy(self):
+        pass
+
+    @classmethod
+    def _from_iterable(cls, it):
+        return set(it)
+
+    def __iter__(self):
+        """Return an iterator over the tags.
+
+        Tags are yielded as unicode strings, decoded using the
+        "ignore" error handler.
+
+        :raises NullPointerError: If the iterator can not be created.
+        """
+        return self.iter(encoding='utf-8', errors='ignore')
+
+    def iter(self, *, encoding=None, errors='strict'):
+        """Aternate iterator constructor controlling string decoding.
+
+        Tags are stored as bytes in the notmuch database, in Python
+        it's easier to work with unicode strings and thus is what the
+        normal iterator returns.  However this method allows you to
+        specify how you would like to get the tags, defaulting to the
+        bytestring representation instead of unicode strings.
+
+        :param encoding: Which codec to use.  The default *None* does not
+           decode at all and will return the unmodified bytes.
+           Otherwise this is passed on to :func:`str.decode`.
+        :param errors: If using a codec, this is the error handler.
+           See :func:`str.decode` to which this is passed on.
+
+        :raises NullPointerError: When things do not go as planned.
+        """
+        # self._cffi_fn should point either to
+        # notmuch_database_get_all_tags, notmuch_thread_get_tags or
+        # notmuch_message_get_tags.  nothmuch.h suggests these never
+        # fail, let's handle NULL anyway.
+        tags_p = self._cffi_fn(self._ptr())
+        if tags_p == capi.ffi.NULL:
+            raise errors.NullPointerError()
+        tags = TagsIter(self, tags_p, encoding=encoding, errors=errors)
+        return tags
+
+    def __len__(self):
+        return sum(1 for t in self)
+
+    def __contains__(self, tag):
+        if isinstance(tag, str):
+            tag = tag.encode()
+        for msg_tag in self.iter():
+            if tag == msg_tag:
+                return True
+        else:
+            return False
+
+    def __eq__(self, other):
+        return tuple(sorted(self.iter())) == tuple(sorted(other.iter()))
+
+    def __hash__(self):
+        return hash(tuple(self.iter()))
+
+    def __repr__(self):
+        return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
+            name=self.__class__.__name__,
+            addr=id(self),
+            tags=', '.join(repr(t) for t in self))
+
+
+class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
+    """The tags associated with a message.
+
+    This is a :class:`collections.abc.MutableSet` object which can be
+    used to manipulate the tags of a message.
+
+    Note that due to the underlying notmuch API the performance of the
+    implementation is not the same as you would expect from normal
+    sets.  E.g. the ``in`` operator and variants are O(n) rather then
+    O(1).
+
+    Tags are bytestrings and calling ``iter()`` will return an
+    iterator yielding bytestrings.  However the :meth:`iter` method
+    can be used to return tags as unicode strings, while all other
+    operations accept either byestrings or unicode strings.  In case
+    unicode strings are used they will be encoded using utf-8 before
+    being passed to notmuch.
+    """
+
+    # Since we subclass ImmutableTagSet we inherit a __hash__.  But we
+    # are mutable, setting it to None will make the Python machinary
+    # recognise us as unhashable.
+    __hash__ = None
+
+    def add(self, tag):
+        """Add a tag to the message.
+
+        :param tag: The tag to add.
+        :type tag: str or bytes.  A str will be encoded using UTF-8.
+
+        :param sync_flags: Whether to sync the maildir flags with the
+           new set of tags.  Leaving this as *None* respects the
+           configuration set in the database, while *True* will always
+           sync and *False* will never sync.
+        :param sync_flags: NoneType or bool
+
+        :raises TypeError: If the tag is not a valid type.
+        :raises TagTooLongError: If the added tag exceeds the maximum
+           lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
+        :raises ReadOnlyDatabaseError: If the database is opened in
+           read-only mode.
+        """
+        if isinstance(tag, str):
+            tag = tag.encode()
+        if not isinstance(tag, bytes):
+            raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
+        ret = capi.lib.notmuch_message_add_tag(self._ptr(), tag)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def discard(self, tag):
+        """Remove a tag from the message.
+
+        :param tag: The tag to remove.
+        :type tag: str of bytes.  A str will be encoded using UTF-8.
+        :param sync_flags: Whether to sync the maildir flags with the
+           new set of tags.  Leaving this as *None* respects the
+           configuration set in the database, while *True* will always
+           sync and *False* will never sync.
+        :param sync_flags: NoneType or bool
+
+        :raises TypeError: If the tag is not a valid type.
+        :raises TagTooLongError: If the tag exceeds the maximum
+           lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
+        :raises ReadOnlyDatabaseError: If the database is opened in
+           read-only mode.
+        """
+        if isinstance(tag, str):
+            tag = tag.encode()
+        if not isinstance(tag, bytes):
+            raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
+        ret = capi.lib.notmuch_message_remove_tag(self._ptr(), tag)
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def clear(self):
+        """Remove all tags from the message.
+
+        :raises ReadOnlyDatabaseError: If the database is opened in
+           read-only mode.
+        """
+        ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def from_maildir_flags(self):
+        """Update the tags based on the state in the message's maildir flags.
+
+        This function examines the filenames of 'message' for maildir
+        flags, and adds or removes tags on 'message' as follows when
+        these flags are present:
+
+        Flag    Action if present
+        ----    -----------------
+        'D'     Adds the "draft" tag to the message
+        'F'     Adds the "flagged" tag to the message
+        'P'     Adds the "passed" tag to the message
+        'R'     Adds the "replied" tag to the message
+        'S'     Removes the "unread" tag from the message
+
+        For each flag that is not present, the opposite action
+        (add/remove) is performed for the corresponding tags.
+
+        Flags are identified as trailing components of the filename
+        after a sequence of ":2,".
+
+        If there are multiple filenames associated with this message,
+        the flag is considered present if it appears in one or more
+        filenames. (That is, the flags from the multiple filenames are
+        combined with the logical OR operator.)
+        """
+        ret = capi.lib.notmuch_message_maildir_flags_to_tags(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+    def to_maildir_flags(self):
+        """Update the message's maildir flags based on the notmuch tags.
+
+        If the message's filename is in a maildir directory, that is a
+        directory named ``new`` or ``cur``, and has a valid maildir
+        filename then the flags will be added as such:
+
+        'D' if the message has the "draft" tag
+        'F' if the message has the "flagged" tag
+        'P' if the message has the "passed" tag
+        'R' if the message has the "replied" tag
+        'S' if the message does not have the "unread" tag
+
+        Any existing flags unmentioned in the list above will be
+        preserved in the renaming.
+
+        Also, if this filename is in a directory named "new", rename it to
+        be within the neighboring directory named "cur".
+
+        In case there are multiple files associated with the message
+        all filenames will get the same logic applied.
+        """
+        ret = capi.lib.notmuch_message_tags_to_maildir_flags(self._ptr())
+        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
+            raise errors.NotmuchError(ret)
+
+
+class TagsIter(base.NotmuchObject, collections.abc.Iterator):
+    """Iterator over tags.
+
+    This is only an interator, not a container so calling
+    :meth:`__iter__` does not return a new, replenished iterator but
+    only itself.
+
+    :param parent: The parent object to keep alive.
+    :param tags_p: The CFFI pointer to the C-level tags iterator.
+    :param encoding: Which codec to use.  The default *None* does not
+       decode at all and will return the unmodified bytes.
+       Otherwise this is passed on to :func:`str.decode`.
+    :param errors: If using a codec, this is the error handler.
+       See :func:`str.decode` to which this is passed on.
+
+    :raises ObjectDestoryedError: if used after destroyed.
+    """
+    _tags_p = base.MemoryPointer()
+
+    def __init__(self, parent, tags_p, *, encoding=None, errors='strict'):
+        self._parent = parent
+        self._tags_p = tags_p
+        self._encoding = encoding
+        self._errors = errors
+
+    def __del__(self):
+        self._destroy()
+
+    @property
+    def alive(self):
+        if not self._parent.alive:
+            return False
+        try:
+            self._tags_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def _destroy(self):
+        if self.alive:
+            try:
+                capi.lib.notmuch_tags_destroy(self._tags_p)
+            except errors.ObjectDestroyedError:
+                pass
+        self._tags_p = None
+
+    def __iter__(self):
+        """Return the iterator itself.
+
+        Note that as this is an iterator and not a container this will
+        not return a new iterator.  Thus any elements already consumed
+        will not be yielded by the :meth:`__next__` method anymore.
+        """
+        return self
+
+    def __next__(self):
+        if not capi.lib.notmuch_tags_valid(self._tags_p):
+            self._destroy()
+            raise StopIteration()
+        tag_p = capi.lib.notmuch_tags_get(self._tags_p)
+        tag = capi.ffi.string(tag_p)
+        if self._encoding:
+            tag = tag.decode(encoding=self._encoding, errors=self._errors)
+        capi.lib.notmuch_tags_move_to_next(self._tags_p)
+        return tag
+
+    def __repr__(self):
+        try:
+            self._tags_p
+        except errors.ObjectDestroyedError:
+            return '<TagsIter (exhausted)>'
+        else:
+            return '<TagsIter>'
diff --git a/bindings/python-cffi/notmuch2/_thread.py b/bindings/python-cffi/notmuch2/_thread.py
new file mode 100644 (file)
index 0000000..a754749
--- /dev/null
@@ -0,0 +1,190 @@
+import collections.abc
+import weakref
+
+from notmuch2 import _base as base
+from notmuch2 import _capi as capi
+from notmuch2 import _errors as errors
+from notmuch2 import _message as message
+from notmuch2 import _tags as tags
+
+
+__all__ = ['Thread']
+
+
+class Thread(base.NotmuchObject, collections.abc.Iterable):
+    _thread_p = base.MemoryPointer()
+
+    def __init__(self, parent, thread_p, *, db):
+        self._parent = parent
+        self._thread_p = thread_p
+        self._db = db
+
+    @property
+    def alive(self):
+        if not self._parent.alive:
+            return False
+        try:
+            self._thread_p
+        except errors.ObjectDestroyedError:
+            return False
+        else:
+            return True
+
+    def __del__(self):
+        self._destroy()
+
+    def _destroy(self):
+        if self.alive:
+            capi.lib.notmuch_thread_destroy(self._thread_p)
+        self._thread_p = None
+
+    @property
+    def threadid(self):
+        """The thread ID as a :class:`BinString`.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_thread_get_thread_id(self._thread_p)
+        return base.BinString.from_cffi(ret)
+
+    def __len__(self):
+        """Return the number of messages in the thread.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        return capi.lib.notmuch_thread_get_total_messages(self._thread_p)
+
+    def toplevel(self):
+        """Return an iterator of the toplevel messages.
+
+        :returns: An iterator yielding :class:`Message` instances.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        msgs_p = capi.lib.notmuch_thread_get_toplevel_messages(self._thread_p)
+        return message.MessageIter(self, msgs_p, db=self._db)
+
+    def __iter__(self):
+        """Return an iterator over all the messages in the thread.
+
+        :returns: An iterator yielding :class:`Message` instances.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        msgs_p = capi.lib.notmuch_thread_get_messages(self._thread_p)
+        return message.MessageIter(self, msgs_p, db=self._db)
+
+    @property
+    def matched(self):
+        """The number of messages in this thread which matched the query.
+
+        Of the messages in the thread this gives the count of messages
+        which did directly match the search query which this thread
+        originates from.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        return capi.lib.notmuch_thread_get_matched_messages(self._thread_p)
+
+    @property
+    def authors(self):
+        """A comma-separated string of all authors in the thread.
+
+        Authors of messages which matched the query the thread was
+        retrieved from will be at the head of the string, ordered by
+        date of their messages.  Following this will be the authors of
+        the other messages in the thread, also ordered by date of
+        their messages.  Both groups of authors are separated by the
+        ``|`` character.
+
+        :returns: The stringified list of authors.
+        :rtype: BinString
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_thread_get_authors(self._thread_p)
+        return base.BinString.from_cffi(ret)
+
+    @property
+    def subject(self):
+        """The subject of the thread, taken from the first message.
+
+        The thread's subject is taken to be the subject of the first
+        message according to query sort order.
+
+        :returns: The thread's subject.
+        :rtype: BinString
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        ret = capi.lib.notmuch_thread_get_subject(self._thread_p)
+        return base.BinString.from_cffi(ret)
+
+    @property
+    def first(self):
+        """Return the date of the oldest message in the thread.
+
+        The time the first message was sent as an integer number of
+        seconds since the *epoch*, 1 Jan 1970.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        return capi.lib.notmuch_thread_get_oldest_date(self._thread_p)
+
+    @property
+    def last(self):
+        """Return the date of the newest message in the thread.
+
+        The time the last message was sent as an integer number of
+        seconds since the *epoch*, 1 Jan 1970.
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        return capi.lib.notmuch_thread_get_newest_date(self._thread_p)
+
+    @property
+    def tags(self):
+        """Return an immutable set with all tags used in this thread.
+
+        This returns an immutable set-like object implementing the
+        collections.abc.Set Abstract Base Class.  Due to the
+        underlying libnotmuch implementation some operations have
+        different performance characteristics then plain set objects.
+        Mainly any lookup operation is O(n) rather then O(1).
+
+        Normal usage treats tags as UTF-8 encoded unicode strings so
+        they are exposed to Python as normal unicode string objects.
+        If you need to handle tags stored in libnotmuch which are not
+        valid unicode do check the :class:`ImmutableTagSet` docs for
+        how to handle this.
+
+        :rtype: ImmutableTagSet
+
+        :raises ObjectDestroyedError: if used after destoryed.
+        """
+        try:
+            ref = self._cached_tagset
+        except AttributeError:
+            tagset = None
+        else:
+            tagset = ref()
+        if tagset is None:
+            tagset = tags.ImmutableTagSet(
+                self, '_thread_p', capi.lib.notmuch_thread_get_tags)
+            self._cached_tagset = weakref.ref(tagset)
+        return tagset
+
+
+class ThreadIter(base.NotmuchIter):
+
+    def __init__(self, parent, threads_p, *, db):
+        self._db = db
+        super().__init__(parent, threads_p,
+                         fn_destroy=capi.lib.notmuch_threads_destroy,
+                         fn_valid=capi.lib.notmuch_threads_valid,
+                         fn_get=capi.lib.notmuch_threads_get,
+                         fn_next=capi.lib.notmuch_threads_move_to_next)
+
+    def __next__(self):
+        thread_p = super().__next__()
+        return Thread(self, thread_p, db=self._db)
index 7baf63cf528910be145d65791c69ae0c2fae5763..37918e3d2cdb3b7bcf0a3e3db860bfab5bd1b0db 100644 (file)
@@ -2,7 +2,7 @@ import setuptools
 
 
 setuptools.setup(
-    name='notdb',
+    name='notmuch2',
     version='0.1',
     description='Pythonic bindings for the notmuch mail database using CFFI',
     author='Floris Bruynooghe',
@@ -10,7 +10,7 @@ setuptools.setup(
     setup_requires=['cffi>=1.0.0'],
     install_requires=['cffi>=1.0.0'],
     packages=setuptools.find_packages(exclude=['tests']),
-    cffi_modules=['notdb/_build.py:ffibuilder'],
+    cffi_modules=['notmuch2/_build.py:ffibuilder'],
     classifiers=[
         'Development Status :: 3 - Alpha',
         'Intended Audience :: Developers',
index b6d3d62cb71f8db41fe3c77920194bc8625b5acf..d3280a67dd0a7a6623a1056224d6191ae85aff38 100644 (file)
@@ -1,7 +1,7 @@
 import pytest
 
-from notdb import _base as base
-from notdb import _errors as errors
+from notmuch2 import _base as base
+from notmuch2 import _errors as errors
 
 
 class TestNotmuchObject:
index 02de0f41ab27475b8a22ad35241367d994c02f54..e3a8344d8cded01e3a0ee3895faaa9173a816745 100644 (file)
@@ -5,10 +5,10 @@ import pathlib
 
 import pytest
 
-import notdb
-import notdb._errors as errors
-import notdb._database as dbmod
-import notdb._message as message
+import notmuch2
+import notmuch2._errors as errors
+import notmuch2._database as dbmod
+import notmuch2._message as message
 
 
 @pytest.fixture
@@ -279,7 +279,7 @@ class TestQuery:
 
     @pytest.fixture
     def db(self, maildir, notmuch):
-        """Return a read-only notdb.Database.
+        """Return a read-only notmuch2.Database.
 
         The database will have 3 messages, 2 threads.
         """
@@ -306,7 +306,7 @@ class TestQuery:
     def test_message_match(self, db):
         msgs = db.messages('*')
         msg = next(msgs)
-        assert isinstance(msg, notdb.Message)
+        assert isinstance(msg, notmuch2.Message)
 
     def test_count_threads(self, db):
         assert db.count_threads('*') == 2
@@ -323,4 +323,4 @@ class TestQuery:
     def test_threads_match(self, db):
         threads = db.threads('*')
         thread = next(threads)
-        assert isinstance(thread, notdb.Thread)
+        assert isinstance(thread, notmuch2.Thread)
index 56d06f34cb88891dd5514b8ca3d6359e79acddbc..532bf92159dd86cb23c5cd50133fa5444c4418dd 100644 (file)
@@ -4,7 +4,7 @@ import pathlib
 
 import pytest
 
-import notdb
+import notmuch2
 
 
 class TestMessage:
@@ -17,7 +17,7 @@ class TestMessage:
 
     @pytest.fixture
     def db(self, maildir):
-        with notdb.Database.create(maildir.path) as db:
+        with notmuch2.Database.create(maildir.path) as db:
             yield db
 
     @pytest.fixture
@@ -26,8 +26,8 @@ class TestMessage:
         yield msg
 
     def test_type(self, msg):
-        assert isinstance(msg, notdb.NotmuchObject)
-        assert isinstance(msg, notdb.Message)
+        assert isinstance(msg, notmuch2.NotmuchObject)
+        assert isinstance(msg, notmuch2.Message)
 
     def test_alive(self, msg):
         assert msg.alive
@@ -41,7 +41,7 @@ class TestMessage:
 
     def test_messageid_type(self, msg):
         assert isinstance(msg.messageid, str)
-        assert isinstance(msg.messageid, notdb.BinString)
+        assert isinstance(msg.messageid, notmuch2.BinString)
         assert isinstance(bytes(msg.messageid), bytes)
 
     def test_messageid(self, msg, maildir_msg):
@@ -53,7 +53,7 @@ class TestMessage:
 
     def test_threadid_type(self, msg):
         assert isinstance(msg.threadid, str)
-        assert isinstance(msg.threadid, notdb.BinString)
+        assert isinstance(msg.threadid, notmuch2.BinString)
         assert isinstance(bytes(msg.threadid), bytes)
 
     def test_path_type(self, msg):
@@ -142,7 +142,7 @@ class TestProperties:
     @pytest.fixture
     def props(self, maildir):
         msgid, path = maildir.deliver()
-        with notdb.Database.create(maildir.path) as db:
+        with notmuch2.Database.create(maildir.path) as db:
             msg, dup = db.add(path, sync_flags=False)
             yield msg.properties
 
index 0cb42d8924d0b3ffdde8da389e8d8404e8bb024d..f12fa1e6b94dbdc4a14ae3efe574c233e1cf7ed3 100644 (file)
@@ -9,8 +9,8 @@ import textwrap
 
 import pytest
 
-from notdb import _database as database
-from notdb import _tags as tags
+from notmuch2 import _database as database
+from notmuch2 import _tags as tags
 
 
 class TestImmutable:
index 366bd8a53ad46f3192b606c61b15ecf34d3b3ad8..1f44b35d2c9e1a798ddeefe597643fb52c4eb30a 100644 (file)
@@ -3,7 +3,7 @@ import time
 
 import pytest
 
-import notdb
+import notmuch2
 
 
 @pytest.fixture
@@ -13,17 +13,17 @@ def thread(maildir, notmuch):
     maildir.deliver(body='bar',
                     headers=[('In-Reply-To', '<{}>'.format(msgid))])
     notmuch('new')
-    with notdb.Database(maildir.path) as db:
+    with notmuch2.Database(maildir.path) as db:
         yield next(db.threads('foo'))
 
 
 def test_type(thread):
-    assert isinstance(thread, notdb.Thread)
+    assert isinstance(thread, notmuch2.Thread)
     assert isinstance(thread, collections.abc.Iterable)
 
 
 def test_threadid(thread):
-    assert isinstance(thread.threadid, notdb.BinString)
+    assert isinstance(thread.threadid, notmuch2.BinString)
     assert thread.threadid
 
 
@@ -37,21 +37,21 @@ def test_toplevel_type(thread):
 
 def test_toplevel(thread):
     msgs = thread.toplevel()
-    assert isinstance(next(msgs), notdb.Message)
+    assert isinstance(next(msgs), notmuch2.Message)
     with pytest.raises(StopIteration):
         next(msgs)
 
 
 def test_toplevel_reply(thread):
     msg = next(thread.toplevel())
-    assert isinstance(next(msg.replies()), notdb.Message)
+    assert isinstance(next(msg.replies()), notmuch2.Message)
 
 
 def test_iter(thread):
     msgs = list(iter(thread))
     assert len(msgs) == len(thread)
     for msg in msgs:
-        assert isinstance(msg, notdb.Message)
+        assert isinstance(msg, notmuch2.Message)
 
 
 def test_matched(thread):
@@ -59,7 +59,7 @@ def test_matched(thread):
 
 
 def test_authors_type(thread):
-    assert isinstance(thread.authors, notdb.BinString)
+    assert isinstance(thread.authors, notmuch2.BinString)
 
 
 def test_authors(thread):
@@ -91,7 +91,7 @@ def test_first_last(thread):
 
 
 def test_tags_type(thread):
-    assert isinstance(thread.tags, notdb.ImmutableTagSet)
+    assert isinstance(thread.tags, notmuch2.ImmutableTagSet)
 
 
 def test_tags_cache(thread):
index d6b87987caf250ee7a254c39a3423caf9f08b497..34148a11bf6049ab16faaf35d122dc8e84747712 100644 (file)
@@ -1,6 +1,6 @@
 [pytest]
 minversion = 3.0
-addopts = -ra --cov=notdb --cov=tests
+addopts = -ra --cov=notmuch2 --cov=tests
 
 [tox]
 envlist = py35,py36,py37,pypy35,pypy36
@@ -10,7 +10,7 @@ deps =
      cffi
      pytest
      pytest-cov
-commands = pytest --cov={envsitepackagesdir}/notdb {posargs}
+commands = pytest --cov={envsitepackagesdir}/notmuch2 {posargs}
 
 [testenv:pypy35]
 basepython = pypy3.5