[Python-checkins] python/nondist/sandbox/mailbox libmailbox.tex, 1.14, 1.15 mailbox.py, 1.15, 1.16 test_mailbox.py, 1.10, 1.11

gregorykjohnson@users.sourceforge.net gregorykjohnson at users.sourceforge.net
Tue Aug 23 05:43:21 CEST 2005


Update of /cvsroot/python/python/nondist/sandbox/mailbox
In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv9944

Modified Files:
	libmailbox.tex mailbox.py test_mailbox.py 
Log Message:
* Overhaul handling of line endings to play more nicely with the email
  and smtplib modules and to be less surprising:
    * Message instances and strings use C-style '\n' line endings, and
      files passed to add() or __setitem__() should be text-mode.
    * When written to disk, messages use native line endings.
    * As in the older module, file-like objects supplied to a message
      factory (or returned by get_file()) are still binary-mode-like.


Index: libmailbox.tex
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/mailbox/libmailbox.tex,v
retrieving revision 1.14
retrieving revision 1.15
diff -u -d -r1.14 -r1.15
--- libmailbox.tex	22 Aug 2005 20:19:27 -0000	1.14
+++ libmailbox.tex	23 Aug 2005 03:43:11 -0000	1.15
@@ -62,11 +62,12 @@
 it.
 
 Parameter \var{message} may be a \class{Message} instance, an
-\class{email.Message.Message} instance, a string, or a file-like object. If
-\var{message} is an instance of the appropriate format-specific
-\class{Message} subclass (e.g., if it's an \class{mboxMessage} instance and
-this is an \class{mbox} instance), its format-specific information is used.
-Otherwise, reasonable defaults for format-specific information are used.
+\class{email.Message.Message} instance, a string, or a file-like object (which
+should be open in text mode). If \var{message} is an instance of the
+appropriate format-specific \class{Message} subclass (e.g., if it's an
+\class{mboxMessage} instance and this is an \class{mbox} instance), its
+format-specific information is used. Otherwise, reasonable defaults for
+format-specific information are used.
 \end{methoddesc}
 
 \begin{methoddesc}{remove}{key}
@@ -87,11 +88,11 @@
 
 As with \method{add()}, parameter \var{message} may be a \class{Message}
 instance, an \class{email.Message.Message} instance, a string, or a file-like
-object. If \var{message} is an instance of the appropriate format-specific
-\class{Message} subclass (e.g., if it's an \class{mboxMessage} instance and
-this is an \class{mbox} instance), its format-specific information is used.
-Otherwise, the format-specific information of the message that currently
-corresponds to \var{key} is left unchanged. 
+object (which should be open in text mode). If \var{message} is an instance of
+the appropriate format-specific \class{Message} subclass (e.g., if it's an
+\class{mboxMessage} instance and this is an \class{mbox} instance), its
+format-specific information is used. Otherwise, the format-specific information
+of the message that currently corresponds to \var{key} is left unchanged. 
 \end{methoddesc}
 
 \begin{methoddesc}{iterkeys}{}
@@ -146,8 +147,9 @@
 
 \begin{methoddesc}{get_file}{key}
 Return a file-like representation of the message corresponding to \var{key},
-or raise a \exception{KeyError} exception if no such message exists. This file
-should be closed once it is no longer needed.
+or raise a \exception{KeyError} exception if no such message exists. The
+file-like object behaves as if open in binary mode. This file should be closed
+once it is no longer needed.
 
 \note{Unlike other representations of messages, file-like representations are
 not necessarily independent of the \class{Mailbox} instance that created them
@@ -228,10 +230,10 @@
 create=True}}}
 A subclass of \class{Mailbox} for mailboxes in Maildir format. Parameter
 \var{factory} is a callable object that accepts a file-like message
-representation and returns a custom representation. If \var{factory} is
-\code{None}, \class{MaildirMessage} is used as the default message
-representation. If \var{create} is \code{True}, the mailbox is created if it
-does not exist.
+representation (which behaves as if open in binary mode) and returns a custom
+representation. If \var{factory} is \code{None}, \class{MaildirMessage} is used
+as the default message representation. If \var{create} is \code{True}, the
+mailbox is created if it does not exist.
 
 It is for historical reasons that \var{factory} defaults to
 \class{rfc822.Message} and that \var{dirname} is named as such rather than
@@ -353,9 +355,10 @@
 \begin{classdesc}{mbox}{path\optional{, factory=None\optional{, create=True}}}
 A subclass of \class{Mailbox} for mailboxes in mbox format. Parameter
 \var{factory} is a callable object that accepts a file-like message
