]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/messages.py
Merge tag '0.13.2'
[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 not tags_p:
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         .. deprecated:: 0.14
204                         This code adds functionality at the python
205                         level that is unlikely to be useful for
206                         anyone. Furthermore the python bindings strive
207                         to be a thin wrapper around libnotmuch, so
208                         this code will be removed in notmuch 0.15.
209         """
210         result = list()
211
212         if format.lower() == "text":
213             set_start = ""
214             set_end = ""
215             set_sep = ""
216         elif format.lower() == "json":
217             set_start = "["
218             set_end = "]"
219             set_sep = ", "
220         else:
221             raise TypeError("format must be either 'text' or 'json'")
222
223         first_set = True
224
225         result.append(set_start)
226
227         # iterate through all toplevel messages in this thread
228         for msg in self:
229             # if not msg:
230             #     break
231             if not first_set:
232                 result.append(set_sep)
233             first_set = False
234
235             result.append(set_start)
236             match = msg.is_match()
237             next_indent = indent
238
239             if (match or entire_thread):
240                 if format.lower() == "text":
241                     result.append(msg.format_message_as_text(indent))
242                 else:
243                     result.append(msg.format_message_as_json(indent))
244                 next_indent = indent + 1
245
246             # get replies and print them also out (if there are any)
247             replies = msg.get_replies().format_messages(format, next_indent, entire_thread)
248             if replies:
249                 result.append(set_sep)
250                 result.extend(replies)
251
252             result.append(set_end)
253         result.append(set_end)
254
255         return result
256
257     def print_messages(self, format, indent=0, entire_thread=False, handle=sys.stdout):
258         """Outputs messages as needed for 'notmuch show' to a file like object.
259
260         :param format: A string of either 'text' or 'json'.
261         :param handle: A file like object to print to (default is sys.stdout).
262         :param indent: A number indicating the reply depth of these messages.
263         :param entire_thread: A bool, indicating whether we want to output
264                        whole threads or only the matching messages.
265
266         .. deprecated:: 0.14
267                         This code adds functionality at the python
268                         level that is unlikely to be useful for
269                         anyone. Furthermore the python bindings strive
270                         to be a thin wrapper around libnotmuch, so
271                         this code will be removed in notmuch 0.15.
272         """
273         handle.write(''.join(self.format_messages(format, indent, entire_thread)))
274
275 class EmptyMessagesResult(Messages):
276     def __init__(self, parent):
277         self._msgs = None
278         self._parent = parent
279
280     def __next__(self):
281         raise StopIteration()
282     next = __next__