]> git.notmuchmail.org Git - notmuch/blob - bindings/python-cffi/notmuch2/_tags.py
python/notmuch2: fix typo for ObjectDestroyedError
[notmuch] / bindings / python-cffi / notmuch2 / _tags.py
1 import collections.abc
2
3 import notmuch2._base as base
4 import notmuch2._capi as capi
5 import notmuch2._errors as errors
6
7
8 __all__ = ['ImmutableTagSet', 'MutableTagSet', 'TagsIter']
9
10
11 class ImmutableTagSet(base.NotmuchObject, collections.abc.Set):
12     """The tags associated with a message thread or whole database.
13
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.
17
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)
21     rather then O(1).
22
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.
27
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
30     the message.
31
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.
40     """
41
42     def __init__(self, parent, ptr_name, cffi_fn):
43         self._parent = parent
44         self._ptr = lambda: getattr(parent, ptr_name)
45         self._cffi_fn = cffi_fn
46
47     def __del__(self):
48         self._destroy()
49
50     @property
51     def alive(self):
52         return self._parent.alive
53
54     def _destroy(self):
55         pass
56
57     @classmethod
58     def _from_iterable(cls, it):
59         return set(it)
60
61     def __iter__(self):
62         """Return an iterator over the tags.
63
64         Tags are yielded as unicode strings, decoded using the
65         "ignore" error handler.
66
67         :raises NullPointerError: If the iterator can not be created.
68         """
69         return self.iter(encoding='utf-8', errors='ignore')
70
71     def iter(self, *, encoding=None, errors='strict'):
72         """Aternate iterator constructor controlling string decoding.
73
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.
79
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.
85
86         :raises NullPointerError: When things do not go as planned.
87         """
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)
96         return tags
97
98     def __len__(self):
99         return sum(1 for t in self)
100
101     def __contains__(self, tag):
102         if isinstance(tag, str):
103             tag = tag.encode()
104         for msg_tag in self.iter():
105             if tag == msg_tag:
106                 return True
107         else:
108             return False
109
110     def __eq__(self, other):
111         return tuple(sorted(self.iter())) == tuple(sorted(other.iter()))
112
113     def __hash__(self):
114         return hash(tuple(self.iter()))
115
116     def __repr__(self):
117         return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
118             name=self.__class__.__name__,
119             addr=id(self),
120             tags=', '.join(repr(t) for t in self))
121
122
123 class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
124     """The tags associated with a message.
125
126     This is a :class:`collections.abc.MutableSet` object which can be
127     used to manipulate the tags of a message.
128
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
132     O(1).
133
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.
140     """
141
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.
145     __hash__ = None
146
147     def add(self, tag):
148         """Add a tag to the message.
149
150         :param tag: The tag to add.
151         :type tag: str or bytes.  A str will be encoded using UTF-8.
152
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
158
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
163            read-only mode.
164         """
165         if isinstance(tag, str):
166             tag = tag.encode()
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)
172
173     def discard(self, tag):
174         """Remove a tag from the message.
175
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
183
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
188            read-only mode.
189         """
190         if isinstance(tag, str):
191             tag = tag.encode()
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)
197
198     def clear(self):
199         """Remove all tags from the message.
200
201         :raises ReadOnlyDatabaseError: If the database is opened in
202            read-only mode.
203         """
204         ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
205         if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
206             raise errors.NotmuchError(ret)
207
208     def from_maildir_flags(self):
209         """Update the tags based on the state in the message's maildir flags.
210
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:
214
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
222
223         For each flag that is not present, the opposite action
224         (add/remove) is performed for the corresponding tags.
225
226         Flags are identified as trailing components of the filename
227         after a sequence of ":2,".
228
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.)
233         """
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)
237
238     def to_maildir_flags(self):
239         """Update the message's maildir flags based on the notmuch tags.
240
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:
244
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
250
251         Any existing flags unmentioned in the list above will be
252         preserved in the renaming.
253
254         Also, if this filename is in a directory named "new", rename it to
255         be within the neighboring directory named "cur".
256
257         In case there are multiple files associated with the message
258         all filenames will get the same logic applied.
259         """
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)
263
264
265 class TagsIter(base.NotmuchObject, collections.abc.Iterator):
266     """Iterator over tags.
267
268     This is only an interator, not a container so calling
269     :meth:`__iter__` does not return a new, replenished iterator but
270     only itself.
271
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.
279
280     :raises ObjectDestroyedError: if used after destroyed.
281     """
282     _tags_p = base.MemoryPointer()
283
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
289
290     def __del__(self):
291         self._destroy()
292
293     @property
294     def alive(self):
295         if not self._parent.alive:
296             return False
297         try:
298             self._tags_p
299         except errors.ObjectDestroyedError:
300             return False
301         else:
302             return True
303
304     def _destroy(self):
305         if self.alive:
306             try:
307                 capi.lib.notmuch_tags_destroy(self._tags_p)
308             except errors.ObjectDestroyedError:
309                 pass
310         self._tags_p = None
311
312     def __iter__(self):
313         """Return the iterator itself.
314
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.
318         """
319         return self
320
321     def __next__(self):
322         if not capi.lib.notmuch_tags_valid(self._tags_p):
323             self._destroy()
324             raise StopIteration()
325         tag_p = capi.lib.notmuch_tags_get(self._tags_p)
326         tag = capi.ffi.string(tag_p)
327         if self._encoding:
328             tag = tag.decode(encoding=self._encoding, errors=self._errors)
329         capi.lib.notmuch_tags_move_to_next(self._tags_p)
330         return tag
331
332     def __repr__(self):
333         try:
334             self._tags_p
335         except errors.ObjectDestroyedError:
336             return '<TagsIter (exhausted)>'
337         else:
338             return '<TagsIter>'