Commit 33677207 authored by Giampaolo Rodola's avatar Giampaolo Rodola

merge 3.3

parents 4db91267 253b40bd
...@@ -1832,6 +1832,15 @@ Deprecated Python modules, functions and methods ...@@ -1832,6 +1832,15 @@ Deprecated Python modules, functions and methods
* :class:`abc.abstractstaticmethod` has been deprecated, use * :class:`abc.abstractstaticmethod` has been deprecated, use
:class:`staticmethod` with :func:`abc.abstractmethod` instead. :class:`staticmethod` with :func:`abc.abstractmethod` instead.
* :mod:`imoprtlib` package:
* :meth:`importlib.abc.SourceLoader.path_mtime` is now deprecated in favour of
:meth:`importlib.abc.SourceLoader.path_stats` as bytecode files now store
both the modification time and size of the source file the bytecode file was
compiled from.
Deprecated functions and types of the C API Deprecated functions and types of the C API
...@@ -1963,11 +1972,6 @@ Porting Python code ...@@ -1963,11 +1972,6 @@ Porting Python code
:attr:`sys.path_importer_cache` where it repesents the use of implicit :attr:`sys.path_importer_cache` where it repesents the use of implicit
finders, but semantically it should not change anything. finders, but semantically it should not change anything.
* :meth:`importlib.abc.SourceLoader.path_mtime` is now deprecated in favour of
:meth:`importlib.abc.SourceLoader.path_stats` as bytecode files now store
both the modification time and size of the source file the bytecode file was
compiled from.
* :class:`importlib.abc.Finder` no longer specifies a `find_module()` abstract * :class:`importlib.abc.Finder` no longer specifies a `find_module()` abstract
method that must be implemented. If you were relying on subclasses to method that must be implemented. If you were relying on subclasses to
implement that method, make sure to check for the method's existence first. implement that method, make sure to check for the method's existence first.
......
...@@ -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):
stops.append(line_pos - len(linesep)) if last_was_empty:
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:
stops.append(line_pos) if last_was_empty:
stops.append(line_pos - len(linesep))
else:
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