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