python: harmonize the sphinx keyword for exceptions
[notmuch] / bindings / python / notmuch / thread.py
1 """
2 This file is part of notmuch.
3
4 Notmuch is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
8
9 Notmuch is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
18 """
19
20 from ctypes import c_char_p, c_long, c_int
21 from notmuch.globals import (
22     nmlib,
23     Python3StringMixIn,
24     NullPointerError,
25     NotInitializedError,
26     NotmuchThreadP,
27     NotmuchThreadsP,
28     NotmuchMessagesP,
29     NotmuchTagsP,
30 )
31 from notmuch.message import Messages
32 from notmuch.tag import Tags
33 from datetime import date
34
35
36 class Threads(Python3StringMixIn):
37     """Represents a list of notmuch threads
38
39     This object provides an iterator over a list of notmuch threads
40     (Technically, it provides a wrapper for the underlying
41     *notmuch_threads_t* structure). Do note that the underlying
42     library only provides a one-time iterator (it cannot reset the
43     iterator to the start). Thus iterating over the function will
44     "exhaust" the list of threads, and a subsequent iteration attempt
45     will raise a :exc:`NotInitializedError`. Also
46     note, that any function that uses iteration will also
47     exhaust the messages. So both::
48
49       for thread in threads: print thread
50
51     as well as::
52
53        number_of_msgs = len(threads)
54
55     will "exhaust" the threads. If you need to re-iterate over a list of
56     messages you will need to retrieve a new :class:`Threads` object.
57
58     Things are not as bad as it seems though, you can store and reuse
59     the single Thread objects as often as you want as long as you
60     keep the parent Threads object around. (Recall that due to
61     hierarchical memory allocation, all derived Threads objects will
62     be invalid when we delete the parent Threads() object, even if it
63     was already "exhausted".) So this works::
64
65       db   = Database()
66       threads = Query(db,'').search_threads() #get a Threads() object
67       threadlist = []
68       for thread in threads:
69          threadlist.append(thread)
70
71       # threads is "exhausted" now and even len(threads) will raise an
72       # exception.
73       # However it will be kept around until all retrieved Thread() objects are
74       # also deleted. If you did e.g. an explicit del(threads) here, the
75       # following lines would fail.
76
77       # You can reiterate over *threadlist* however as often as you want.
78       # It is simply a list with Thread objects.
79
80       print (threadlist[0].get_thread_id())
81       print (threadlist[1].get_thread_id())
82       print (threadlist[0].get_total_messages())
83     """
84
85     #notmuch_threads_get
86     _get = nmlib.notmuch_threads_get
87     _get.argtypes = [NotmuchThreadsP]
88     _get.restype = NotmuchThreadP
89
90     def __init__(self, threads_p, parent=None):
91         """
92         :param threads_p:  A pointer to an underlying *notmuch_threads_t*
93              structure. These are not publically exposed, so a user
94              will almost never instantiate a :class:`Threads` object
95              herself. They are usually handed back as a result,
96              e.g. in :meth:`Query.search_threads`.  *threads_p* must be
97              valid, we will raise an :exc:`NullPointerError` if it is
98              `None`.
99         :type threads_p: :class:`ctypes.c_void_p`
100         :param parent: The parent object
101              (ie :class:`Query`) these tags are derived from. It saves
102              a reference to it, so we can automatically delete the db
103              object once all derived objects are dead.
104         :TODO: Make the iterator work more than once and cache the tags in
105                the Python object.(?)
106         """
107         if not threads_p:
108             raise NullPointerError()
109
110         self._threads = threads_p
111         #store parent, so we keep them alive as long as self  is alive
112         self._parent = parent
113
114     def __iter__(self):
115         """ Make Threads an iterator """
116         return self
117
118     _valid = nmlib.notmuch_threads_valid
119     _valid.argtypes = [NotmuchThreadsP]
120     _valid.restype = bool
121
122     _move_to_next = nmlib.notmuch_threads_move_to_next
123     _move_to_next.argtypes = [NotmuchThreadsP]
124     _move_to_next.restype = None
125
126     def __next__(self):
127         if not self._threads:
128             raise NotInitializedError()
129
130         if not self._valid(self._threads):
131             self._threads = None
132             raise StopIteration
133
134         thread = Thread(Threads._get(self._threads), self)
135         self._move_to_next(self._threads)
136         return thread
137     next = __next__ # python2.x iterator protocol compatibility
138
139     def __len__(self):
140         """len(:class:`Threads`) returns the number of contained Threads
141
142         .. note:: As this iterates over the threads, we will not be able to
143                iterate over them again! So this will fail::
144
145                  #THIS FAILS
146                  threads = Database().create_query('').search_threads()
147                  if len(threads) > 0:              #this 'exhausts' threads
148                      # next line raises :exc:`NotInitializedError`!!!
149                      for thread in threads: print thread
150         """
151         if not self._threads:
152             raise NotInitializedError()
153
154         i = 0
155         # returns 'bool'. On out-of-memory it returns None
156         while self._valid(self._threads):
157             self._move_to_next(self._threads)
158             i += 1
159         # reset self._threads to mark as "exhausted"
160         self._threads = None
161         return i
162
163     def __nonzero__(self):
164         """Check if :class:`Threads` contains at least one more valid thread
165
166         The existence of this function makes 'if Threads: foo' work, as
167         that will implicitely call len() exhausting the iterator if
168         __nonzero__ does not exist. This function makes `bool(Threads())`
169         work repeatedly.
170
171         :return: True if there is at least one more thread in the
172            Iterator, False if not. None on a "Out-of-memory" error.
173         """
174         return self._threads is not None and \
175             self._valid(self._threads) > 0
176
177     _destroy = nmlib.notmuch_threads_destroy
178     _destroy.argtypes = [NotmuchThreadsP]
179     _destroy.argtypes = None
180
181     def __del__(self):
182         """Close and free the notmuch Threads"""
183         if self._threads is not None:
184             self._destroy(self._threads)
185
186
187 class Thread(object):
188     """Represents a single message thread."""
189
190     """notmuch_thread_get_thread_id"""
191     _get_thread_id = nmlib.notmuch_thread_get_thread_id
192     _get_thread_id.argtypes = [NotmuchThreadP]
193     _get_thread_id.restype = c_char_p
194
195     """notmuch_thread_get_authors"""
196     _get_authors = nmlib.notmuch_thread_get_authors
197     _get_authors.argtypes = [NotmuchThreadP]
198     _get_authors.restype = c_char_p
199
200     """notmuch_thread_get_subject"""
201     _get_subject = nmlib.notmuch_thread_get_subject
202     _get_subject.argtypes = [NotmuchThreadP]
203     _get_subject.restype = c_char_p
204
205     """notmuch_thread_get_toplevel_messages"""
206     _get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages
207     _get_toplevel_messages.argtypes = [NotmuchThreadP]
208     _get_toplevel_messages.restype = NotmuchMessagesP
209
210     _get_newest_date = nmlib.notmuch_thread_get_newest_date
211     _get_newest_date.argtypes = [NotmuchThreadP]
212     _get_newest_date.restype = c_long
213
214     _get_oldest_date = nmlib.notmuch_thread_get_oldest_date
215     _get_oldest_date.argtypes = [NotmuchThreadP]
216     _get_oldest_date.restype = c_long
217
218     """notmuch_thread_get_tags"""
219     _get_tags = nmlib.notmuch_thread_get_tags
220     _get_tags.argtypes = [NotmuchThreadP]
221     _get_tags.restype = NotmuchTagsP
222
223     def __init__(self, thread_p, parent=None):
224         """
225         :param thread_p: A pointer to an internal notmuch_thread_t
226             Structure.  These are not publically exposed, so a user
227             will almost never instantiate a :class:`Thread` object
228             herself. They are usually handed back as a result,
229             e.g. when iterating through :class:`Threads`. *thread_p*
230             must be valid, we will raise an :exc:`NullPointerError`
231             if it is `None`.
232
233         :param parent: A 'parent' object is passed which this message is
234               derived from. We save a reference to it, so we can
235               automatically delete the parent object once all derived
236               objects are dead.
237         """
238         if not thread_p:
239             raise NullPointerError()
240         self._thread = thread_p
241         #keep reference to parent, so we keep it alive
242         self._parent = parent
243
244     def get_thread_id(self):
245         """Get the thread ID of 'thread'
246
247         The returned string belongs to 'thread' and will only be valid
248         for as long as the thread is valid.
249
250         :returns: String with a message ID
251         :raises: :exc:`NotInitializedError` if the thread
252                     is not initialized.
253         """
254         if not self._thread:
255             raise NotInitializedError()
256         return Thread._get_thread_id(self._thread).decode('utf-8', 'ignore')
257
258     _get_total_messages = nmlib.notmuch_thread_get_total_messages
259     _get_total_messages.argtypes = [NotmuchThreadP]
260     _get_total_messages.restype = c_int
261
262     def get_total_messages(self):
263         """Get the total number of messages in 'thread'
264
265         :returns: The number of all messages in the database
266                   belonging to this thread. Contrast with
267                   :meth:`get_matched_messages`.
268         :raises: :exc:`NotInitializedError` if the thread
269                     is not initialized.
270         """
271         if not self._thread:
272             raise NotInitializedError()
273         return self._get_total_messages(self._thread)
274
275     def get_toplevel_messages(self):
276         """Returns a :class:`Messages` iterator for the top-level messages in
277            'thread'
278
279            This iterator will not necessarily iterate over all of the messages
280            in the thread. It will only iterate over the messages in the thread
281            which are not replies to other messages in the thread.
282
283            To iterate over all messages in the thread, the caller will need to
284            iterate over the result of :meth:`Message.get_replies` for each
285            top-level message (and do that recursively for the resulting
286            messages, etc.).
287
288         :returns: :class:`Messages`
289         :raises: :exc:`NotInitializedError` if query is not initialized
290         :raises: :exc:`NullPointerError` if search_messages failed
291         """
292         if not self._thread:
293             raise NotInitializedError()
294
295         msgs_p = Thread._get_toplevel_messages(self._thread)
296
297         if not msgs_p:
298             raise NullPointerError()
299
300         return Messages(msgs_p, self)
301
302     _get_matched_messages = nmlib.notmuch_thread_get_matched_messages
303     _get_matched_messages.argtypes = [NotmuchThreadP]
304     _get_matched_messages.restype = c_int
305
306     def get_matched_messages(self):
307         """Returns the number of messages in 'thread' that matched the query
308
309         :returns: The number of all messages belonging to this thread that
310                   matched the :class:`Query`from which this thread was created.
311                   Contrast with :meth:`get_total_messages`.
312         :raises: :exc:`NotInitializedError` if the thread
313                     is not initialized.
314         """
315         if not self._thread:
316             raise NotInitializedError()
317         return self._get_matched_messages(self._thread)
318
319     def get_authors(self):
320         """Returns the authors of 'thread'
321
322         The returned string is a comma-separated list of the names of the
323         authors of mail messages in the query results that belong to this
324         thread.
325
326         The returned string belongs to 'thread' and will only be valid for
327         as long as this Thread() is not deleted.
328         """
329         if not self._thread:
330             raise NotInitializedError()
331         authors = Thread._get_authors(self._thread)
332         if not authors:
333             return None
334         return authors.decode('UTF-8', 'ignore')
335
336     def get_subject(self):
337         """Returns the Subject of 'thread'
338
339         The returned string belongs to 'thread' and will only be valid for
340         as long as this Thread() is not deleted.
341         """
342         if not self._thread:
343             raise NotInitializedError()
344         subject = Thread._get_subject(self._thread)
345         if not subject:
346             return None
347         return subject.decode('UTF-8', 'ignore')
348
349     def get_newest_date(self):
350         """Returns time_t of the newest message date
351
352         :returns: A time_t timestamp.
353         :rtype: c_unit64
354         :raises: :exc:`NotInitializedError` if the message
355                     is not initialized.
356         """
357         if not self._thread:
358             raise NotInitializedError()
359         return Thread._get_newest_date(self._thread)
360
361     def get_oldest_date(self):
362         """Returns time_t of the oldest message date
363
364         :returns: A time_t timestamp.
365         :rtype: c_unit64
366         :raises: :exc:`NotInitializedError` if the message
367                     is not initialized.
368         """
369         if not self._thread:
370             raise NotInitializedError()
371         return Thread._get_oldest_date(self._thread)
372
373     def get_tags(self):
374         """ Returns the message tags
375
376         In the Notmuch database, tags are stored on individual
377         messages, not on threads. So the tags returned here will be all
378         tags of the messages which matched the search and which belong to
379         this thread.
380
381         The :class:`Tags` object is owned by the thread and as such, will only
382         be valid for as long as this :class:`Thread` is valid (e.g. until the
383         query from which it derived is explicitely deleted).
384
385         :returns: A :class:`Tags` iterator.
386         :raises: :exc:`NotInitializedError` if query is not initialized
387         :raises: :exc:`NullPointerError` if search_messages failed
388         """
389         if not self._thread:
390             raise NotInitializedError()
391
392         tags_p = Thread._get_tags(self._thread)
393         if tags_p == None:
394             raise NullPointerError()
395         return Tags(tags_p, self)
396
397     def __unicode__(self):
398         frm = "thread:%s %12s [%d/%d] %s; %s (%s)"
399
400         return frm % (self.get_thread_id(),
401                       date.fromtimestamp(self.get_newest_date()),
402                       self.get_matched_messages(),
403                       self.get_total_messages(),
404                       self.get_authors(),
405                       self.get_subject(),
406                       self.get_tags(),
407                      )
408
409     _destroy = nmlib.notmuch_thread_destroy
410     _destroy.argtypes = [NotmuchThreadP]
411     _destroy.restype = None
412
413     def __del__(self):
414         """Close and free the notmuch Thread"""
415         if self._thread is not None:
416             self._destroy(self._thread)