-representation and returns a custom representation. If \var{factory} is
-\code{None}, \class{mboxMessage} is used as the default message representation.
-If \var{create} is \code{True}, the mailbox is created if it does not exist.
+representation (which behaves as if open in binary mode) and returns a custom
+representation. If \var{factory} is \code{None}, \class{mboxMessage} is used as
+the default message representation. If \var{create} is \code{True}, the mailbox
+is created if it does not exist.
 \end{classdesc}
 
 The mbox format is the classic format for storing mail on \UNIX{} systems. All
@@ -405,9 +408,10 @@
 \begin{classdesc}{MH}{path\optional{, factory=None\optional{, create=True}}}
 A subclass of \class{Mailbox} for mailboxes in MH format. Parameter
 \var{factory} is a callable object that accepts a file-like message
-representation and returns a custom representation. If \var{factory} is
-\code{None}, \class{MHMessage} is used as the default message representation.
-If \var{create} is \code{True}, the mailbox is created if it does not exist.
+representation (which behaves as if open in binary mode) and returns a custom
+representation. If \var{factory} is \code{None}, \class{MHMessage} is used as
+the default message representation. If \var{create} is \code{True}, the mailbox
+is created if it does not exist.
 \end{classdesc}
 
 MH is a directory-based mailbox format invented for the MH Message Handling
@@ -511,10 +515,10 @@
 \begin{classdesc}{Babyl}{path\optional{, factory=None\optional{, create=True}}}
 A subclass of \class{Mailbox} for mailboxes in Babyl format. Parameter
 \var{factory} is a callable object that accepts a file-like message
-representation and returns a custom representation. If \var{factory} is
-\code{None}, \class{BabylMessage} is used as the default message
-representation. If \var{create} is \code{True}, the mailbox is created if it
-does not exist.
+representation (which behaves as if open in binary mode) and returns a custom
+representation. If \var{factory} is \code{None}, \class{BabylMessage} is used
+as the default message representation. If \var{create} is \code{True}, the
+mailbox is created if it does not exist.
 \end{classdesc}
 
 Babyl is a single-file mailbox format invented for the Rmail mail user agent
@@ -574,9 +578,10 @@
 \begin{classdesc}{MMDF}{path\optional{, factory=None\optional{, create=True}}}
 A subclass of \class{Mailbox} for mailboxes in MMDF format. Parameter
 \var{factory} is a callable object that accepts a file-like message
-representation and returns a custom representation. If \var{factory} is
-\code{None}, \class{MMDFMessage} is used as the default message representation.
-If \var{create} is \code{True}, the mailbox is created if it does not exist.
+representation (which behaves as if open in binary mode) and returns a custom
+representation. If \var{factory} is \code{None}, \class{MMDFMessage} is used as
+the default message representation. If \var{create} is \code{True}, the mailbox
+is created if it does not exist.
 \end{classdesc}
 
 MMDF is a single-file mailbox format invented for the Multichannel Memorandum
@@ -1010,7 +1015,8 @@
 \begin{methoddesc}{set_visible}{visible}
 Set the message's visible headers to be the same as the headers in
 \var{message}. Parameter \var{visible} should be a \class{Message} instance, an
-\class{email.Message.Message} instance, a string, or a file-like object.
+\class{email.Message.Message} instance, a string, or a file-like object (which
+should be open in text mode).
 \end{methoddesc}
 
 \begin{methoddesc}{update_visible}{}

Index: mailbox.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/mailbox/mailbox.py,v
retrieving revision 1.15
retrieving revision 1.16
diff -u -d -r1.15 -r1.16
--- mailbox.py	22 Aug 2005 20:19:28 -0000	1.15
+++ mailbox.py	23 Aug 2005 03:43:11 -0000	1.16
@@ -183,30 +183,30 @@
         raise NotImplementedError('Method must be implemented by subclass')
 
     def _dump_message(self, message, target, mangle_from_=False):
+        # Most files are opened in binary mode to allow predictable seeking.
+        # To get native line endings on disk, the user-friendly \n line endings
+        # used in strings and by email.Message are translated here.
         """Dump message contents to target file."""
         if isinstance(message, email.Message.Message):
-            generator = email.Generator.Generator(target, mangle_from_, 0)
-            generator.flatten(message)
+            buffer = StringIO.StringIO()
+            gen = email.Generator.Generator(buffer, mangle_from_, 0)
+            gen.flatten(message)
+            buffer.seek(0)
+            target.write(buffer.read().replace('\n', os.linesep))
         elif isinstance(message, str):
             if mangle_from_:
