3 import notmuch2._base as base
4 import notmuch2._capi as capi
5 import notmuch2._errors as errors
8 __all__ = ['ImmutableTagSet', 'MutableTagSet', 'TagsIter']
11 class ImmutableTagSet(base.NotmuchObject, collections.abc.Set):
12 """The tags associated with a message thread or whole database.
14 Both a thread as well as the database expose the union of all tags
15 in messages associated with them. This exposes these as a
16 :class:`collections.abc.Set` object.
18 Note that due to the underlying notmuch API the performance of the
19 implementation is not the same as you would expect from normal
20 sets. E.g. the :meth:`__contains__` and :meth:`__len__` are O(n)
23 Tags are internally stored as bytestrings but normally exposed as
24 unicode strings using the UTF-8 encoding and the *ignore* decoder
25 error handler. However the :meth:`iter` method can be used to
26 return tags as bytestrings or using a different error handler.
28 Note that when doing arithmetic operations on tags, this class
29 will return a plain normal set as it is no longer associated with
32 :param parent: the parent object
33 :param ptr_name: the name of the attribute on the parent which will
34 return the memory pointer. This allows this object to
35 access the pointer via the parent's descriptor and thus
36 trigger :class:`MemoryPointer`'s memory safety.
37 :param cffi_fn: the callable CFFI wrapper to retrieve the tags
38 iter. This can be one of notmuch_database_get_all_tags,
39 notmuch_thread_get_tags or notmuch_message_get_tags.
42 def __init__(self, parent, ptr_name, cffi_fn):
44 self._ptr = lambda: getattr(parent, ptr_name)
45 self._cffi_fn = cffi_fn
52 return self._parent.alive
58 def _from_iterable(cls, it):
62 """Return an iterator over the tags.
64 Tags are yielded as unicode strings, decoded using the
65 "ignore" error handler.
67 :raises NullPointerError: If the iterator can not be created.
69 return self.iter(encoding='utf-8', errors='ignore')
71 def iter(self, *, encoding=None, errors='strict'):
72 """Aternate iterator constructor controlling string decoding.
74 Tags are stored as bytes in the notmuch database, in Python
75 it's easier to work with unicode strings and thus is what the
76 normal iterator returns. However this method allows you to
77 specify how you would like to get the tags, defaulting to the
78 bytestring representation instead of unicode strings.
80 :param encoding: Which codec to use. The default *None* does not
81 decode at all and will return the unmodified bytes.
82 Otherwise this is passed on to :func:`str.decode`.
83 :param errors: If using a codec, this is the error handler.
84 See :func:`str.decode` to which this is passed on.
86 :raises NullPointerError: When things do not go as planned.
88 # self._cffi_fn should point either to
89 # notmuch_database_get_all_tags, notmuch_thread_get_tags or
90 # notmuch_message_get_tags. nothmuch.h suggests these never
91 # fail, let's handle NULL anyway.
92 tags_p = self._cffi_fn(self._ptr())
93 if tags_p == capi.ffi.NULL:
94 raise errors.NullPointerError()
95 tags = TagsIter(self, tags_p, encoding=encoding, errors=errors)
99 return sum(1 for t in self)
101 def __contains__(self, tag):
102 if isinstance(tag, str):
104 for msg_tag in self.iter():
110 def __eq__(self, other):
111 return tuple(sorted(self.iter())) == tuple(sorted(other.iter()))
114 return hash(tuple(self.iter()))
117 return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
118 name=self.__class__.__name__,
120 tags=', '.join(repr(t) for t in self))
123 class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
124 """The tags associated with a message.
126 This is a :class:`collections.abc.MutableSet` object which can be
127 used to manipulate the tags of a message.
129 Note that due to the underlying notmuch API the performance of the
130 implementation is not the same as you would expect from normal
131 sets. E.g. the ``in`` operator and variants are O(n) rather then
134 Tags are bytestrings and calling ``iter()`` will return an
135 iterator yielding bytestrings. However the :meth:`iter` method
136 can be used to return tags as unicode strings, while all other
137 operations accept either byestrings or unicode strings. In case
138 unicode strings are used they will be encoded using utf-8 before
139 being passed to notmuch.
142 # Since we subclass ImmutableTagSet we inherit a __hash__. But we
143 # are mutable, setting it to None will make the Python machinary
144 # recognise us as unhashable.
148 """Add a tag to the message.
150 :param tag: The tag to add.
151 :type tag: str or bytes. A str will be encoded using UTF-8.
153 :param sync_flags: Whether to sync the maildir flags with the
154 new set of tags. Leaving this as *None* respects the
155 configuration set in the database, while *True* will always
156 sync and *False* will never sync.
157 :param sync_flags: NoneType or bool
159 :raises TypeError: If the tag is not a valid type.
160 :raises TagTooLongError: If the added tag exceeds the maximum
161 lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
162 :raises ReadOnlyDatabaseError: If the database is opened in
165 if isinstance(tag, str):
167 if not isinstance(tag, bytes):
168 raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
169 ret = capi.lib.notmuch_message_add_tag(self._ptr(), tag)
170 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
171 raise errors.NotmuchError(ret)
173 def discard(self, tag):
174 """Remove a tag from the message.
176 :param tag: The tag to remove.
177 :type tag: str of bytes. A str will be encoded using UTF-8.
178 :param sync_flags: Whether to sync the maildir flags with the
179 new set of tags. Leaving this as *None* respects the
180 configuration set in the database, while *True* will always
181 sync and *False* will never sync.
182 :param sync_flags: NoneType or bool
184 :raises TypeError: If the tag is not a valid type.
185 :raises TagTooLongError: If the tag exceeds the maximum
186 lenght, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
187 :raises ReadOnlyDatabaseError: If the database is opened in
190 if isinstance(tag, str):
192 if not isinstance(tag, bytes):
193 raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
194 ret = capi.lib.notmuch_message_remove_tag(self._ptr(), tag)
195 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
196 raise errors.NotmuchError(ret)
199 """Remove all tags from the message.
201 :raises ReadOnlyDatabaseError: If the database is opened in
204 ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
205 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
206 raise errors.NotmuchError(ret)
208 def from_maildir_flags(self):
209 """Update the tags based on the state in the message's maildir flags.
211 This function examines the filenames of 'message' for maildir
212 flags, and adds or removes tags on 'message' as follows when
213 these flags are present:
215 Flag Action if present
216 ---- -----------------
217 'D' Adds the "draft" tag to the message
218 'F' Adds the "flagged" tag to the message
219 'P' Adds the "passed" tag to the message
220 'R' Adds the "replied" tag to the message
221 'S' Removes the "unread" tag from the message
223 For each flag that is not present, the opposite action
224 (add/remove) is performed for the corresponding tags.
226 Flags are identified as trailing components of the filename
227 after a sequence of ":2,".
229 If there are multiple filenames associated with this message,
230 the flag is considered present if it appears in one or more
231 filenames. (That is, the flags from the multiple filenames are
232 combined with the logical OR operator.)
234 ret = capi.lib.notmuch_message_maildir_flags_to_tags(self._ptr())
235 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
236 raise errors.NotmuchError(ret)
238 def to_maildir_flags(self):
239 """Update the message's maildir flags based on the notmuch tags.
241 If the message's filename is in a maildir directory, that is a
242 directory named ``new`` or ``cur``, and has a valid maildir
243 filename then the flags will be added as such:
245 'D' if the message has the "draft" tag
246 'F' if the message has the "flagged" tag
247 'P' if the message has the "passed" tag
248 'R' if the message has the "replied" tag
249 'S' if the message does not have the "unread" tag
251 Any existing flags unmentioned in the list above will be
252 preserved in the renaming.
254 Also, if this filename is in a directory named "new", rename it to
255 be within the neighboring directory named "cur".
257 In case there are multiple files associated with the message
258 all filenames will get the same logic applied.
260 ret = capi.lib.notmuch_message_tags_to_maildir_flags(self._ptr())
261 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
262 raise errors.NotmuchError(ret)
265 class TagsIter(base.NotmuchObject, collections.abc.Iterator):
266 """Iterator over tags.
268 This is only an interator, not a container so calling
269 :meth:`__iter__` does not return a new, replenished iterator but
272 :param parent: The parent object to keep alive.
273 :param tags_p: The CFFI pointer to the C-level tags iterator.
274 :param encoding: Which codec to use. The default *None* does not
275 decode at all and will return the unmodified bytes.
276 Otherwise this is passed on to :func:`str.decode`.
277 :param errors: If using a codec, this is the error handler.
278 See :func:`str.decode` to which this is passed on.
280 :raises ObjectDestoryedError: if used after destroyed.
282 _tags_p = base.MemoryPointer()
284 def __init__(self, parent, tags_p, *, encoding=None, errors='strict'):
285 self._parent = parent
286 self._tags_p = tags_p
287 self._encoding = encoding
288 self._errors = errors
295 if not self._parent.alive:
299 except errors.ObjectDestroyedError:
307 capi.lib.notmuch_tags_destroy(self._tags_p)
308 except errors.ObjectDestroyedError:
313 """Return the iterator itself.
315 Note that as this is an iterator and not a container this will
316 not return a new iterator. Thus any elements already consumed
317 will not be yielded by the :meth:`__next__` method anymore.
322 if not capi.lib.notmuch_tags_valid(self._tags_p):
324 raise StopIteration()
325 tag_p = capi.lib.notmuch_tags_get(self._tags_p)
326 tag = capi.ffi.string(tag_p)
328 tag = tag.decode(encoding=self._encoding, errors=self._errors)
329 capi.lib.notmuch_tags_move_to_next(self._tags_p)
335 except errors.ObjectDestroyedError:
336 return '<TagsIter (exhausted)>'