X-Git-Url: https://git.notmuchmail.org/git?a=blobdiff_plain;f=bindings%2Fpython%2Fnotmuch%2Fmessage.py;h=883ed233c7e170fbfcab9b58aca2676f31417920;hb=bb514d7862aa38f6628ef1c8cb69ed5ec72098fd;hp=e0c7eda2b46d492cd37cc6a8712c6db6e5a88a03;hpb=3434d194026ff65217d9342ffe511f67fd71e79f;p=notmuch diff --git a/bindings/python/notmuch/message.py b/bindings/python/notmuch/message.py index e0c7eda2..883ed233 100644 --- a/bindings/python/notmuch/message.py +++ b/bindings/python/notmuch/message.py @@ -19,15 +19,15 @@ Copyright 2010 Sebastian Spaeth ' """ -from ctypes import c_char_p, c_void_p, c_long, c_uint, c_int +from ctypes import c_char_p, c_long, c_uint, c_int from datetime import date -from notmuch.globals import (nmlib, STATUS, NotmuchError, Enum, _str, +from notmuch.globals import ( + nmlib, STATUS, NotmuchError, Enum, _str, Python3StringMixIn, NotmuchTagsP, NotmuchMessagesP, NotmuchMessageP, NotmuchFilenamesP) from notmuch.tag import Tags from notmuch.filename import Filenames import sys import email -import types try: import simplejson as json except ImportError: @@ -117,7 +117,7 @@ class Messages(object): :TODO: Make the iterator work more than once and cache the tags in the Python object.(?) """ - if msgs_p is None: + if not msgs_p: raise NotmuchError(STATUS.NULL_POINTER) self._msgs = msgs_p @@ -128,10 +128,12 @@ class Messages(object): """Return the unique :class:`Tags` in the contained messages :returns: :class:`Tags` - :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited + :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not init'ed - .. note:: :meth:`collect_tags` will iterate over the messages and - therefore will not allow further iterations. + .. note:: + + :meth:`collect_tags` will iterate over the messages and therefore + will not allow further iterations. """ if self._msgs is None: raise NotmuchError(STATUS.NOT_INITIALIZED) @@ -157,7 +159,7 @@ class Messages(object): _move_to_next.argtypes = [NotmuchMessagesP] _move_to_next.restype = None - def next(self): + def __next__(self): if self._msgs is None: raise NotmuchError(STATUS.NOT_INITIALIZED) @@ -168,6 +170,7 @@ class Messages(object): msg = Message(Messages._get(self._msgs), self) self._move_to_next(self._msgs) return msg + next = __next__ # python2.x iterator protocol compatibility def __nonzero__(self): """ @@ -185,14 +188,17 @@ class Messages(object): if self._msgs is not None: self._destroy(self._msgs) - def print_messages(self, format, indent=0, entire_thread=False): - """Outputs messages as needed for 'notmuch show' to sys.stdout + def format_messages(self, format, indent=0, entire_thread=False): + """Formats messages as needed for 'notmuch show'. :param format: A string of either 'text' or 'json'. :param indent: A number indicating the reply depth of these messages. :param entire_thread: A bool, indicating whether we want to output whole threads or only the matching messages. + :return: a list of lines """ + result = list() + if format.lower() == "text": set_start = "" set_end = "" @@ -206,38 +212,61 @@ class Messages(object): first_set = True - sys.stdout.write(set_start) + result.append(set_start) # iterate through all toplevel messages in this thread for msg in self: # if not msg: # break if not first_set: - sys.stdout.write(set_sep) + result.append(set_sep) first_set = False - sys.stdout.write(set_start) + result.append(set_start) match = msg.is_match() next_indent = indent if (match or entire_thread): if format.lower() == "text": - sys.stdout.write(msg.format_message_as_text(indent)) + result.append(msg.format_message_as_text(indent)) else: - sys.stdout.write(msg.format_message_as_json(indent)) + result.append(msg.format_message_as_json(indent)) next_indent = indent + 1 # get replies and print them also out (if there are any) - replies = msg.get_replies() - if not replies is None: - sys.stdout.write(set_sep) - replies.print_messages(format, next_indent, entire_thread) + replies = msg.get_replies().format_messages(format, next_indent, entire_thread) + if replies: + result.append(set_sep) + result.extend(replies) + + result.append(set_end) + result.append(set_end) + + return result + + def print_messages(self, format, indent=0, entire_thread=False, handle=sys.stdout): + """Outputs messages as needed for 'notmuch show' to a file like object. - sys.stdout.write(set_end) - sys.stdout.write(set_end) + :param format: A string of either 'text' or 'json'. + :param handle: A file like object to print to (default is sys.stdout). + :param indent: A number indicating the reply depth of these messages. + :param entire_thread: A bool, indicating whether we want to output + whole threads or only the matching messages. + """ + handle.write(''.join(self.format_messages(format, indent, entire_thread))) -class Message(object): +class EmptyMessagesResult(Messages): + def __init__(self, parent): + self._msgs = None + self._parent = parent + + def __next__(self): + raise StopIteration() + next = __next__ + + +class Message(Python3StringMixIn): """Represents a single Email message Technically, this wraps the underlying *notmuch_message_t* @@ -320,7 +349,7 @@ class Message(object): automatically delete the parent object once all derived objects are dead. """ - if msg_p is None: + if not msg_p: raise NotmuchError(STATUS.NULL_POINTER) self._msg = msg_p #keep reference to parent, so we keep it alive @@ -335,7 +364,7 @@ class Message(object): """ if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_message_id(self._msg) + return Message._get_message_id(self._msg).decode('utf-8', 'ignore') def get_thread_id(self): """Returns the thread ID @@ -353,23 +382,23 @@ class Message(object): if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_thread_id(self._msg) + return Message._get_thread_id(self._msg).decode('utf-8', 'ignore') def get_replies(self): """Gets all direct replies to this message as :class:`Messages` iterator - .. note:: This call only makes sense if 'message' was - ultimately obtained from a :class:`Thread` object, (such as - by coming directly from the result of calling - :meth:`Thread.get_toplevel_messages` or by any number of - subsequent calls to :meth:`get_replies`). If this message was - obtained through some non-thread means, (such as by a call - to :meth:`Query.search_messages`), then this function will - return `None`. - - :returns: :class:`Messages` or `None` if there are no replies to - this message. + .. note:: + + This call only makes sense if 'message' was ultimately obtained from + a :class:`Thread` object, (such as by coming directly from the + result of calling :meth:`Thread.get_toplevel_messages` or by any + number of subsequent calls to :meth:`get_replies`). If this message + was obtained through some non-thread means, (such as by a call to + :meth:`Query.search_messages`), then this function will return + an empty Messages iterator. + + :returns: :class:`Messages`. :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message is not initialized. """ @@ -378,8 +407,8 @@ class Message(object): msgs_p = Message._get_replies(self._msg) - if msgs_p is None: - return None + if not msgs_p: + return EmptyMessagesResult(self) return Messages(msgs_p, self) @@ -422,10 +451,10 @@ class Message(object): raise NotmuchError(STATUS.NOT_INITIALIZED) #Returns NULL if any error occurs. - header = Message._get_header(self._msg, header) + header = Message._get_header(self._msg, _str(header)) if header == None: raise NotmuchError(STATUS.NULL_POINTER) - return header.decode('UTF-8') + return header.decode('UTF-8', 'ignore') def get_filename(self): """Returns the file path of the message file @@ -436,7 +465,7 @@ class Message(object): """ if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_filename(self._msg) + return Message._get_filename(self._msg).decode('utf-8', 'ignore') def get_filenames(self): """Get all filenames for the email corresponding to 'message' @@ -647,7 +676,7 @@ class Message(object): This means that changes to the message state, (via :meth:`add_tag`, :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be - committed to the database until the message is :meth:`thaw`ed. + committed to the database until the message is :meth:`thaw` ed. Multiple calls to freeze/thaw are valid and these calls will "stack". That is there must be as many calls to thaw as to freeze @@ -756,11 +785,11 @@ class Message(object): not work yet, as the modified tags have not been committed yet to the database. - :returns: a :class:`STATUS`. In short, you want to see + :returns: a :class:`STATUS` value. In short, you want to see notmuch.STATUS.SUCCESS here. See there for details.""" if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - status = Message._tags_to_maildir_flags(self._msg) + return Message._tags_to_maildir_flags(self._msg) def maildir_flags_to_tags(self): """Synchronize file Maildir flags to notmuch tags @@ -787,19 +816,18 @@ class Message(object): notmuch.STATUS.SUCCESS here. See there for details.""" if self._msg is None: raise NotmuchError(STATUS.NOT_INITIALIZED) - status = Message._tags_to_maildir_flags(self._msg) + return Message._tags_to_maildir_flags(self._msg) def __repr__(self): """Represent a Message() object by str()""" return self.__str__() - def __str__(self): - """A message() is represented by a 1-line summary""" - msg = {} - msg['from'] = self.get_header('from') - msg['tags'] = self.get_tags() - msg['date'] = date.fromtimestamp(self.get_date()) - return "%(from)s (%(date)s) (%(tags)s)" % (msg) + def __unicode__(self): + format = "%s (%s) (%s)" + return format % (self.get_header('from'), + self.get_tags(), + date.fromtimestamp(self.get_date()), + ) def get_message_parts(self): """Output like notmuch show"""