python: move the exception classes into error.py
[notmuch] / bindings / python / notmuch / threads.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 notmuch.globals import (
21     nmlib,
22     Python3StringMixIn,
23     NotmuchThreadP,
24     NotmuchThreadsP,
25 )
26 from .errors import (
27     NullPointerError,
28     NotInitializedError,
29 )
30 from .thread import Thread
31
32 class Threads(Python3StringMixIn):
33     """Represents a list of notmuch threads
34
35     This object provides an iterator over a list of notmuch threads
36     (Technically, it provides a wrapper for the underlying
37     *notmuch_threads_t* structure). Do note that the underlying
38     library only provides a one-time iterator (it cannot reset the
39     iterator to the start). Thus iterating over the function will
40     "exhaust" the list of threads, and a subsequent iteration attempt
41     will raise a :exc:`NotInitializedError`. Also
42     note, that any function that uses iteration will also
43     exhaust the messages. So both::
44
45       for thread in threads: print thread
46
47     as well as::
48
49        number_of_msgs = len(threads)
50
51     will "exhaust" the threads. If you need to re-iterate over a list of
52     messages you will need to retrieve a new :class:`Threads` object.
53
54     Things are not as bad as it seems though, you can store and reuse
55     the single Thread objects as often as you want as long as you
56     keep the parent Threads object around. (Recall that due to
57     hierarchical memory allocation, all derived Threads objects will
58     be invalid when we delete the parent Threads() object, even if it
59     was already "exhausted".) So this works::
60
61       db   = Database()
62       threads = Query(db,'').search_threads() #get a Threads() object
63       threadlist = []
64       for thread in threads:
65          threadlist.append(thread)
66
67       # threads is "exhausted" now and even len(threads) will raise an
68       # exception.
69       # However it will be kept around until all retrieved Thread() objects are
70       # also deleted. If you did e.g. an explicit del(threads) here, the
71       # following lines would fail.
72
73       # You can reiterate over *threadlist* however as often as you want.
74       # It is simply a list with Thread objects.
75
76       print (threadlist[0].get_thread_id())
77       print (threadlist[1].get_thread_id())
78       print (threadlist[0].get_total_messages())
79     """
80
81     #notmuch_threads_get
82     _get = nmlib.notmuch_threads_get
83     _get.argtypes = [NotmuchThreadsP]
84     _get.restype = NotmuchThreadP
85
86     def __init__(self, threads_p, parent=None):
87         """
88         :param threads_p:  A pointer to an underlying *notmuch_threads_t*
89              structure. These are not publically exposed, so a user
90              will almost never instantiate a :class:`Threads` object
91              herself. They are usually handed back as a result,
92              e.g. in :meth:`Query.search_threads`.  *threads_p* must be
93              valid, we will raise an :exc:`NullPointerError` if it is
94              `None`.
95         :type threads_p: :class:`ctypes.c_void_p`
96         :param parent: The parent object
97              (ie :class:`Query`) these tags are derived from. It saves
98              a reference to it, so we can automatically delete the db
99              object once all derived objects are dead.
100         :TODO: Make the iterator work more than once and cache the tags in
101                the Python object.(?)
102         """
103         if not threads_p:
104             raise NullPointerError()
105
106         self._threads = threads_p
107         #store parent, so we keep them alive as long as self  is alive
108         self._parent = parent
109
110     def __iter__(self):
111         """ Make Threads an iterator """
112         return self
113
114     _valid = nmlib.notmuch_threads_valid
115     _valid.argtypes = [NotmuchThreadsP]
116     _valid.restype = bool
117
118     _move_to_next = nmlib.notmuch_threads_move_to_next
119     _move_to_next.argtypes = [NotmuchThreadsP]
120     _move_to_next.restype = None
121
122     def __next__(self):
123         if not self._threads:
124             raise NotInitializedError()
125
126         if not self._valid(self._threads):
127             self._threads = None
128             raise StopIteration
129
130         thread = Thread(Threads._get(self._threads), self)
131         self._move_to_next(self._threads)
132         return thread
133     next = __next__ # python2.x iterator protocol compatibility
134
135     def __len__(self):
136         """len(:class:`Threads`) returns the number of contained Threads
137
138         .. note:: As this iterates over the threads, we will not be able to
139                iterate over them again! So this will fail::
140
141                  #THIS FAILS
142                  threads = Database().create_query('').search_threads()
143                  if len(threads) > 0:              #this 'exhausts' threads
144                      # next line raises :exc:`NotInitializedError`!!!
145                      for thread in threads: print thread
146         """
147         if not self._threads:
148             raise NotInitializedError()
149
150         i = 0
151         # returns 'bool'. On out-of-memory it returns None
152         while self._valid(self._threads):
153             self._move_to_next(self._threads)
154             i += 1
155         # reset self._threads to mark as "exhausted"
156         self._threads = None
157         return i
158
159     def __nonzero__(self):
160         """Check if :class:`Threads` contains at least one more valid thread
161
162         The existence of this function makes 'if Threads: foo' work, as
163         that will implicitely call len() exhausting the iterator if
164         __nonzero__ does not exist. This function makes `bool(Threads())`
165         work repeatedly.
166
167         :return: True if there is at least one more thread in the
168            Iterator, False if not. None on a "Out-of-memory" error.
169         """
170         return self._threads is not None and \
171             self._valid(self._threads) > 0
172
173     _destroy = nmlib.notmuch_threads_destroy
174     _destroy.argtypes = [NotmuchThreadsP]
175     _destroy.argtypes = None
176
177     def __del__(self):
178         """Close and free the notmuch Threads"""
179         if self._threads is not None:
180             self._destroy(self._threads)