From: Floris Bruynooghe Date: Sun, 17 Nov 2019 16:41:35 +0000 (+0100) Subject: Rename package to notmuch2 X-Git-Tag: archive/debian/0.30_rc0-1~120 X-Git-Url: https://git.cworth.org/git?p=notmuch;a=commitdiff_plain;h=e2df30f7a98f91543d0b3561dbb366eb4b3d812c Rename package to notmuch2 This is based on a previous discussion on the list where this was more or less seen as the least-bad option. --- diff --git a/bindings/python-cffi/notdb/__init__.py b/bindings/python-cffi/notdb/__init__.py deleted file mode 100644 index 67051df5..00000000 --- a/bindings/python-cffi/notdb/__init__.py +++ /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 index acb64413..00000000 --- a/bindings/python-cffi/notdb/_base.py +++ /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 '' - else: - return '' diff --git a/bindings/python-cffi/notdb/_build.py b/bindings/python-cffi/notdb/_build.py deleted file mode 100644 index 6be7e5b1..00000000 --- a/bindings/python-cffi/notdb/_build.py +++ /dev/null @@ -1,302 +0,0 @@ -import cffi - - -ffibuilder = cffi.FFI() -ffibuilder.set_source( - 'notdb._capi', - r""" - #include - #include - #include - - #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 index d414082a..00000000 --- a/bindings/python-cffi/notdb/_database.py +++ /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 index 924e722f..00000000 --- a/bindings/python-cffi/notdb/_errors.py +++ /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 index 9b2b037f..00000000 --- a/bindings/python-cffi/notdb/_message.py +++ /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: - - :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 index 613aaf12..00000000 --- a/bindings/python-cffi/notdb/_query.py +++ /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 index a25a2264..00000000 --- a/bindings/python-cffi/notdb/_tags.py +++ /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 '' - else: - return '' diff --git a/bindings/python-cffi/notdb/_thread.py b/bindings/python-cffi/notdb/_thread.py deleted file mode 100644 index e1ef6b07..00000000 --- a/bindings/python-cffi/notdb/_thread.py +++ /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 index 00000000..4d76ec15 --- /dev/null +++ b/bindings/python-cffi/notmuch2/__init__.py @@ -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 index 00000000..31258149 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_base.py @@ -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 '' + else: + return '' diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py new file mode 100644 index 00000000..3ba3e558 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_build.py @@ -0,0 +1,302 @@ +import cffi + + +ffibuilder = cffi.FFI() +ffibuilder.set_source( + 'notmuch2._capi', + r""" + #include + #include + #include + + #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 index 00000000..a15c4d03 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_database.py @@ -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 index 00000000..1c88763b --- /dev/null +++ b/bindings/python-cffi/notmuch2/_errors.py @@ -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 index 00000000..bb561426 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_message.py @@ -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: + + :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 index 00000000..1db6ec96 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_query.py @@ -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 index 00000000..fe422a79 --- /dev/null +++ b/bindings/python-cffi/notmuch2/_tags.py @@ -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 '' + else: + return '' diff --git a/bindings/python-cffi/notmuch2/_thread.py b/bindings/python-cffi/notmuch2/_thread.py new file mode 100644 index 00000000..a754749f --- /dev/null +++ b/bindings/python-cffi/notmuch2/_thread.py @@ -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) diff --git a/bindings/python-cffi/setup.py b/bindings/python-cffi/setup.py index 7baf63cf..37918e3d 100644 --- a/bindings/python-cffi/setup.py +++ b/bindings/python-cffi/setup.py @@ -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', diff --git a/bindings/python-cffi/tests/test_base.py b/bindings/python-cffi/tests/test_base.py index b6d3d62c..d3280a67 100644 --- a/bindings/python-cffi/tests/test_base.py +++ b/bindings/python-cffi/tests/test_base.py @@ -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: diff --git a/bindings/python-cffi/tests/test_database.py b/bindings/python-cffi/tests/test_database.py index 02de0f41..e3a8344d 100644 --- a/bindings/python-cffi/tests/test_database.py +++ b/bindings/python-cffi/tests/test_database.py @@ -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) diff --git a/bindings/python-cffi/tests/test_message.py b/bindings/python-cffi/tests/test_message.py index 56d06f34..532bf921 100644 --- a/bindings/python-cffi/tests/test_message.py +++ b/bindings/python-cffi/tests/test_message.py @@ -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 diff --git a/bindings/python-cffi/tests/test_tags.py b/bindings/python-cffi/tests/test_tags.py index 0cb42d89..f12fa1e6 100644 --- a/bindings/python-cffi/tests/test_tags.py +++ b/bindings/python-cffi/tests/test_tags.py @@ -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: diff --git a/bindings/python-cffi/tests/test_thread.py b/bindings/python-cffi/tests/test_thread.py index 366bd8a5..1f44b35d 100644 --- a/bindings/python-cffi/tests/test_thread.py +++ b/bindings/python-cffi/tests/test_thread.py @@ -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): diff --git a/bindings/python-cffi/tox.ini b/bindings/python-cffi/tox.ini index d6b87987..34148a11 100644 --- a/bindings/python-cffi/tox.ini +++ b/bindings/python-cffi/tox.ini @@ -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