-                message = message.replace(os.linesep + 'From ',
-                                          os.linesep + '>From ')
+                message = message.replace('\nFrom ', '\n>From ')
+            message = message.replace('\n', os.linesep)
             target.write(message)
         elif hasattr(message, 'read'):
-            if mangle_from_:
-                while True:
-                    line = message.readline()
-                    if line == '':
-                        break
-                    if line.startswith('From '):
-                        line = '>From ' + line[5:]
-                    target.write(line)
-            else:
-                while True:
-                    buffer = message.read(4096)     # Buffer size is arbitrary.
-                    if buffer == '':
-                        break
-                    target.write(buffer)
+            while True:
+                line = message.readline()
+                if line == '':
+                    break
+                if mangle_from_ and line.startswith('From '):
+                    line = '>From ' + line[5:]
+                line = line.replace('\n', os.linesep)
+                target.write(line)
         else:
             raise TypeError('Invalid message type: %s' % type(message))
 
@@ -294,7 +294,7 @@
     def get_message(self, key):
         """Return a Message representation or raise a KeyError."""
         subpath = self._lookup(key)
-        f = file(os.path.join(self._path, subpath), 'rb')
+        f = file(os.path.join(self._path, subpath), 'r')
         try:
             msg = MaildirMessage(f)
         finally:
@@ -308,7 +308,7 @@
 
     def get_string(self, key):
         """Return a string representation or raise a KeyError."""
-        f = file(os.path.join(self._path, self._lookup(key)), 'rb')
+        f = file(os.path.join(self._path, self._lookup(key)), 'r')
         try:
             return f.read()
         finally:
@@ -627,9 +627,10 @@
         """Return a Message representation or raise a KeyError."""
         start, stop = self._lookup(key)
         self._file.seek(start)
-        from_line = self._file.readline()
-        msg = self._message_factory(self._file.read(stop - self._file.tell()))
-        msg.set_from(from_line[5:-len(os.linesep)])
+        from_line = self._file.readline().replace(os.linesep, '')
+        string = self._file.read(stop - self._file.tell())
+        msg = self._message_factory(string.replace(os.linesep, '\n'))
+        msg.set_from(from_line[5:])
         return msg
 
     def get_string(self, key, from_=False):
@@ -638,7 +639,8 @@
         self._file.seek(start)
         if not from_:
             self._file.readline()
-        return self._file.read(stop - self._file.tell())
+        string = self._file.read(stop - self._file.tell())
+        return string.replace(os.linesep, '\n')
 
     def get_file(self, key, from_=False):
         """Return a file-like representation or raise a KeyError."""
@@ -652,10 +654,10 @@
         """Format a message and blindly write to self._file."""
         from_line = None
         if isinstance(message, str) and message.startswith('From '):
-            newline = message.find(os.linesep)
+            newline = message.find('\n')
             if newline != -1:
                 from_line = message[:newline]
-                message = message[newline + len(os.linesep):]
+                message = message[newline + 1:]
             else:
                 from_line = message
                 message = ''
@@ -836,9 +838,9 @@
         """Return a Message representation or raise a KeyError."""
         try:
             if self._locked:
-                f = file(os.path.join(self._path, str(key)), 'rb+')
+                f = file(os.path.join(self._path, str(key)), 'r+')
             else:
-                f = file(os.path.join(self._path, str(key)), 'rb')
+                f = file(os.path.join(self._path, str(key)), 'r')
         except IOError, e:
             if e.errno == errno.ENOENT:
                 raise KeyError('No message with key: %s' % key)
@@ -863,9 +865,9 @@
         """Return a string representation or raise a KeyError."""
         try:
             if self._locked:
-                f = file(os.path.join(self._path, str(key)), 'rb+')
+                f = file(os.path.join(self._path, str(key)), 'r+')
             else:
-                f = file(os.path.join(self._path, str(key)), 'rb')
+                f = file(os.path.join(self._path, str(key)), 'r')
         except IOError, e:
             if e.errno == errno.ENOENT:
                 raise KeyError('No message with key: %s' % key)
@@ -961,7 +963,7 @@
     def get_sequences(self):
         """Return a name-to-key-list dictionary to define each sequence."""
         results = {}
-        f = file(os.path.join(self._path, '.mh_sequences'), 'rb')
+        f = file(os.path.join(self._path, '.mh_sequences'), 'r')
         try:
             all_keys = set(self.keys())
             for line in f:
@@ -987,7 +989,7 @@
 
     def set_sequences(self, sequences):
         """Set sequences using the given name-to-key-list dictionary."""
