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()))
113 def issubset(self, other):
116 def issuperset(self, other):
119 def union(self, other):
122 def intersection(self, other):
125 def difference(self, other):
128 def symmetric_difference(self, other):
135 return hash(tuple(self.iter()))
138 return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
139 name=self.__class__.__name__,
141 tags=', '.join(repr(t) for t in self))
144 class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
145 """The tags associated with a message.
147 This is a :class:`collections.abc.MutableSet` object which can be
148 used to manipulate the tags of a message.
150 Note that due to the underlying notmuch API the performance of the
151 implementation is not the same as you would expect from normal
152 sets. E.g. the ``in`` operator and variants are O(n) rather then
155 Tags are bytestrings and calling ``iter()`` will return an
156 iterator yielding bytestrings. However the :meth:`iter` method
157 can be used to return tags as unicode strings, while all other
158 operations accept either byestrings or unicode strings. In case
159 unicode strings are used they will be encoded using utf-8 before
160 being passed to notmuch.
163 # Since we subclass ImmutableTagSet we inherit a __hash__. But we
164 # are mutable, setting it to None will make the Python machinery
165 # recognise us as unhashable.
169 """Add a tag to the message.
171 :param tag: The tag to add.
172 :type tag: str or bytes. A str will be encoded using UTF-8.
174 :param sync_flags: Whether to sync the maildir flags with the
175 new set of tags. Leaving this as *None* respects the
176 configuration set in the database, while *True* will always
177 sync and *False* will never sync.
178 :param sync_flags: NoneType or bool
180 :raises TypeError: If the tag is not a valid type.
181 :raises TagTooLongError: If the added tag exceeds the maximum
182 length, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
183 :raises ReadOnlyDatabaseError: If the database is opened in
186 if isinstance(tag, str):
188 if not isinstance(tag, bytes):
189 raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
190 ret = capi.lib.notmuch_message_add_tag(self._ptr(), tag)
191 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
192 raise errors.NotmuchError(ret)
194 def discard(self, tag):
195 """Remove a tag from the message.
197 :param tag: The tag to remove.
198 :type tag: str of bytes. A str will be encoded using UTF-8.
199 :param sync_flags: Whether to sync the maildir flags with the
200 new set of tags. Leaving this as *None* respects the
201 configuration set in the database, while *True* will always
202 sync and *False* will never sync.
203 :param sync_flags: NoneType or bool
205 :raises TypeError: If the tag is not a valid type.
206 :raises TagTooLongError: If the tag exceeds the maximum
207 length, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
208 :raises ReadOnlyDatabaseError: If the database is opened in
211 if isinstance(tag, str):
213 if not isinstance(tag, bytes):
214 raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
215 ret = capi.lib.notmuch_message_remove_tag(self._ptr(), tag)
216 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
217 raise errors.NotmuchError(ret)
220 """Remove all tags from the message.
222 :raises ReadOnlyDatabaseError: If the database is opened in
225 ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
226 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
227 raise errors.NotmuchError(ret)
229 def from_maildir_flags(self):
230 """Update the tags based on the state in the message's maildir flags.
232 This function examines the filenames of 'message' for maildir
233 flags, and adds or removes tags on 'message' as follows when
234 these flags are present:
236 Flag Action if present
237 ---- -----------------
238 'D' Adds the "draft" tag to the message
239 'F' Adds the "flagged" tag to the message
240 'P' Adds the "passed" tag to the message
241 'R' Adds the "replied" tag to the message
242 'S' Removes the "unread" tag from the message
244 For each flag that is not present, the opposite action
245 (add/remove) is performed for the corresponding tags.
247 Flags are identified as trailing components of the filename
248 after a sequence of ":2,".
250 If there are multiple filenames associated with this message,
251 the flag is considered present if it appears in one or more
252 filenames. (That is, the flags from the multiple filenames are
253 combined with the logical OR operator.)
255 ret = capi.lib.notmuch_message_maildir_flags_to_tags(self._ptr())
256 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
257 raise errors.NotmuchError(ret)
259 def to_maildir_flags(self):
260 """Update the message's maildir flags based on the notmuch tags.
262 If the message's filename is in a maildir directory, that is a
263 directory named ``new`` or ``cur``, and has a valid maildir
264 filename then the flags will be added as such:
266 'D' if the message has the "draft" tag
267 'F' if the message has the "flagged" tag
268 'P' if the message has the "passed" tag
269 'R' if the message has the "replied" tag
270 'S' if the message does not have the "unread" tag
272 Any existing flags unmentioned in the list above will be
273 preserved in the renaming.
275 Also, if this filename is in a directory named "new", rename it to
276 be within the neighboring directory named "cur".
278 In case there are multiple files associated with the message
279 all filenames will get the same logic applied.
281 ret = capi.lib.notmuch_message_tags_to_maildir_flags(self._ptr())
282 if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
283 raise errors.NotmuchError(ret)
286 class TagsIter(base.NotmuchObject, collections.abc.Iterator):
287 """Iterator over tags.
289 This is only an iterator, not a container so calling
290 :meth:`__iter__` does not return a new, replenished iterator but
293 :param parent: The parent object to keep alive.
294 :param tags_p: The CFFI pointer to the C-level tags iterator.
295 :param encoding: Which codec to use. The default *None* does not
296 decode at all and will return the unmodified bytes.
297 Otherwise this is passed on to :func:`str.decode`.
298 :param errors: If using a codec, this is the error handler.
299 See :func:`str.decode` to which this is passed on.
301 :raises ObjectDestroyedError: if used after destroyed.
303 _tags_p = base.MemoryPointer()
305 def __init__(self, parent, tags_p, *, encoding=None, errors='strict'):
306 self._parent = parent
307 self._tags_p = tags_p
308 self._encoding = encoding
309 self._errors = errors
316 if not self._parent.alive:
320 except errors.ObjectDestroyedError:
328 capi.lib.notmuch_tags_destroy(self._tags_p)
329 except errors.ObjectDestroyedError:
334 """Return the iterator itself.
336 Note that as this is an iterator and not a container this will
337 not return a new iterator. Thus any elements already consumed
338 will not be yielded by the :meth:`__next__` method anymore.
343 if not capi.lib.notmuch_tags_valid(self._tags_p):
345 raise StopIteration()
346 tag_p = capi.lib.notmuch_tags_get(self._tags_p)
347 tag = capi.ffi.string(tag_p)
349 tag = tag.decode(encoding=self._encoding, errors=self._errors)
350 capi.lib.notmuch_tags_move_to_next(self._tags_p)
356 except errors.ObjectDestroyedError:
357 return '<TagsIter (exhausted)>'