]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/messages.py
python: cleanup the __nonzero__ implementations
[notmuch] / bindings / python / notmuch / messages.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                Jesse Rosenthal <jrosenthal@jhu.edu>
19 """
20
21 from .globals import (
22     nmlib,
23     NotmuchTagsP,
24     NotmuchMessageP,
25     NotmuchMessagesP,
26 )
27 from .errors import (
28     NullPointerError,
29     NotInitializedError,
30 )
31 from .tag import Tags
32 from .message import Message
33
34 import sys
35
36 class Messages(object):
37     """Represents a list of notmuch messages
38
39     This object provides an iterator over a list of notmuch messages
40     (Technically, it provides a wrapper for the underlying
41     *notmuch_messages_t* structure). Do note that the underlying library
42     only provides a one-time iterator (it cannot reset the iterator to
43     the start). Thus iterating over the function will "exhaust" the list
44     of messages, and a subsequent iteration attempt will raise a
45     :exc:`NotInitializedError`. If you need to
46     re-iterate over a list of messages you will need to retrieve a new
47     :class:`Messages` object or cache your :class:`Message`\s in a list
48     via::
49
50        msglist = list(msgs)
51
52     You can store and reuse the single :class:`Message` objects as often
53     as you want as long as you keep the parent :class:`Messages` object
54     around. (Due to hierarchical memory allocation, all derived
55     :class:`Message` objects will be invalid when we delete the parent
56     :class:`Messages` object, even if it was already exhausted.) So
57     this works::
58
59       db   = Database()
60       msgs = Query(db,'').search_messages() #get a Messages() object
61       msglist = list(msgs)
62
63       # msgs is "exhausted" now and msgs.next() will raise an exception.
64       # However it will be kept alive until all retrieved Message()
65       # objects are also deleted. If you do e.g. an explicit del(msgs)
66       # here, the following lines would fail.
67
68       # You can reiterate over *msglist* however as often as you want.
69       # It is simply a list with :class:`Message`s.
70
71       print (msglist[0].get_filename())
72       print (msglist[1].get_filename())
73       print (msglist[0].get_message_id())
74
75
76     As :class:`Message` implements both __hash__() and __cmp__(), it is
77     possible to make sets out of :class:`Messages` and use set
78     arithmetic (this happens in python and will of course be *much*
79     slower than redoing a proper query with the appropriate filters::
80
81         s1, s2 = set(msgs1), set(msgs2)
82         s.union(s2)
83         s1 -= s2
84         ...
85
86     Be careful when using set arithmetic between message sets derived
87     from different Databases (ie the same database reopened after
88     messages have changed). If messages have added or removed associated
89     files in the meantime, it is possible that the same message would be
90     considered as a different object (as it points to a different file).
91     """
92
93     #notmuch_messages_get
94     _get = nmlib.notmuch_messages_get
95     _get.argtypes = [NotmuchMessagesP]
96     _get.restype = NotmuchMessageP
97
98     _collect_tags = nmlib.notmuch_messages_collect_tags
99     _collect_tags.argtypes = [NotmuchMessagesP]
100     _collect_tags.restype = NotmuchTagsP
101
102     def __init__(self, msgs_p, parent=None):
103         """
104         :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
105              structure. These are not publically exposed, so a user
106              will almost never instantiate a :class:`Messages` object
107              herself. They are usually handed back as a result,
108              e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
109              valid, we will raise an :exc:`NullPointerError` if it is
110              `None`.
111         :type msgs_p: :class:`ctypes.c_void_p`
112         :param parent: The parent object
113              (ie :class:`Query`) these tags are derived from. It saves
114              a reference to it, so we can automatically delete the db
115              object once all derived objects are dead.
116         :TODO: Make the iterator work more than once and cache the tags in
117                the Python object.(?)
118         """
119         if not msgs_p:
120             raise NullPointerError()
121
122         self._msgs = msgs_p
123         #store parent, so we keep them alive as long as self  is alive
124         self._parent = parent
125
126     def collect_tags(self):
127         """Return the unique :class:`Tags` in the contained messages
128
129         :returns: :class:`Tags`
130         :exceptions: :exc:`NotInitializedError` if not init'ed
131
132         .. note::
133
134             :meth:`collect_tags` will iterate over the messages and therefore
135             will not allow further iterations.
136         """
137         if not self._msgs:
138             raise NotInitializedError()
139
140         # collect all tags (returns NULL on error)
141         tags_p = Messages._collect_tags(self._msgs)
142         #reset _msgs as we iterated over it and can do so only once
143         self._msgs = None
144
145         if tags_p == None:
146             raise NullPointerError()
147         return Tags(tags_p, self)
148
149     def __iter__(self):
150         """ Make Messages an iterator """
151         return self
152
153     _valid = nmlib.notmuch_messages_valid
154     _valid.argtypes = [NotmuchMessagesP]
155     _valid.restype = bool
156
157     _move_to_next = nmlib.notmuch_messages_move_to_next
158     _move_to_next.argtypes = [NotmuchMessagesP]
159     _move_to_next.restype = None
160
161     def __next__(self):
162         if not self._msgs:
163             raise NotInitializedError()
164
165         if not self._valid(self._msgs):
166             self._msgs = None
167             raise StopIteration
168
169         msg = Message(Messages._get(self._msgs), self)
170         self._move_to_next(self._msgs)
171         return msg
172     next = __next__ # python2.x iterator protocol compatibility
173
174     def __nonzero__(self):
175         '''
176         Implement truth value testing. If __nonzero__ is not
177         implemented, the python runtime would fall back to `len(..) >
178         0` thus exhausting the iterator.
179
180         :returns: True if the wrapped iterator has at least one more object
181                   left.
182         '''
183         return self._msgs and self._valid(self._msgs)
184
185     _destroy = nmlib.notmuch_messages_destroy
186     _destroy.argtypes = [NotmuchMessagesP]
187     _destroy.restype = None
188
189     def __del__(self):
190         """Close and free the notmuch Messages"""
191         if self._msgs:
192             self._destroy(self._msgs)
193
194     def format_messages(self, format, indent=0, entire_thread=False):
195         """Formats messages as needed for 'notmuch show'.
196
197         :param format: A string of either 'text' or 'json'.
198         :param indent: A number indicating the reply depth of these messages.
199         :param entire_thread: A bool, indicating whether we want to output
200                        whole threads or only the matching messages.
201         :return: a list of lines
202         """
203         result = list()
204
205         if format.lower() == "text":
206             set_start = ""
207             set_end = ""
208             set_sep = ""
209         elif format.lower() == "json":
210             set_start = "["
211             set_end = "]"
212             set_sep = ", "
213         else:
214             raise TypeError("format must be either 'text' or 'json'")
215
216         first_set = True
217
218         result.append(set_start)
219
220         # iterate through all toplevel messages in this thread
221         for msg in self:
222             # if not msg:
223             #     break
224             if not first_set:
225                 result.append(set_sep)
226             first_set = False
227
228             result.append(set_start)
229             match = msg.is_match()
230             next_indent = indent
231
232             if (match or entire_thread):
233                 if format.lower() == "text":
234                     result.append(msg.format_message_as_text(indent))
235                 else:
236                     result.append(msg.format_message_as_json(indent))
237                 next_indent = indent + 1
238
239             # get replies and print them also out (if there are any)
240             replies = msg.get_replies().format_messages(format, next_indent, entire_thread)
241             if replies:
242                 result.append(set_sep)
243                 result.extend(replies)
244
245             result.append(set_end)
246         result.append(set_end)
247
248         return result
249
250     def print_messages(self, format, indent=0, entire_thread=False, handle=sys.stdout):
251         """Outputs messages as needed for 'notmuch show' to a file like object.
252
253         :param format: A string of either 'text' or 'json'.
254         :param handle: A file like object to print to (default is sys.stdout).
255         :param indent: A number indicating the reply depth of these messages.
256         :param entire_thread: A bool, indicating whether we want to output
257                        whole threads or only the matching messages.
258         """
259         handle.write(''.join(self.format_messages(format, indent, entire_thread)))
260
261 class EmptyMessagesResult(Messages):
262     def __init__(self, parent):
263         self._msgs = None
264         self._parent = parent
265
266     def __next__(self):
267         raise StopIteration()
268     next = __next__