-        f = file(os.path.join(self._path, '.mh_sequences'), 'rb+')
+        f = file(os.path.join(self._path, '.mh_sequences'), 'r+')
         try:
             os.close(os.open(f.name, os.O_WRONLY | os.O_TRUNC))
             for name, keys in sequences.iteritems():
@@ -1008,9 +1010,9 @@
                         f.write(' %s' % key)
                     prev = key
                 if completing:
-                    f.write(str(prev) + os.linesep)
+                    f.write(str(prev) + '\n')
                 else:
-                    f.write(os.linesep)
+                    f.write('\n')
         finally:
             f.close()
 
@@ -1022,7 +1024,7 @@
         for key in self.iterkeys():
             if key - 1 != prev:
                 changes.append((key, prev + 1))
-                f = file(os.path.join(self._path, str(key)), 'rb+')
+                f = file(os.path.join(self._path, str(key)), 'r+')
                 try:
                     if self._locked:
                         _lock_file(f)
@@ -1105,14 +1107,15 @@
             line = self._file.readline()
             if line == '*** EOOH ***' + os.linesep or line == '':
                 break
-            original_headers.write(line)
+            original_headers.write(line.replace(os.linesep, '\n'))
         visible_headers = StringIO.StringIO()
         while True:
             line = self._file.readline()
             if line == os.linesep or line == '':
                 break
-            visible_headers.write(line)
-        body = self._file.read(stop - self._file.tell())
+            visible_headers.write(line.replace(os.linesep, '\n'))
+        body = self._file.read(stop - self._file.tell()).replace(os.linesep,
+                                                                 '\n')
         msg = BabylMessage(original_headers.getvalue() + body)
         msg.set_visible(visible_headers.getvalue())
         if key in self._labels:
@@ -1129,13 +1132,14 @@
             line = self._file.readline()
             if line == '*** EOOH ***' + os.linesep or line == '':
                 break
-            original_headers.write(line)
+            original_headers.write(line.replace(os.linesep, '\n'))
         while True:
             line = self._file.readline()
             if line == os.linesep or line == '':
                 break
         return original_headers.getvalue() + \
-               self._file.read(stop - self._file.tell())
+               self._file.read(stop - self._file.tell()).replace(os.linesep,
+                                                                 '\n')
 
     def get_file(self, key):
         """Return a file-like representation or raise a KeyError."""
@@ -1213,49 +1217,57 @@
         else:
             self._file.write('1,,' + os.linesep)
         if isinstance(message, email.Message.Message):
-            pseudofile = StringIO.StringIO()
-            ps_generator = email.Generator.Generator(pseudofile, False, 0)
-            ps_generator.flatten(message)
-            pseudofile.seek(0)
+            orig_buffer = StringIO.StringIO()
+            orig_generator = email.Generator.Generator(orig_buffer, False, 0)
+            orig_generator.flatten(message)
+            orig_buffer.seek(0)
             while True:
-                line = pseudofile.readline()
-                self._file.write(line)
-                if line == os.linesep or line == '':
+                line = orig_buffer.readline()
+                self._file.write(line.replace('\n', os.linesep))
+                if line == '\n' or line == '':
                     break
             self._file.write('*** EOOH ***' + os.linesep)
             if isinstance(message, BabylMessage):
-                generator = email.Generator.Generator(self._file, False, 0)
-                generator.flatten(message.get_visible())
+                vis_buffer = StringIO.StringIO()
+                vis_generator = email.Generator.Generator(vis_buffer, False, 0)
+                vis_generator.flatten(message.get_visible())
+                while True:
+                    line = vis_buffer.readline()
+                    self._file.write(line.replace('\n', os.linesep))
+                    if line == '\n' or line == '':
+                        break
             else:
-                pseudofile.seek(0)
+                orig_buffer.seek(0)
                 while True:
-                    line = pseudofile.readline()
-                    self._file.write(line)
-                    if line == os.linesep or line == '':
+                    line = orig_buffer.readline()
+                    self._file.write(line.replace('\n', os.linesep))
+                    if line == '\n' or line == '':
                         break
             while True:
-                buffer = pseudofile.read(4096)  # Buffer size is arbitrary.
+                buffer = orig_buffer.read(4096) # Buffer size is arbitrary.
                 if buffer == '':
                     break
-                self._file.write(buffer)
+                self._file.write(buffer.replace('\n', os.linesep))
         elif isinstance(message, str):
