Kaydet (Commit) f6b30385 authored tarafından Petri Lehtinen's avatar Petri Lehtinen

#15222: Merge 3.2

...@@ -208,6 +208,9 @@ class Mailbox: ...@@ -208,6 +208,9 @@ class Mailbox:
raise ValueError("String input must be ASCII-only; " raise ValueError("String input must be ASCII-only; "
"use bytes or a Message instead") "use bytes or a Message instead")
# Whether each message must end in a newline
_append_newline = False
def _dump_message(self, message, target, mangle_from_=False): def _dump_message(self, message, target, mangle_from_=False):
# This assumes the target file is open in binary mode. # This assumes the target file is open in binary mode.
"""Dump message contents to target file.""" """Dump message contents to target file."""
...@@ -219,6 +222,9 @@ class Mailbox: ...@@ -219,6 +222,9 @@ class Mailbox:
data = buffer.read() data = buffer.read()
data = data.replace(b'\n', linesep) data = data.replace(b'\n', linesep)
target.write(data) target.write(data)
if self._append_newline and not data.endswith(linesep):
# Make sure the message ends with a newline
target.write(linesep)
elif isinstance(message, (str, bytes, io.StringIO)): elif isinstance(message, (str, bytes, io.StringIO)):
if isinstance(message, io.StringIO): if isinstance(message, io.StringIO):
warnings.warn("Use of StringIO input is deprecated, " warnings.warn("Use of StringIO input is deprecated, "
...@@ -230,11 +236,15 @@ class Mailbox: ...@@ -230,11 +236,15 @@ class Mailbox:
message = message.replace(b'\nFrom ', b'\n>From ') message = message.replace(b'\nFrom ', b'\n>From ')
message = message.replace(b'\n', linesep) message = message.replace(b'\n', linesep)
target.write(message) target.write(message)
if self._append_newline and not message.endswith(linesep):
# Make sure the message ends with a newline
target.write(linesep)
elif hasattr(message, 'read'): elif hasattr(message, 'read'):
if hasattr(message, 'buffer'): if hasattr(message, 'buffer'):
warnings.warn("Use of text mode files is deprecated, " warnings.warn("Use of text mode files is deprecated, "
"use a binary mode file instead", DeprecationWarning, 3) "use a binary mode file instead", DeprecationWarning, 3)
message = message.buffer message = message.buffer
lastline = None
while True: while True:
line = message.readline() line = message.readline()
# Universal newline support. # Universal newline support.
...@@ -248,6 +258,10 @@ class Mailbox: ...@@ -248,6 +258,10 @@ class Mailbox:
line = b'>From ' + line[5:] line = b'>From ' + line[5:]
line = line.replace(b'\n', linesep) line = line.replace(b'\n', linesep)
target.write(line) target.write(line)
lastline = line
if self._append_newline and lastline and not lastline.endswith(linesep):
# Make sure the message ends with a newline
target.write(linesep)
else: else:
raise TypeError('Invalid message type: %s' % type(message)) raise TypeError('Invalid message type: %s' % type(message))
...@@ -833,30 +847,48 @@ class mbox(_mboxMMDF): ...@@ -833,30 +847,48 @@ class mbox(_mboxMMDF):
_mangle_from_ = True _mangle_from_ = True
# All messages must end in a newline character, and
# _post_message_hooks outputs an empty line between messages.
_append_newline = True
def __init__(self, path, factory=None, create=True): def __init__(self, path, factory=None, create=True):
"""Initialize an mbox mailbox.""" """Initialize an mbox mailbox."""
self._message_factory = mboxMessage self._message_factory = mboxMessage
_mboxMMDF.__init__(self, path, factory, create) _mboxMMDF.__init__(self, path, factory, create)
def _pre_message_hook(self, f): def _post_message_hook(self, f):
"""Called before writing each message to file f.""" """Called after writing each message to file f."""
if f.tell() != 0:
f.write(linesep) f.write(linesep)
def _generate_toc(self): def _generate_toc(self):
"""Generate key-to-(start, stop) table of contents.""" """Generate key-to-(start, stop) table of contents."""
starts, stops = [], [] starts, stops = [], []
last_was_empty = False
self._file.seek(0) self._file.seek(0)
while True: while True:
line_pos = self._file.tell() line_pos = self._file.tell()
line = self._file.readline() line = self._file.readline()
if line.startswith(b'From '): if line.startswith(b'From '):
if len(stops) < len(starts): if len(stops) < len(starts):
if last_was_empty:
stops.append(line_pos - len(linesep)) stops.append(line_pos - len(linesep))
else:
# The last line before the "From " line wasn't
# blank, but we consider it a start of a
# message anyway.
stops.append(line_pos)
starts.append(line_pos) starts.append(line_pos)
last_was_empty = False
elif not line: elif not line:
if last_was_empty:
stops.append(line_pos - len(linesep))
else:
stops.append(line_pos) stops.append(line_pos)
break break
elif line == linesep:
last_was_empty = True
else:
last_was_empty = False
self._toc = dict(enumerate(zip(starts, stops))) self._toc = dict(enumerate(zip(starts, stops)))
self._next_key = len(self._toc) self._next_key = len(self._toc)
self._file_length = self._file.tell() self._file_length = self._file.tell()
......
...@@ -53,7 +53,7 @@ class TestMailbox(TestBase): ...@@ -53,7 +53,7 @@ class TestMailbox(TestBase):
maxDiff = None maxDiff = None
_factory = None # Overridden by subclasses to reuse tests _factory = None # Overridden by subclasses to reuse tests
_template = 'From: foo\n\n%s' _template = 'From: foo\n\n%s\n'
def setUp(self): def setUp(self):
self._path = support.TESTFN self._path = support.TESTFN
...@@ -232,7 +232,7 @@ class TestMailbox(TestBase): ...@@ -232,7 +232,7 @@ class TestMailbox(TestBase):
key0 = self._box.add(self._template % 0) key0 = self._box.add(self._template % 0)
msg = self._box.get(key0) msg = self._box.get(key0)
self.assertEqual(msg['from'], 'foo') self.assertEqual(msg['from'], 'foo')
self.assertEqual(msg.get_payload(), '0') self.assertEqual(msg.get_payload(), '0\n')
self.assertIs(self._box.get('foo'), None) self.assertIs(self._box.get('foo'), None)
self.assertIs(self._box.get('foo', False), False) self.assertIs(self._box.get('foo', False), False)
self._box.close() self._box.close()
...@@ -240,14 +240,14 @@ class TestMailbox(TestBase): ...@@ -240,14 +240,14 @@ class TestMailbox(TestBase):
key1 = self._box.add(self._template % 1) key1 = self._box.add(self._template % 1)
msg = self._box.get(key1) msg = self._box.get(key1)
self.assertEqual(msg['from'], 'foo') self.assertEqual(msg['from'], 'foo')
self.assertEqual(msg.get_payload(), '1') self.assertEqual(msg.get_payload(), '1\n')
def test_getitem(self): def test_getitem(self):
# Retrieve message using __getitem__() # Retrieve message using __getitem__()
key0 = self._box.add(self._template % 0) key0 = self._box.add(self._template % 0)
msg = self._box[key0] msg = self._box[key0]
self.assertEqual(msg['from'], 'foo') self.assertEqual(msg['from'], 'foo')
self.assertEqual(msg.get_payload(), '0') self.assertEqual(msg.get_payload(), '0\n')
self.assertRaises(KeyError, lambda: self._box['foo']) self.assertRaises(KeyError, lambda: self._box['foo'])
self._box.discard(key0) self._box.discard(key0)
self.assertRaises(KeyError, lambda: self._box[key0]) self.assertRaises(KeyError, lambda: self._box[key0])
...@@ -259,7 +259,7 @@ class TestMailbox(TestBase): ...@@ -259,7 +259,7 @@ class TestMailbox(TestBase):
msg0 = self._box.get_message(key0) msg0 = self._box.get_message(key0)
self.assertIsInstance(msg0, mailbox.Message) self.assertIsInstance(msg0, mailbox.Message)
self.assertEqual(msg0['from'], 'foo') self.assertEqual(msg0['from'], 'foo')
self.assertEqual(msg0.get_payload(), '0') self.assertEqual(msg0.get_payload(), '0\n')
self._check_sample(self._box.get_message(key1)) self._check_sample(self._box.get_message(key1))
def test_get_bytes(self): def test_get_bytes(self):
...@@ -432,15 +432,15 @@ class TestMailbox(TestBase): ...@@ -432,15 +432,15 @@ class TestMailbox(TestBase):
self.assertIn(key0, self._box) self.assertIn(key0, self._box)
key1 = self._box.add(self._template % 1) key1 = self._box.add(self._template % 1)
self.assertIn(key1, self._box) self.assertIn(key1, self._box)
self.assertEqual(self._box.pop(key0).get_payload(), '0') self.assertEqual(self._box.pop(key0).get_payload(), '0\n')
self.assertNotIn(key0, self._box) self.assertNotIn(key0, self._box)
self.assertIn(key1, self._box) self.assertIn(key1, self._box)
key2 = self._box.add(self._template % 2) key2 = self._box.add(self._template % 2)
self.assertIn(key2, self._box) self.assertIn(key2, self._box)
self.assertEqual(self._box.pop(key2).get_payload(), '2') self.assertEqual(self._box.pop(key2).get_payload(), '2\n')
self.assertNotIn(key2, self._box) self.assertNotIn(key2, self._box)
self.assertIn(key1, self._box) self.assertIn(key1, self._box)
self.assertEqual(self._box.pop(key1).get_payload(), '1') self.assertEqual(self._box.pop(key1).get_payload(), '1\n')
self.assertNotIn(key1, self._box) self.assertNotIn(key1, self._box)
self.assertEqual(len(self._box), 0) self.assertEqual(len(self._box), 0)
...@@ -635,7 +635,7 @@ class TestMaildir(TestMailbox, unittest.TestCase): ...@@ -635,7 +635,7 @@ class TestMaildir(TestMailbox, unittest.TestCase):
msg_returned = self._box.get_message(key) msg_returned = self._box.get_message(key)
self.assertEqual(msg_returned.get_subdir(), 'new') self.assertEqual(msg_returned.get_subdir(), 'new')
self.assertEqual(msg_returned.get_flags(), '') self.assertEqual(msg_returned.get_flags(), '')
self.assertEqual(msg_returned.get_payload(), '1') self.assertEqual(msg_returned.get_payload(), '1\n')
msg2 = mailbox.MaildirMessage(self._template % 2) msg2 = mailbox.MaildirMessage(self._template % 2)
msg2.set_info('2,S') msg2.set_info('2,S')
self._box[key] = msg2 self._box[key] = msg2
...@@ -643,7 +643,7 @@ class TestMaildir(TestMailbox, unittest.TestCase): ...@@ -643,7 +643,7 @@ class TestMaildir(TestMailbox, unittest.TestCase):
msg_returned = self._box.get_message(key) msg_returned = self._box.get_message(key)
self.assertEqual(msg_returned.get_subdir(), 'new') self.assertEqual(msg_returned.get_subdir(), 'new')
self.assertEqual(msg_returned.get_flags(), 'S') self.assertEqual(msg_returned.get_flags(), 'S')
self.assertEqual(msg_returned.get_payload(), '3') self.assertEqual(msg_returned.get_payload(), '3\n')
def test_consistent_factory(self): def test_consistent_factory(self):
# Add a message. # Add a message.
...@@ -996,20 +996,20 @@ class _TestMboxMMDF(_TestSingleFile): ...@@ -996,20 +996,20 @@ class _TestMboxMMDF(_TestSingleFile):
def test_add_from_string(self): def test_add_from_string(self):
# Add a string starting with 'From ' to the mailbox # Add a string starting with 'From ' to the mailbox
key = self._box.add('From foo@bar blah\nFrom: foo\n\n0') key = self._box.add('From foo@bar blah\nFrom: foo\n\n0\n')
self.assertEqual(self._box[key].get_from(), 'foo@bar blah') self.assertEqual(self._box[key].get_from(), 'foo@bar blah')
self.assertEqual(self._box[key].get_payload(), '0') self.assertEqual(self._box[key].get_payload(), '0\n')
def test_add_from_bytes(self): def test_add_from_bytes(self):
# Add a byte string starting with 'From ' to the mailbox # Add a byte string starting with 'From ' to the mailbox
key = self._box.add(b'From foo@bar blah\nFrom: foo\n\n0') key = self._box.add(b'From foo@bar blah\nFrom: foo\n\n0\n')
self.assertEqual(self._box[key].get_from(), 'foo@bar blah') self.assertEqual(self._box[key].get_from(), 'foo@bar blah')
self.assertEqual(self._box[key].get_payload(), '0') self.assertEqual(self._box[key].get_payload(), '0\n')
def test_add_mbox_or_mmdf_message(self): def test_add_mbox_or_mmdf_message(self):
# Add an mboxMessage or MMDFMessage # Add an mboxMessage or MMDFMessage
for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage): for class_ in (mailbox.mboxMessage, mailbox.MMDFMessage):
msg = class_('From foo@bar blah\nFrom: foo\n\n0') msg = class_('From foo@bar blah\nFrom: foo\n\n0\n')
key = self._box.add(msg) key = self._box.add(msg)
def test_open_close_open(self): def test_open_close_open(self):
...@@ -1116,6 +1116,29 @@ class TestMbox(_TestMboxMMDF, unittest.TestCase): ...@@ -1116,6 +1116,29 @@ class TestMbox(_TestMboxMMDF, unittest.TestCase):
perms = st.st_mode perms = st.st_mode
self.assertFalse((perms & 0o111)) # Execute bits should all be off. self.assertFalse((perms & 0o111)) # Execute bits should all be off.
def test_terminating_newline(self):
message = email.message.Message()
message['From'] = 'john@example.com'
message.set_payload('No newline at the end')
i = self._box.add(message)
# A newline should have been appended to the payload
message = self._box.get(i)
self.assertEqual(message.get_payload(), 'No newline at the end\n')
def test_message_separator(self):
# Check there's always a single blank line after each message
self._box.add('From: foo\n\n0') # No newline at the end
with open(self._path) as f:
data = f.read()
self.assertEqual(data[-3:], '0\n\n')
self._box.add('From: foo\n\n0\n') # Newline at the end
with open(self._path) as f:
data = f.read()
self.assertEqual(data[-3:], '0\n\n')
class TestMMDF(_TestMboxMMDF, unittest.TestCase): class TestMMDF(_TestMboxMMDF, unittest.TestCase):
_factory = lambda self, path, factory=None: mailbox.MMDF(path, factory) _factory = lambda self, path, factory=None: mailbox.MMDF(path, factory)
......
...@@ -33,6 +33,8 @@ Core and Builtins ...@@ -33,6 +33,8 @@ Core and Builtins
Library Library
------- -------
- Issue #15222: Insert blank line after each message in mbox mailboxes
- Issue #16013: Fix CSV Reader parsing issue with ending quote characters. - Issue #16013: Fix CSV Reader parsing issue with ending quote characters.
Patch by Serhiy Storchaka. Patch by Serhiy Storchaka.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment