Kaydet (Commit) 06ed218e authored tarafından R David Murray's avatar R David Murray

#20476: add a message_factory policy attribute to email.

üst 37df068e
...@@ -73,8 +73,9 @@ Here is the API for the :class:`BytesFeedParser`: ...@@ -73,8 +73,9 @@ Here is the API for the :class:`BytesFeedParser`:
.. class:: BytesFeedParser(_factory=None, *, policy=policy.compat32) .. class:: BytesFeedParser(_factory=None, *, policy=policy.compat32)
Create a :class:`BytesFeedParser` instance. Optional *_factory* is a Create a :class:`BytesFeedParser` instance. Optional *_factory* is a
no-argument callable; if not specified determine the default based on the no-argument callable; if not specified use the
*policy*. Call *_factory* whenever a new message object is needed. :attr:`~email.policy.Policy.message_factory` from the *policy*. Call
*_factory* whenever a new message object is needed.
If *policy* is specified use the rules it specifies to update the If *policy* is specified use the rules it specifies to update the
representation of the message. If *policy* is not set, use the representation of the message. If *policy* is not set, use the
...@@ -91,6 +92,7 @@ Here is the API for the :class:`BytesFeedParser`: ...@@ -91,6 +92,7 @@ Here is the API for the :class:`BytesFeedParser`:
.. versionadded:: 3.2 .. versionadded:: 3.2
.. versionchanged:: 3.3 Added the *policy* keyword. .. versionchanged:: 3.3 Added the *policy* keyword.
.. versionchanged:: 3.6 _factory defaults to the policy ``message_factory``.
.. method:: feed(data) .. method:: feed(data)
...@@ -146,6 +148,7 @@ message body, instead setting the payload to the raw body. ...@@ -146,6 +148,7 @@ message body, instead setting the payload to the raw body.
.. versionchanged:: 3.3 .. versionchanged:: 3.3
Removed the *strict* argument that was deprecated in 2.4. Added the Removed the *strict* argument that was deprecated in 2.4. Added the
*policy* keyword. *policy* keyword.
.. versionchanged:: 3.6 _class defaults to the policy ``message_factory``.
.. method:: parse(fp, headersonly=False) .. method:: parse(fp, headersonly=False)
...@@ -194,6 +197,7 @@ message body, instead setting the payload to the raw body. ...@@ -194,6 +197,7 @@ message body, instead setting the payload to the raw body.
.. versionchanged:: 3.3 .. versionchanged:: 3.3
Removed the *strict* argument. Added the *policy* keyword. Removed the *strict* argument. Added the *policy* keyword.
.. versionchanged:: 3.6 _class defaults to the policy ``message_factory``.
.. method:: parse(fp, headersonly=False) .. method:: parse(fp, headersonly=False)
...@@ -230,8 +234,7 @@ in the top-level :mod:`email` package namespace. ...@@ -230,8 +234,7 @@ in the top-level :mod:`email` package namespace.
.. currentmodule:: email .. currentmodule:: email
.. function:: message_from_bytes(s, _class=None, *, \ .. function:: message_from_bytes(s, _class=None, *, policy=policy.compat32)
policy=policy.compat32)
Return a message object structure from a :term:`bytes-like object`. This is Return a message object structure from a :term:`bytes-like object`. This is
equivalent to ``BytesParser().parsebytes(s)``. Optional *_class* and equivalent to ``BytesParser().parsebytes(s)``. Optional *_class* and
...@@ -243,7 +246,7 @@ in the top-level :mod:`email` package namespace. ...@@ -243,7 +246,7 @@ in the top-level :mod:`email` package namespace.
Removed the *strict* argument. Added the *policy* keyword. Removed the *strict* argument. Added the *policy* keyword.
.. function:: message_from_binary_file(fp, _class=None, *, \ .. function:: message_from_binary_file(fp, _class=None, *,
policy=policy.compat32) policy=policy.compat32)
Return a message object structure tree from an open binary :term:`file Return a message object structure tree from an open binary :term:`file
...@@ -256,8 +259,7 @@ in the top-level :mod:`email` package namespace. ...@@ -256,8 +259,7 @@ in the top-level :mod:`email` package namespace.
Removed the *strict* argument. Added the *policy* keyword. Removed the *strict* argument. Added the *policy* keyword.
.. function:: message_from_string(s, _class=None, *, \ .. function:: message_from_string(s, _class=None, *, policy=policy.compat32)
policy=policy.compat32)
Return a message object structure from a string. This is equivalent to Return a message object structure from a string. This is equivalent to
``Parser().parsestr(s)``. *_class* and *policy* are interpreted as ``Parser().parsestr(s)``. *_class* and *policy* are interpreted as
...@@ -267,8 +269,7 @@ in the top-level :mod:`email` package namespace. ...@@ -267,8 +269,7 @@ in the top-level :mod:`email` package namespace.
Removed the *strict* argument. Added the *policy* keyword. Removed the *strict* argument. Added the *policy* keyword.
.. function:: message_from_file(fp, _class=None, *, \ .. function:: message_from_file(fp, _class=None, *, policy=policy.compat32)
policy=policy.compat32)
Return a message object structure tree from an open :term:`file object`. Return a message object structure tree from an open :term:`file object`.
This is equivalent to ``Parser().parse(fp)``. *_class* and *policy* are This is equivalent to ``Parser().parse(fp)``. *_class* and *policy* are
...@@ -276,6 +277,7 @@ in the top-level :mod:`email` package namespace. ...@@ -276,6 +277,7 @@ in the top-level :mod:`email` package namespace.
.. versionchanged:: 3.3 .. versionchanged:: 3.3
Removed the *strict* argument. Added the *policy* keyword. Removed the *strict* argument. Added the *policy* keyword.
.. versionchanged:: 3.6 _class defaults to the policy ``message_factory``.
Here's an example of how you might use :func:`message_from_bytes` at an Here's an example of how you might use :func:`message_from_bytes` at an
......
...@@ -221,6 +221,14 @@ added matters. To illustrate:: ...@@ -221,6 +221,14 @@ added matters. To illustrate::
The *mangle_from_* parameter. The *mangle_from_* parameter.
.. attribute:: message_factory
A factory function for constructing a new empty message object. Used
by the parser when building messages. Defaults to
:class:`~email.message.Message`.
.. versionadded:: 3.6
The following :class:`Policy` method is intended to be called by code using The following :class:`Policy` method is intended to be called by code using
the email library to create policy instances with custom settings: the email library to create policy instances with custom settings:
...@@ -368,6 +376,9 @@ added matters. To illustrate:: ...@@ -368,6 +376,9 @@ added matters. To illustrate::
on the type of the field. The parsing and folding algorithm fully implement on the type of the field. The parsing and folding algorithm fully implement
:rfc:`2047` and :rfc:`5322`. :rfc:`2047` and :rfc:`5322`.
The default value for the :attr:`~email.policy.Policy.message_factory`
attribute is :class:`~email.message.EmailMessage`.
In addition to the settable attributes listed above that apply to all In addition to the settable attributes listed above that apply to all
policies, this policy adds the following additional attributes: policies, this policy adds the following additional attributes:
......
...@@ -598,6 +598,13 @@ The :mod:`email.mime` classes now all accept an optional *policy* keyword. ...@@ -598,6 +598,13 @@ The :mod:`email.mime` classes now all accept an optional *policy* keyword.
The :class:`~email.generator.DecodedGenerator` now supports the *policy* The :class:`~email.generator.DecodedGenerator` now supports the *policy*
keyword. keyword.
There is a new :mod:`~email.policy` attribute,
:attr:`~email.policy.Policy.message_factory`, that controls what class is used
by default when the parser creates new message objects. For the
:attr:`email.policy.compat32` policy this is :class:`~email.message.Message`,
for the new policies it is :class:`~email.message.EmailMessage`.
(Contributed by R. David Murray in :issue:`20476`.)
encodings encodings
--------- ---------
......
...@@ -154,6 +154,8 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta): ...@@ -154,6 +154,8 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta):
them. This is used when the message is being them. This is used when the message is being
serialized by a generator. Default: True. serialized by a generator. Default: True.
message_factory -- the class to use to create new message objects.
""" """
raise_on_defect = False raise_on_defect = False
...@@ -161,6 +163,8 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta): ...@@ -161,6 +163,8 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta):
cte_type = '8bit' cte_type = '8bit'
max_line_length = 78 max_line_length = 78
mangle_from_ = False mangle_from_ = False
# XXX To avoid circular imports, this is set in email.message.
message_factory = None
def handle_defect(self, obj, defect): def handle_defect(self, obj, defect):
"""Based on policy, either raise defect or call register_defect. """Based on policy, either raise defect or call register_defect.
......
...@@ -24,7 +24,6 @@ __all__ = ['FeedParser', 'BytesFeedParser'] ...@@ -24,7 +24,6 @@ __all__ = ['FeedParser', 'BytesFeedParser']
import re import re
from email import errors from email import errors
from email import message
from email._policybase import compat32 from email._policybase import compat32
from collections import deque from collections import deque
from io import StringIO from io import StringIO
...@@ -148,13 +147,7 @@ class FeedParser: ...@@ -148,13 +147,7 @@ class FeedParser:
self.policy = policy self.policy = policy
self._old_style_factory = False self._old_style_factory = False
if _factory is None: if _factory is None:
# What this should be: self._factory = policy.message_factory
#self._factory = policy.default_message_factory
# but, because we are post 3.4 feature freeze, fix with temp hack:
if self.policy is compat32:
self._factory = message.Message
else:
self._factory = message.EmailMessage
else: else:
self._factory = _factory self._factory = _factory
try: try:
......
...@@ -4,18 +4,17 @@ ...@@ -4,18 +4,17 @@
"""Basic message object for the email package object model.""" """Basic message object for the email package object model."""
__all__ = ['Message'] __all__ = ['Message', 'EmailMessage']
import re import re
import uu import uu
import quopri import quopri
import warnings
from io import BytesIO, StringIO from io import BytesIO, StringIO
# Intrapackage imports # Intrapackage imports
from email import utils from email import utils
from email import errors from email import errors
from email._policybase import compat32 from email._policybase import Policy, compat32
from email import charset as _charset from email import charset as _charset
from email._encoded_words import decode_b from email._encoded_words import decode_b
Charset = _charset.Charset Charset = _charset.Charset
...@@ -1163,3 +1162,6 @@ class EmailMessage(MIMEPart): ...@@ -1163,3 +1162,6 @@ class EmailMessage(MIMEPart):
super().set_content(*args, **kw) super().set_content(*args, **kw)
if 'MIME-Version' not in self: if 'MIME-Version' not in self:
self['MIME-Version'] = '1.0' self['MIME-Version'] = '1.0'
# Set message_factory on Policy here to avoid a circular import.
Policy.message_factory = Message
...@@ -7,6 +7,7 @@ from email._policybase import Policy, Compat32, compat32, _extend_docstrings ...@@ -7,6 +7,7 @@ from email._policybase import Policy, Compat32, compat32, _extend_docstrings
from email.utils import _has_surrogates from email.utils import _has_surrogates
from email.headerregistry import HeaderRegistry as HeaderRegistry from email.headerregistry import HeaderRegistry as HeaderRegistry
from email.contentmanager import raw_data_manager from email.contentmanager import raw_data_manager
from email.message import EmailMessage
__all__ = [ __all__ = [
'Compat32', 'Compat32',
...@@ -82,6 +83,7 @@ class EmailPolicy(Policy): ...@@ -82,6 +83,7 @@ class EmailPolicy(Policy):
""" """
message_factory = EmailMessage
utf8 = False utf8 = False
refold_source = 'long' refold_source = 'long'
header_factory = HeaderRegistry() header_factory = HeaderRegistry()
......
import io import io
import email import email
import unittest import unittest
from email.message import Message from email.message import Message, EmailMessage
from email.policy import default from email.policy import default
from test.test_email import TestEmailBase from test.test_email import TestEmailBase
...@@ -39,38 +39,71 @@ class TestParserBase: ...@@ -39,38 +39,71 @@ class TestParserBase:
# The unicode line splitter splits on unicode linebreaks, which are # The unicode line splitter splits on unicode linebreaks, which are
# more numerous than allowed by the email RFCs; make sure we are only # more numerous than allowed by the email RFCs; make sure we are only
# splitting on those two. # splitting on those two.
msg = self.parser( for parser in self.parsers:
"Next-Line: not\x85broken\r\n" with self.subTest(parser=parser.__name__):
"Null: not\x00broken\r\n" msg = parser(
"Vertical-Tab: not\vbroken\r\n" "Next-Line: not\x85broken\r\n"
"Form-Feed: not\fbroken\r\n" "Null: not\x00broken\r\n"
"File-Separator: not\x1Cbroken\r\n" "Vertical-Tab: not\vbroken\r\n"
"Group-Separator: not\x1Dbroken\r\n" "Form-Feed: not\fbroken\r\n"
"Record-Separator: not\x1Ebroken\r\n" "File-Separator: not\x1Cbroken\r\n"
"Line-Separator: not\u2028broken\r\n" "Group-Separator: not\x1Dbroken\r\n"
"Paragraph-Separator: not\u2029broken\r\n" "Record-Separator: not\x1Ebroken\r\n"
"\r\n", "Line-Separator: not\u2028broken\r\n"
policy=default, "Paragraph-Separator: not\u2029broken\r\n"
) "\r\n",
self.assertEqual(msg.items(), [ policy=default,
("Next-Line", "not\x85broken"), )
("Null", "not\x00broken"), self.assertEqual(msg.items(), [
("Vertical-Tab", "not\vbroken"), ("Next-Line", "not\x85broken"),
("Form-Feed", "not\fbroken"), ("Null", "not\x00broken"),
("File-Separator", "not\x1Cbroken"), ("Vertical-Tab", "not\vbroken"),
("Group-Separator", "not\x1Dbroken"), ("Form-Feed", "not\fbroken"),
("Record-Separator", "not\x1Ebroken"), ("File-Separator", "not\x1Cbroken"),
("Line-Separator", "not\u2028broken"), ("Group-Separator", "not\x1Dbroken"),
("Paragraph-Separator", "not\u2029broken"), ("Record-Separator", "not\x1Ebroken"),
]) ("Line-Separator", "not\u2028broken"),
self.assertEqual(msg.get_payload(), "") ("Paragraph-Separator", "not\u2029broken"),
])
self.assertEqual(msg.get_payload(), "")
class MyMessage(EmailMessage):
pass
def test_custom_message_factory_on_policy(self):
for parser in self.parsers:
with self.subTest(parser=parser.__name__):
MyPolicy = default.clone(message_factory=self.MyMessage)
msg = parser("To: foo\n\ntest", policy=MyPolicy)
self.assertIsInstance(msg, self.MyMessage)
def test_factory_arg_overrides_policy(self):
for parser in self.parsers:
with self.subTest(parser=parser.__name__):
MyPolicy = default.clone(message_factory=self.MyMessage)
msg = parser("To: foo\n\ntest", Message, policy=MyPolicy)
self.assertNotIsInstance(msg, self.MyMessage)
self.assertIsInstance(msg, Message)
# Play some games to get nice output in subTest. This code could be clearer
# if staticmethod supported __name__.
def message_from_file(s, *args, **kw):
f = io.StringIO(s)
return email.message_from_file(f, *args, **kw)
class TestParser(TestParserBase, TestEmailBase): class TestParser(TestParserBase, TestEmailBase):
parser = staticmethod(email.message_from_string) parsers = (email.message_from_string, message_from_file)
def message_from_bytes(s, *args, **kw):
return email.message_from_bytes(s.encode(), *args, **kw)
def message_from_binary_file(s, *args, **kw):
f = io.BytesIO(s.encode())
return email.message_from_binary_file(f, *args, **kw)
class TestBytesParser(TestParserBase, TestEmailBase): class TestBytesParser(TestParserBase, TestEmailBase):
def parser(self, s, *args, **kw): parsers = (message_from_bytes, message_from_binary_file)
return email.message_from_bytes(s.encode(), *args, **kw)
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -5,6 +5,7 @@ import unittest ...@@ -5,6 +5,7 @@ import unittest
import email.policy import email.policy
import email.parser import email.parser
import email.generator import email.generator
import email.message
from email import headerregistry from email import headerregistry
def make_defaults(base_defaults, differences): def make_defaults(base_defaults, differences):
...@@ -23,6 +24,7 @@ class PolicyAPITests(unittest.TestCase): ...@@ -23,6 +24,7 @@ class PolicyAPITests(unittest.TestCase):
'cte_type': '8bit', 'cte_type': '8bit',
'raise_on_defect': False, 'raise_on_defect': False,
'mangle_from_': True, 'mangle_from_': True,
'message_factory': email.message.Message,
} }
# These default values are the ones set on email.policy.default. # These default values are the ones set on email.policy.default.
# If any of these defaults change, the docs must be updated. # If any of these defaults change, the docs must be updated.
...@@ -34,6 +36,7 @@ class PolicyAPITests(unittest.TestCase): ...@@ -34,6 +36,7 @@ class PolicyAPITests(unittest.TestCase):
'refold_source': 'long', 'refold_source': 'long',
'content_manager': email.policy.EmailPolicy.content_manager, 'content_manager': email.policy.EmailPolicy.content_manager,
'mangle_from_': False, 'mangle_from_': False,
'message_factory': email.message.EmailMessage,
}) })
# For each policy under test, we give here what we expect the defaults to # For each policy under test, we give here what we expect the defaults to
...@@ -62,20 +65,22 @@ class PolicyAPITests(unittest.TestCase): ...@@ -62,20 +65,22 @@ class PolicyAPITests(unittest.TestCase):
def test_defaults(self): def test_defaults(self):
for policy, expected in self.policies.items(): for policy, expected in self.policies.items():
for attr, value in expected.items(): for attr, value in expected.items():
self.assertEqual(getattr(policy, attr), value, with self.subTest(policy=policy, attr=attr):
("change {} docs/docstrings if defaults have " self.assertEqual(getattr(policy, attr), value,
"changed").format(policy)) ("change {} docs/docstrings if defaults have "
"changed").format(policy))
def test_all_attributes_covered(self): def test_all_attributes_covered(self):
for policy, expected in self.policies.items(): for policy, expected in self.policies.items():
for attr in dir(policy): for attr in dir(policy):
if (attr.startswith('_') or with self.subTest(policy=policy, attr=attr):
isinstance(getattr(email.policy.EmailPolicy, attr), if (attr.startswith('_') or
types.FunctionType)): isinstance(getattr(email.policy.EmailPolicy, attr),
continue types.FunctionType)):
else: continue
self.assertIn(attr, expected, else:
"{} is not fully tested".format(attr)) self.assertIn(attr, expected,
"{} is not fully tested".format(attr))
def test_abc(self): def test_abc(self):
with self.assertRaises(TypeError) as cm: with self.assertRaises(TypeError) as cm:
...@@ -237,6 +242,9 @@ class PolicyAPITests(unittest.TestCase): ...@@ -237,6 +242,9 @@ class PolicyAPITests(unittest.TestCase):
# wins), but that the order still works (right overrides left). # wins), but that the order still works (right overrides left).
class TestException(Exception):
pass
class TestPolicyPropagation(unittest.TestCase): class TestPolicyPropagation(unittest.TestCase):
# The abstract methods are used by the parser but not by the wrapper # The abstract methods are used by the parser but not by the wrapper
...@@ -244,40 +252,40 @@ class TestPolicyPropagation(unittest.TestCase): ...@@ -244,40 +252,40 @@ class TestPolicyPropagation(unittest.TestCase):
# policy was actually propagated all the way to feedparser. # policy was actually propagated all the way to feedparser.
class MyPolicy(email.policy.Policy): class MyPolicy(email.policy.Policy):
def badmethod(self, *args, **kw): def badmethod(self, *args, **kw):
raise Exception("test") raise TestException("test")
fold = fold_binary = header_fetch_parser = badmethod fold = fold_binary = header_fetch_parser = badmethod
header_source_parse = header_store_parse = badmethod header_source_parse = header_store_parse = badmethod
def test_message_from_string(self): def test_message_from_string(self):
with self.assertRaisesRegex(Exception, "^test$"): with self.assertRaisesRegex(TestException, "^test$"):
email.message_from_string("Subject: test\n\n", email.message_from_string("Subject: test\n\n",
policy=self.MyPolicy) policy=self.MyPolicy)
def test_message_from_bytes(self): def test_message_from_bytes(self):
with self.assertRaisesRegex(Exception, "^test$"): with self.assertRaisesRegex(TestException, "^test$"):
email.message_from_bytes(b"Subject: test\n\n", email.message_from_bytes(b"Subject: test\n\n",
policy=self.MyPolicy) policy=self.MyPolicy)
def test_message_from_file(self): def test_message_from_file(self):
f = io.StringIO('Subject: test\n\n') f = io.StringIO('Subject: test\n\n')
with self.assertRaisesRegex(Exception, "^test$"): with self.assertRaisesRegex(TestException, "^test$"):
email.message_from_file(f, policy=self.MyPolicy) email.message_from_file(f, policy=self.MyPolicy)
def test_message_from_binary_file(self): def test_message_from_binary_file(self):
f = io.BytesIO(b'Subject: test\n\n') f = io.BytesIO(b'Subject: test\n\n')
with self.assertRaisesRegex(Exception, "^test$"): with self.assertRaisesRegex(TestException, "^test$"):
email.message_from_binary_file(f, policy=self.MyPolicy) email.message_from_binary_file(f, policy=self.MyPolicy)
# These are redundant, but we need them for black-box completeness. # These are redundant, but we need them for black-box completeness.
def test_parser(self): def test_parser(self):
p = email.parser.Parser(policy=self.MyPolicy) p = email.parser.Parser(policy=self.MyPolicy)
with self.assertRaisesRegex(Exception, "^test$"): with self.assertRaisesRegex(TestException, "^test$"):
p.parsestr('Subject: test\n\n') p.parsestr('Subject: test\n\n')
def test_bytes_parser(self): def test_bytes_parser(self):
p = email.parser.BytesParser(policy=self.MyPolicy) p = email.parser.BytesParser(policy=self.MyPolicy)
with self.assertRaisesRegex(Exception, "^test$"): with self.assertRaisesRegex(TestException, "^test$"):
p.parsebytes(b'Subject: test\n\n') p.parsebytes(b'Subject: test\n\n')
# Now that we've established that all the parse methods get the # Now that we've established that all the parse methods get the
......
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