-            body_start = message.find(os.linesep + os.linesep) + \
-                         2 * len(os.linesep)
-            if body_start - 2 * len(os.linesep) != -1:
-                self._file.write(message[:body_start])
+            body_start = message.find('\n\n') + 2
+            if body_start - 2 != -1:
+                self._file.write(message[:body_start].replace('\n',
+                                                              os.linesep))
                 self._file.write('*** EOOH ***' + os.linesep)
-                self._file.write(message[:body_start])
-                self._file.write(message[body_start:])
+                self._file.write(message[:body_start].replace('\n',
+                                                              os.linesep))
+                self._file.write(message[body_start:].replace('\n',
+                                                              os.linesep))
             else:
                 self._file.write('*** EOOH ***' + os.linesep + os.linesep)
-                self._file.write(message)
+                self._file.write(message.replace('\n', os.linesep))
         elif hasattr(message, 'readline'):
             original_pos = message.tell()
             first_pass = True
             while True:
                 line = message.readline()
-                self._file.write(line)
-                if line == os.linesep or line == '':
+                self._file.write(line.replace('\n', os.linesep))
+                if line == '\n' or line == '':
                     self._file.write('*** EOOH ***' + os.linesep)
                     if first_pass:
                         first_pass = False
@@ -1266,7 +1278,7 @@
                 buffer = message.read(4096)     # Buffer size is arbitrary.
                 if buffer == '':
                     break
-                self._file.write(buffer)
+                self._file.write(buffer.replace('\n', os.linesep))
         else:
             raise TypeError('Invalid message type: %s' % type(message))
         stop = self._file.tell()

Index: test_mailbox.py
===================================================================
RCS file: /cvsroot/python/python/nondist/sandbox/mailbox/test_mailbox.py,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -d -r1.10 -r1.11
--- test_mailbox.py	22 Aug 2005 20:19:28 -0000	1.10
+++ test_mailbox.py	23 Aug 2005 03:43:11 -0000	1.11
@@ -49,7 +49,7 @@
 class TestMailbox(TestBase):
 
     _factory = None     # Overridden by subclasses to reuse tests
-    _template = 'From: foo' + os.linesep + os.linesep + '%s'
+    _template = 'From: foo\n\n%s'
 
     def setUp(self):
         self._path = test_support.TESTFN
@@ -72,8 +72,7 @@
         self.assert_(len(self._box) == 4)
         keys.append(self._box.add(_sample_message))
         self.assert_(len(self._box) == 5)
-        self.assert_(self._box.get_string(keys[0]) == self._template % 0,
-                     self._box.get_string(keys[0]))
+        self.assert_(self._box.get_string(keys[0]) == self._template % 0)
         for i in (1, 2, 3, 4):
             self._check_sample(self._box[keys[i]])
 
@@ -167,8 +166,10 @@
         # Get file representations of messages
         key0 = self._box.add(self._template % 0)
         key1 = self._box.add(_sample_message)
-        self.assert_(self._box.get_file(key0).read() == self._template % 0)
-        self.assert_(self._box.get_file(key1).read() == _sample_message)
+        self.assert_(self._box.get_file(key0).read().replace(os.linesep, '\n')
+                     == self._template % 0)
+        self.assert_(self._box.get_file(key1).read().replace(os.linesep, '\n')
+                     == _sample_message)
 
     def test_iterkeys(self):
         # Get keys using iterkeys()
@@ -408,7 +409,8 @@
                       _sample_message, StringIO.StringIO(_sample_message)):
             output = StringIO.StringIO()
             self._box._dump_message(input, output)
-            self.assert_(output.getvalue() == _sample_message)
+            self.assert_(output.getvalue() ==
+                         _sample_message.replace('\n', os.linesep))
         output = StringIO.StringIO()
         self.assertRaises(TypeError,
                           lambda: self._box._dump_message(None, output))
@@ -677,16 +679,14 @@
 
     def test_add_from_string(self):
         # Add a string starting with 'From ' to the mailbox
-        key = self._box.add('From foo at bar blah%sFrom: foo%s%s0' %
-                            (os.linesep, os.linesep, os.linesep))
+        key = self._box.add('From foo at bar blah\nFrom: foo\n\n0')
         self.assert_(self._box[key].get_from() == 'foo at bar blah')
         self.assert_(self._box[key].get_payload() == '0')
 
     def test_add_mbox_or_mmdf_message(self):
         # Add an mboxMessage or MMDFMessage
         for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
-            msg = class_('From foo at bar blah%sFrom: foo%s%s0' %
-                         (os.linesep, os.linesep, os.linesep))
+            msg = class_('From foo at bar blah\nFrom: foo\n\n0')
             key = self._box.add(msg)
 
     def test_open_close_open(self):



More information about the Python-checkins mailing list