+++ /dev/null
-import collections.abc
-
-import notdb._base as base
-import notdb._capi as capi
-import notdb._errors as errors
-
-
-__all__ = ['ImmutableTagSet', 'MutableTagSet', 'TagsIter']
-
-
-class ImmutableTagSet(base.NotmuchObject, collections.abc.Set):
- """The tags associated with a message thread or whole database.
-
- Both a thread as well as the database expose the union of all tags
- in messages associated with them. This exposes these as a
- :class:`collections.abc.Set` object.
-
- Note that due to the underlying notmuch API the performance of the
- implementation is not the same as you would expect from normal
- sets. E.g. the :meth:`__contains__` and :meth:`__len__` are O(n)
- rather then O(1).
-
- Tags are internally stored as bytestrings but normally exposed as
- unicode strings using the UTF-8 encoding and the *ignore* decoder
- error handler. However the :meth:`iter` method can be used to
- return tags as bytestrings or using a different error handler.
-
- Note that when doing arithmetic operations on tags, this class
- will return a plain normal set as it is no longer associated with
- the message.
-
- :param parent: the parent object
- :param ptr_name: the name of the attribute on the parent which will
- return the memory pointer. This allows this object to
- access the pointer via the parent's descriptor and thus
- trigger :class:`MemoryPointer`'s memory safety.
- :param cffi_fn: the callable CFFI wrapper to retrieve the tags
- iter. This can be one of notmuch_database_get_all_tags,
- notmuch_thread_get_tags or notmuch_message_get_tags.
- """
-
- def __init__(self, parent, ptr_name, cffi_fn):
- self._parent = parent
- self._ptr = lambda: getattr(parent, ptr_name)
- self._cffi_fn = cffi_fn
-
- def __del__(self):
- self._destroy()
-
- @property
- def alive(self):
- return self._parent.alive
-
- def _destroy(self):
- pass
-
- @classmethod
- def _from_iterable(cls, it):
- return set(it)
-
- def __iter__(self):
- """Return an iterator over the tags.
-
- Tags are yielded as unicode strings, decoded using the
- "ignore" error handler.
-
- :raises NullPointerError: If the iterator can not be created.
- """
- return self.iter(encoding='utf-8', errors='ignore')
-
- def iter(self, *, encoding=None, errors='strict'):
- """Aternate iterator constructor controlling string decoding.
-
- Tags are stored as bytes in the notmuch database, in Python
- it's easier to work with unicode strings and thus is what the
- normal iterator returns. However this method allows you to
- specify how you would like to get the tags, defaulting to the
- bytestring representation instead of unicode strings.
-
- :param encoding: Which codec to use. The default *None* does not
- decode at all and will return the unmodified bytes.
- Otherwise this is passed on to :func:`str.decode`.
- :param errors: If using a codec, this is the error handler.
- See :func:`str.decode` to which this is passed on.
-
- :raises NullPointerError: When things do not go as planned.
- """
- # self._cffi_fn should point either to
- # notmuch_database_get_all_tags, notmuch_thread_get_tags or
- # notmuch_message_get_tags. nothmuch.h suggests these never
- # fail, let's handle NULL anyway.
- tags_p = self._cffi_fn(self._ptr())
- if tags_p == capi.ffi.NULL:
- raise errors.NullPointerError()
- tags = TagsIter(self, tags_p, encoding=encoding, errors=errors)
- return tags
-
- def __len__(self):
- return sum(1 for t in self)
-
- def __contains__(self, tag):
- if isinstance(tag, str):
- tag = tag.encode()
- for msg_tag in self.iter():
- if tag == msg_tag:
- return True
- else:
- return False
-
- def __eq__(self, other):
- return tuple(sorted(self.iter())) == tuple(sorted(other.iter()))
-
- def __hash__(self):
- return hash(tuple(self.iter()))
-
- def __repr__(self):
- return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
- name=self.__class__.__name__,
- addr=id(self),
- tags=', '.join(repr(t) for t in self))
-
-
-class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
- """The tags associated with a message.
-
- This is a :class:`collections.abc.MutableSet` object which can be
- used to manipulate the tags of a message.
-
- Note that due to the underlying notmuch API the performance of the
- implementation is not the same as you would expect from normal
- sets. E.g. the ``in`` operator and variants are O(n) rather then
- O(1).
-
- Tags are bytestrings and calling ``iter()`` will return an
- iterator yielding bytestrings. However the :meth:`iter` method
- can be used to return tags as unicode strings, while all other
- operations accept either byestrings or unicode strings. In case
- unicode strings are used they will be encoded using utf-8 before
- being passed to notmuch.
- """
-
- # Since we subclass ImmutableTagSet we inherit a __hash__. But we
- # are mutable, setting it to None will make the Python machinary
- # recognise us as unhashable.
- __hash__ = None
-
- def add(self, tag):
- """Add a tag to the message.
-
- :param tag: The tag to add.
- :type tag: str or bytes. A str will be encoded using UTF-8.
-
- :param sync_flags: Whether to sync the maildir flags with the
- new set of tags. Leaving this as *None* respects the
- configuration set in the database, while *True* will always
- sync and *False* will never sync.
- :param sync_flags: NoneType or bool
-
- :raises TypeError: If the tag is not a valid type.
- :raises TagTooLongError: If the added tag exceeds the maximum
- lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
- :raises ReadOnlyDatabaseError: If the database is opened in
- read-only mode.
- """
- if isinstance(tag, str):
- tag = tag.encode()
- if not isinstance(tag, bytes):
- raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
- ret = capi.lib.notmuch_message_add_tag(self._ptr(), tag)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def discard(self, tag):
- """Remove a tag from the message.
-
- :param tag: The tag to remove.
- :type tag: str of bytes. A str will be encoded using UTF-8.
- :param sync_flags: Whether to sync the maildir flags with the
- new set of tags. Leaving this as *None* respects the
- configuration set in the database, while *True* will always
- sync and *False* will never sync.
- :param sync_flags: NoneType or bool
-
- :raises TypeError: If the tag is not a valid type.
- :raises TagTooLongError: If the tag exceeds the maximum
- lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
- :raises ReadOnlyDatabaseError: If the database is opened in
- read-only mode.
- """
- if isinstance(tag, str):
- tag = tag.encode()
- if not isinstance(tag, bytes):
- raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
- ret = capi.lib.notmuch_message_remove_tag(self._ptr(), tag)
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def clear(self):
- """Remove all tags from the message.
-
- :raises ReadOnlyDatabaseError: If the database is opened in
- read-only mode.
- """
- ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def from_maildir_flags(self):
- """Update the tags based on the state in the message's maildir flags.
-
- This function examines the filenames of 'message' for maildir
- flags, and adds or removes tags on 'message' as follows when
- these flags are present:
-
- Flag Action if present
- ---- -----------------
- 'D' Adds the "draft" tag to the message
- 'F' Adds the "flagged" tag to the message
- 'P' Adds the "passed" tag to the message
- 'R' Adds the "replied" tag to the message
- 'S' Removes the "unread" tag from the message
-
- For each flag that is not present, the opposite action
- (add/remove) is performed for the corresponding tags.
-
- Flags are identified as trailing components of the filename
- after a sequence of ":2,".
-
- If there are multiple filenames associated with this message,
- the flag is considered present if it appears in one or more
- filenames. (That is, the flags from the multiple filenames are
- combined with the logical OR operator.)
- """
- ret = capi.lib.notmuch_message_maildir_flags_to_tags(self._ptr())
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
- def to_maildir_flags(self):
- """Update the message's maildir flags based on the notmuch tags.
-
- If the message's filename is in a maildir directory, that is a
- directory named ``new`` or ``cur``, and has a valid maildir
- filename then the flags will be added as such:
-
- 'D' if the message has the "draft" tag
- 'F' if the message has the "flagged" tag
- 'P' if the message has the "passed" tag
- 'R' if the message has the "replied" tag
- 'S' if the message does not have the "unread" tag
-
- Any existing flags unmentioned in the list above will be
- preserved in the renaming.
-
- Also, if this filename is in a directory named "new", rename it to
- be within the neighboring directory named "cur".
-
- In case there are multiple files associated with the message
- all filenames will get the same logic applied.
- """
- ret = capi.lib.notmuch_message_tags_to_maildir_flags(self._ptr())
- if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
- raise errors.NotmuchError(ret)
-
-
-class TagsIter(base.NotmuchObject, collections.abc.Iterator):
- """Iterator over tags.
-
- This is only an interator, not a container so calling
- :meth:`__iter__` does not return a new, replenished iterator but
- only itself.
-
- :param parent: The parent object to keep alive.
- :param tags_p: The CFFI pointer to the C-level tags iterator.
- :param encoding: Which codec to use. The default *None* does not
- decode at all and will return the unmodified bytes.
- Otherwise this is passed on to :func:`str.decode`.
- :param errors: If using a codec, this is the error handler.
- See :func:`str.decode` to which this is passed on.
-
- :raises ObjectDestoryedError: if used after destroyed.
- """
- _tags_p = base.MemoryPointer()
-
- def __init__(self, parent, tags_p, *, encoding=None, errors='strict'):
- self._parent = parent
- self._tags_p = tags_p
- self._encoding = encoding
- self._errors = errors
-
- def __del__(self):
- self._destroy()
-
- @property
- def alive(self):
- if not self._parent.alive:
- return False
- try:
- self._tags_p
- except errors.ObjectDestroyedError:
- return False
- else:
- return True
-
- def _destroy(self):
- if self.alive:
- try:
- capi.lib.notmuch_tags_destroy(self._tags_p)
- except errors.ObjectDestroyedError:
- pass
- self._tags_p = None
-
- def __iter__(self):
- """Return the iterator itself.
-
- Note that as this is an iterator and not a container this will
- not return a new iterator. Thus any elements already consumed
- will not be yielded by the :meth:`__next__` method anymore.
- """
- return self
-
- def __next__(self):
- if not capi.lib.notmuch_tags_valid(self._tags_p):
- self._destroy()
- raise StopIteration()
- tag_p = capi.lib.notmuch_tags_get(self._tags_p)
- tag = capi.ffi.string(tag_p)
- if self._encoding:
- tag = tag.decode(encoding=self._encoding, errors=self._errors)
- capi.lib.notmuch_tags_move_to_next(self._tags_p)
- return tag
-
- def __repr__(self):
- try:
- self._tags_p
- except errors.ObjectDestroyedError:
- return '<TagsIter (exhausted)>'
- else:
- return '<TagsIter>'