python: move the exception classes into error.py
[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         :return: True if there is at least one more thread in the
177             Iterator, False if not."""
178         return self._msgs is not None and \
179             self._valid(self._msgs) > 0
180
181     _destroy = nmlib.notmuch_messages_destroy
182     _destroy.argtypes = [NotmuchMessagesP]
183     _destroy.restype = None
184
185     def __del__(self):
186         """Close and free the notmuch Messages"""
187         if self._msgs is not None:
188             self._destroy(self._msgs)
189
190     def format_messages(self, format, indent=0, entire_thread=False):
191         """Formats messages as needed for 'notmuch show'.
192
193         :param format: A string of either 'text' or 'json'.
194         :param indent: A number indicating the reply depth of these messages.
195         :param entire_thread: A bool, indicating whether we want to output
196                        whole threads or only the matching messages.
197         :return: a list of lines
198         """
199         result = list()
200
201         if format.lower() == "text":
202             set_start = ""
203             set_end = ""
204             set_sep = ""
205         elif format.lower() == "json":
206             set_start = "["
207             set_end = "]"
208             set_sep = ", "
209         else:
210             raise TypeError("format must be either 'text' or 'json'")
211
212         first_set = True
213
214         result.append(set_start)
215
216         # iterate through all toplevel messages in this thread
217         for msg in self:
218             # if not msg:
219             #     break
220             if not first_set:
221                 result.append(set_sep)
222             first_set = False
223
224             result.append(set_start)
225             match = msg.is_match()
226             next_indent = indent
227
228             if (match or entire_thread):
229                 if format.lower() == "text":
230                     result.append(msg.format_message_as_text(indent))
231                 else:
232                     result.append(msg.format_message_as_json(indent))
233                 next_indent = indent + 1
234
235             # get replies and print them also out (if there are any)
236             replies = msg.get_replies().format_messages(format, next_indent, entire_thread)
237             if replies:
238                 result.append(set_sep)
239                 result.extend(replies)
240
241             result.append(set_end)
242         result.append(set_end)
243
244         return result
245
246     def print_messages(self, format, indent=0, entire_thread=False, handle=sys.stdout):
247         """Outputs messages as needed for 'notmuch show' to a file like object.
248
249         :param format: A string of either 'text' or 'json'.
250         :param handle: A file like object to print to (default is sys.stdout).
251         :param indent: A number indicating the reply depth of these messages.
252         :param entire_thread: A bool, indicating whether we want to output
253                        whole threads or only the matching messages.
254         """
255         handle.write(''.join(self.format_messages(format, indent, entire_thread)))
256
257 class EmptyMessagesResult(Messages):
258     def __init__(self, parent):
259         self._msgs = None
260         self._parent = parent
261
262     def __next__(self):
263         raise StopIteration()
264     next = __next__