]> git.cworth.org Git - notmuch-old/blobdiff - bindings/python-cffi/notdb/_message.py
Rename package to notmuch2
[notmuch-old] / bindings / python-cffi / notdb / _message.py
diff --git a/bindings/python-cffi/notdb/_message.py b/bindings/python-cffi/notdb/_message.py
deleted file mode 100644 (file)
index 9b2b037..0000000
+++ /dev/null
@@ -1,691 +0,0 @@
-import collections
-import contextlib
-import os
-import pathlib
-import weakref
-
-import notdb._base as base
-import notdb._capi as capi
-import notdb._errors as errors
-import notdb._tags as tags
-
-
-__all__ = ['Message']
-
-
-class Message(base.NotmuchObject):
-    """An email message stored in the notmuch database.
-
-    This should not be directly created, instead it will be returned
-    by calling methods on :class:`Database`.  A message keeps a
-    reference to the database object since the database object can not
-    be released while the message is in use.
-
-    Note that this represents a message in the notmuch database.  For
-    full email functionality you may want to use the :mod:`email`
-    package from Python's standard library.  You could e.g. create
-    this as such::
-
-       notmuch_msg = db.get_message(msgid)  # or from a query
-       parser = email.parser.BytesParser(policy=email.policy.default)
-       with notmuch_msg.path.open('rb) as fp:
-           email_msg = parser.parse(fp)
-
-    Most commonly the functionality provided by notmuch is sufficient
-    to read email however.
-
-    Messages are considered equal when they have the same message ID.
-    This is how libnotmuch treats messages as well, the
-    :meth:`pathnames` function returns multiple results for
-    duplicates.
-
-    :param parent: The parent object.  This is probably one off a
-       :class:`Database`, :class:`Thread` or :class:`Query`.
-    :type parent: NotmuchObject
-    :param db: The database instance this message is associated with.
-       This could be the same as the parent.
-    :type db: Database
-    :param msg_p: The C pointer to the ``notmuch_message_t``.
-    :type msg_p: <cdata>
-
-    :param dup: Whether the message was a duplicate on insertion.
-
-    :type dup: None or bool
-    """
-    _msg_p = base.MemoryPointer()
-
-    def __init__(self, parent, msg_p, *, db):
-        self._parent = parent
-        self._msg_p = msg_p
-        self._db = db
-
-    @property
-    def alive(self):
-        if not self._parent.alive:
-            return False
-        try:
-            self._msg_p
-        except errors.ObjectDestroyedError:
-            return False
-        else:
-            return True
-
-    def __del__(self):
-        self._destroy()
-
-    def _destroy(self):
-        if self.alive:
-            capi.lib.notmuch_message_destroy(self._msg_p)
-        self._msg_p = None
-
-    @property
-    def messageid(self):
-        """The message ID as a string.
-
-        The message ID is decoded with the ignore error handler.  This
-        is fine as long as the message ID is well formed.  If it is
-        not valid ASCII then this will be lossy.  So if you need to be
-        able to write the exact same message ID back you should use
-        :attr:`messageidb`.
-
-        Note that notmuch will decode the message ID value and thus
-        strip off the surrounding ``<`` and ``>`` characters.  This is
-        different from Python's :mod:`email` package behaviour which
-        leaves these characters in place.
-
-        :returns: The message ID.
-        :rtype: :class:`BinString`, this is a normal str but calling
-           bytes() on it will return the original bytes used to create
-           it.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
-        return base.BinString(capi.ffi.string(ret))
-
-    @property
-    def threadid(self):
-        """The thread ID.
-
-        The thread ID is decoded with the surrogateescape error
-        handler so that it is possible to reconstruct the original
-        thread ID if it is not valid UTF-8.
-
-        :returns: The thread ID.
-        :rtype: :class:`BinString`, this is a normal str but calling
-           bytes() on it will return the original bytes used to create
-           it.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
-        return base.BinString(capi.ffi.string(ret))
-
-    @property
-    def path(self):
-        """A pathname of the message as a pathlib.Path instance.
-
-        If multiple files in the database contain the same message ID
-        this will be just one of the files, chosen at random.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_filename(self._msg_p)
-        return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
-
-    @property
-    def pathb(self):
-        """A pathname of the message as a bytes object.
-
-        See :attr:`path` for details, this is the same but does return
-        the path as a bytes object which is faster but less convenient.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_filename(self._msg_p)
-        return capi.ffi.string(ret)
-
-    def filenames(self):
-        """Return an iterator of all files for this message.
-
-        If multiple files contained the same message ID they will all
-        be returned here.  The files are returned as intances of
-        :class:`pathlib.Path`.
-
-        :returns: Iterator yielding :class:`pathlib.Path` instances.
-        :rtype: iter
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
-        return PathIter(self, fnames_p)
-
-    def filenamesb(self):
-        """Return an iterator of all files for this message.
-
-        This is like :meth:`pathnames` but the files are returned as
-        byte objects instead.
-
-        :returns: Iterator yielding :class:`bytes` instances.
-        :rtype: iter
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
-        return FilenamesIter(self, fnames_p)
-
-    @property
-    def ghost(self):
-        """Indicates whether this message is a ghost message.
-
-        A ghost message if a message which we know exists, but it has
-        no files or content associated with it.  This can happen if
-        it was referenced by some other message.  Only the
-        :attr:`messageid` and :attr:`threadid` attributes are valid
-        for it.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_flag(
-            self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
-        return bool(ret)
-
-    @property
-    def excluded(self):
-        """Indicates whether this message was excluded from the query.
-
-        When a message is created from a search, sometimes messages
-        that where excluded by the search query could still be
-        returned by it, e.g. because they are part of a thread
-        matching the query.  the :meth:`Database.query` method allows
-        these messages to be flagged, which results in this property
-        being set to *True*.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_get_flag(
-            self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
-        return bool(ret)
-
-    @property
-    def date(self):
-        """The message date as an integer.
-
-        The time the message was sent as an integer number of seconds
-        since the *epoch*, 1 Jan 1970.  This is derived from the
-        message's header, you can get the original header value with
-        :meth:`header`.
-
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        return capi.lib.notmuch_message_get_date(self._msg_p)
-
-    def header(self, name):
-        """Return the value of the named header.
-
-        Returns the header from notmuch, some common headers are
-        stored in the database, others are read from the file.
-        Headers are returned with their newlines stripped and
-        collapsed concatenated together if they occur multiple times.
-        You may be better off using the standard library email
-        package's ``email.message_from_file(msg.path.open())`` if that
-        is not sufficient for you.
-
-        :param header: Case-insensitive header name to retrieve.
-        :type header: str or bytes
-
-        :returns: The header value, an empty string if the header is
-           not present.
-        :rtype: str
-
-        :raises LookupError: if the header is not present.
-        :raises NullPointerError: For unexpected notmuch errors.
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        # The returned is supposedly guaranteed to be UTF-8.  Header
-        # names must be ASCII as per RFC x822.
-        if isinstance(name, str):
-            name = name.encode('ascii')
-        ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
-        if ret == capi.ffi.NULL:
-            raise errors.NullPointerError()
-        hdr = capi.ffi.string(ret)
-        if not hdr:
-            raise LookupError
-        return hdr.decode(encoding='utf-8')
-
-    @property
-    def tags(self):
-        """The tags associated with the message.
-
-        This behaves as a set.  But removing and adding items to the
-        set removes and adds them to the message in the database.
-
-        :raises ReadOnlyDatabaseError: When manipulating tags on a
-           database opened in read-only mode.
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        try:
-            ref = self._cached_tagset
-        except AttributeError:
-            tagset = None
-        else:
-            tagset = ref()
-        if tagset is None:
-            tagset = tags.MutableTagSet(
-                self, '_msg_p', capi.lib.notmuch_message_get_tags)
-            self._cached_tagset = weakref.ref(tagset)
-        return tagset
-
-    @contextlib.contextmanager
-    def frozen(self):
-        """Context manager to freeze the message state.
-
-        This allows you to perform atomic tag updates::
-
-           with msg.frozen():
-               msg.tags.clear()
-               msg.tags.add('foo')
-
-        Using This would ensure the message never ends up with no tags
-        applied at all.
-
-        It is safe to nest calls to this context manager.
-
-        :raises ReadOnlyDatabaseError: if the database is opened in
-           read-only mode.
-        :raises UnbalancedFreezeThawError: if you somehow managed to
-           call __exit__ of this context manager more than once.  Why
-           did you do that?
-        :raises ObjectDestroyedError: if used after destoryed.
-        """
-        ret = capi.lib.notmuch_message_freeze(self._msg_p)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        self._frozen = True
-        try:
-            yield
-        except Exception:
-            # Only way to "rollback" these changes is to destroy
-            # ourselves and re-create.  Behold.
-            msgid = self.messageid
-            self._destroy()
-            with contextlib.suppress(Exception):
-                new = self._db.find(msgid)
-                self._msg_p = new._msg_p
-                new._msg_p = None
-                del new
-            raise
-        else:
-            ret = capi.lib.notmuch_message_thaw(self._msg_p)
-            if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-                raise errors.NotmuchError(ret)
-            self._frozen = False
-
-    @property
-    def properties(self):
-        """A map of arbitrary key-value pairs associated with the message.
-
-        Be aware that properties may be used by other extensions to
-        store state in.  So delete or modify with care.
-
-        The properties map is somewhat special.  It is essentially a
-        multimap-like structure where each key can have multiple
-        values.  Therefore accessing a single item using
-        :meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
-        will only return you the *first* item if there are multiple
-        and thus are only recommended if you know there to be only one
-        value.
-
-        Instead the map has an additional :meth:`PropertiesMap.all`
-        method which can be used to retrieve all properties of a given
-        key.  This method also allows iterating of a a subset of the
-        keys starting with a given prefix.
-        """
-        try:
-            ref = self._cached_props
-        except AttributeError:
-            props = None
-        else:
-            props = ref()
-        if props is None:
-            props = PropertiesMap(self, '_msg_p')
-            self._cached_props = weakref.ref(props)
-        return props
-
-    def replies(self):
-        """Return an iterator of all replies to this message.
-
-        This method will only work if the message was created from a
-        thread.  Otherwise it will yield no results.
-
-        :returns: An iterator yielding :class:`Message` instances.
-        :rtype: MessageIter
-        """
-        # The notmuch_messages_valid call accepts NULL and this will
-        # become an empty iterator, raising StopIteration immediately.
-        # Hence no return value checking here.
-        msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p)
-        return MessageIter(self, msgs_p, db=self._db)
-
-    def __hash__(self):
-        return hash(self.messageid)
-
-    def __eq__(self, other):
-        if isinstance(other, self.__class__):
-            return self.messageid == other.messageid
-
-
-class FilenamesIter(base.NotmuchIter):
-    """Iterator for binary filenames objects."""
-
-    def __init__(self, parent, iter_p):
-        super().__init__(parent, iter_p,
-                         fn_destroy=capi.lib.notmuch_filenames_destroy,
-                         fn_valid=capi.lib.notmuch_filenames_valid,
-                         fn_get=capi.lib.notmuch_filenames_get,
-                         fn_next=capi.lib.notmuch_filenames_move_to_next)
-
-    def __next__(self):
-        fname = super().__next__()
-        return capi.ffi.string(fname)
-
-
-class PathIter(FilenamesIter):
-    """Iterator for pathlib.Path objects."""
-
-    def __next__(self):
-        fname = super().__next__()
-        return pathlib.Path(os.fsdecode(fname))
-
-
-class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
-    """A mutable mapping to manage properties.
-
-    Both keys and values of properties are supposed to be UTF-8
-    strings in libnotmuch.  However since the uderlying API uses
-    bytestrings you can use either str or bytes to represent keys and
-    all returned keys and values use :class:`BinString`.
-
-    Also be aware that ``iter(this_map)`` will return duplicate keys,
-    while the :class:`collections.abc.KeysView` returned by
-    :meth:`keys` is a :class:`collections.abc.Set` subclass.  This
-    means the former will yield duplicate keys while the latter won't.
-    It also means ``len(list(iter(this_map)))`` could be different
-    than ``len(this_map.keys())``.  ``len(this_map)`` will correspond
-    with the lenght of the default iterator.
-
-    Be aware that libnotmuch exposes all of this as iterators, so
-    quite a few operations have O(n) performance instead of the usual
-    O(1).
-    """
-    Property = collections.namedtuple('Property', ['key', 'value'])
-    _marker = object()
-
-    def __init__(self, msg, ptr_name):
-        self._msg = msg
-        self._ptr = lambda: getattr(msg, ptr_name)
-
-    @property
-    def alive(self):
-        if not self._msg.alive:
-            return False
-        try:
-            self._ptr
-        except errors.ObjectDestroyedError:
-            return False
-        else:
-            return True
-
-    def _destroy(self):
-        pass
-
-    def __iter__(self):
-        """Return an iterator which iterates over the keys.
-
-        Be aware that a single key may have multiple values associated
-        with it, if so it will appear multiple times here.
-        """
-        iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
-        return PropertiesKeyIter(self, iter_p)
-
-    def __len__(self):
-        iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
-        it = base.NotmuchIter(
-            self, iter_p,
-            fn_destroy=capi.lib.notmuch_message_properties_destroy,
-            fn_valid=capi.lib.notmuch_message_properties_valid,
-            fn_get=capi.lib.notmuch_message_properties_key,
-            fn_next=capi.lib.notmuch_message_properties_move_to_next,
-        )
-        return len(list(it))
-
-    def __getitem__(self, key):
-        """Return **the first** peroperty associated with a key."""
-        if isinstance(key, str):
-            key = key.encode('utf-8')
-        value_pp = capi.ffi.new('char**')
-        ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-        if value_pp[0] == capi.ffi.NULL:
-            raise KeyError
-        return base.BinString.from_cffi(value_pp[0])
-
-    def keys(self):
-        """Return a :class:`collections.abc.KeysView` for this map.
-
-        Even when keys occur multiple times this is a subset of set()
-        so will only contain them once.
-        """
-        return collections.abc.KeysView({k: None for k in self})
-
-    def items(self):
-        """Return a :class:`collections.abc.ItemsView` for this map.
-
-        The ItemsView treats a ``(key, value)`` pair as unique, so
-        dupcliate ``(key, value)`` pairs will be merged together.
-        However duplicate keys with different values will be returned.
-        """
-        items = set()
-        props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
-        while capi.lib.notmuch_message_properties_valid(props_p):
-            key = capi.lib.notmuch_message_properties_key(props_p)
-            value = capi.lib.notmuch_message_properties_value(props_p)
-            items.add((base.BinString.from_cffi(key),
-                       base.BinString.from_cffi(value)))
-            capi.lib.notmuch_message_properties_move_to_next(props_p)
-        capi.lib.notmuch_message_properties_destroy(props_p)
-        return PropertiesItemsView(items)
-
-    def values(self):
-        """Return a :class:`collecions.abc.ValuesView` for this map.
-
-        All unique property values are included in the view.
-        """
-        values = set()
-        props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
-        while capi.lib.notmuch_message_properties_valid(props_p):
-            value = capi.lib.notmuch_message_properties_value(props_p)
-            values.add(base.BinString.from_cffi(value))
-            capi.lib.notmuch_message_properties_move_to_next(props_p)
-        capi.lib.notmuch_message_properties_destroy(props_p)
-        return PropertiesValuesView(values)
-
-    def __setitem__(self, key, value):
-        """Add a key-value pair to the properties.
-
-        You may prefer to use :meth:`add` for clarity since this
-        method usually implies implicit overwriting of an existing key
-        if it exists, while for properties this is not the case.
-        """
-        self.add(key, value)
-
-    def add(self, key, value):
-        """Add a key-value pair to the properties."""
-        if isinstance(key, str):
-            key = key.encode('utf-8')
-        if isinstance(value, str):
-            value = value.encode('utf-8')
-        ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def __delitem__(self, key):
-        """Remove all properties with this key."""
-        if isinstance(key, str):
-            key = key.encode('utf-8')
-        ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def remove(self, key, value):
-        """Remove a key-value pair from the properties."""
-        if isinstance(key, str):
-            key = key.encode('utf-8')
-        if isinstance(value, str):
-            value = value.encode('utf-8')
-        ret = capi.lib.notmuch_message_remove_property(self._ptr(), key, value)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def pop(self, key, default=_marker):
-        try:
-            value = self[key]
-        except KeyError:
-            if default is self._marker:
-                raise
-            else:
-                return default
-        else:
-            self.remove(key, value)
-            return value
-
-    def popitem(self):
-        try:
-            key = next(iter(self))
-        except StopIteration:
-            raise KeyError
-        value = self.pop(key)
-        return (key, value)
-
-    def clear(self):
-        ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
-                                                             capi.ffi.NULL)
-        if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
-            raise errors.NotmuchError(ret)
-
-    def getall(self, prefix='', *, exact=False):
-        """Return an iterator yielding all properties for a given key prefix.
-
-        The returned iterator yields all peroperties which start with
-        a given key prefix as ``(key, value)`` namedtuples.  If called
-        with ``exact=True`` then only properties which exactly match
-        the prefix are returned, those a key longer than the prefix
-        will not be included.
-
-        :param prefix: The prefix of the key.
-        """
-        if isinstance(prefix, str):
-            prefix = prefix.encode('utf-8')
-        props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
-                                                          prefix, exact)
-        return PropertiesIter(self, props_p)
-
-
-class PropertiesKeyIter(base.NotmuchIter):
-
-    def __init__(self, parent, iter_p):
-        super().__init__(
-            parent,
-            iter_p,
-            fn_destroy=capi.lib.notmuch_message_properties_destroy,
-            fn_valid=capi.lib.notmuch_message_properties_valid,
-            fn_get=capi.lib.notmuch_message_properties_key,
-            fn_next=capi.lib.notmuch_message_properties_move_to_next)
-
-    def __next__(self):
-        item = super().__next__()
-        return base.BinString.from_cffi(item)
-
-
-class PropertiesIter(base.NotmuchIter):
-
-    def __init__(self, parent, iter_p):
-        super().__init__(
-            parent,
-            iter_p,
-            fn_destroy=capi.lib.notmuch_message_properties_destroy,
-            fn_valid=capi.lib.notmuch_message_properties_valid,
-            fn_get=capi.lib.notmuch_message_properties_key,
-            fn_next=capi.lib.notmuch_message_properties_move_to_next,
-        )
-
-    def __next__(self):
-        if not self._fn_valid(self._iter_p):
-            self._destroy()
-            raise StopIteration
-        key = capi.lib.notmuch_message_properties_key(self._iter_p)
-        value = capi.lib.notmuch_message_properties_value(self._iter_p)
-        capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
-        return PropertiesMap.Property(base.BinString.from_cffi(key),
-                                      base.BinString.from_cffi(value))
-
-
-class PropertiesItemsView(collections.abc.Set):
-
-    __slots__ = ('_items',)
-
-    def __init__(self, items):
-        self._items = items
-
-    @classmethod
-    def _from_iterable(self, it):
-        return set(it)
-
-    def __len__(self):
-        return len(self._items)
-
-    def __contains__(self, item):
-        return item in self._items
-
-    def __iter__(self):
-        yield from self._items
-
-
-collections.abc.ItemsView.register(PropertiesItemsView)
-
-
-class PropertiesValuesView(collections.abc.Set):
-
-    __slots__ = ('_values',)
-
-    def __init__(self, values):
-        self._values = values
-
-    def __len__(self):
-        return len(self._values)
-
-    def __contains__(self, value):
-        return value in self._values
-
-    def __iter__(self):
-        yield from self._values
-
-
-collections.abc.ValuesView.register(PropertiesValuesView)
-
-
-class MessageIter(base.NotmuchIter):
-
-    def __init__(self, parent, msgs_p, *, db):
-        self._db = db
-        super().__init__(parent, msgs_p,
-                         fn_destroy=capi.lib.notmuch_messages_destroy,
-                         fn_valid=capi.lib.notmuch_messages_valid,
-                         fn_get=capi.lib.notmuch_messages_get,
-                         fn_next=capi.lib.notmuch_messages_move_to_next)
-
-    def __next__(self):
-        msg_p = super().__next__()
-        return Message(self, msg_p, db=self._db)