Skip to content

Commit 06ed218

Browse files
committed
python#20476: add a message_factory policy attribute to email.
1 parent 37df068 commit 06ed218

File tree

9 files changed

+128
-66
lines changed

9 files changed

+128
-66
lines changed

Doc/library/email.parser.rst

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ Here is the API for the :class:`BytesFeedParser`:
7373
.. class:: BytesFeedParser(_factory=None, *, policy=policy.compat32)
7474

7575
Create a :class:`BytesFeedParser` instance. Optional *_factory* is a
76-
no-argument callable; if not specified determine the default based on the
77-
*policy*. Call *_factory* whenever a new message object is needed.
76+
no-argument callable; if not specified use the
77+
:attr:`~email.policy.Policy.message_factory` from the *policy*. Call
78+
*_factory* whenever a new message object is needed.
7879

7980
If *policy* is specified use the rules it specifies to update the
8081
representation of the message. If *policy* is not set, use the
@@ -91,6 +92,7 @@ Here is the API for the :class:`BytesFeedParser`:
9192
.. versionadded:: 3.2
9293

9394
.. versionchanged:: 3.3 Added the *policy* keyword.
95+
.. versionchanged:: 3.6 _factory defaults to the policy ``message_factory``.
9496

9597

9698
.. method:: feed(data)
@@ -146,6 +148,7 @@ message body, instead setting the payload to the raw body.
146148
.. versionchanged:: 3.3
147149
Removed the *strict* argument that was deprecated in 2.4. Added the
148150
*policy* keyword.
151+
.. versionchanged:: 3.6 _class defaults to the policy ``message_factory``.
149152

150153

151154
.. method:: parse(fp, headersonly=False)
@@ -194,6 +197,7 @@ message body, instead setting the payload to the raw body.
194197

195198
.. versionchanged:: 3.3
196199
Removed the *strict* argument. Added the *policy* keyword.
200+
.. versionchanged:: 3.6 _class defaults to the policy ``message_factory``.
197201

198202

199203
.. method:: parse(fp, headersonly=False)
@@ -230,8 +234,7 @@ in the top-level :mod:`email` package namespace.
230234
.. currentmodule:: email
231235

232236

233-
.. function:: message_from_bytes(s, _class=None, *, \
234-
policy=policy.compat32)
237+
.. function:: message_from_bytes(s, _class=None, *, policy=policy.compat32)
235238

236239
Return a message object structure from a :term:`bytes-like object`. This is
237240
equivalent to ``BytesParser().parsebytes(s)``. Optional *_class* and
@@ -243,7 +246,7 @@ in the top-level :mod:`email` package namespace.
243246
Removed the *strict* argument. Added the *policy* keyword.
244247

245248

246-
.. function:: message_from_binary_file(fp, _class=None, *, \
249+
.. function:: message_from_binary_file(fp, _class=None, *,
247250
policy=policy.compat32)
248251

249252
Return a message object structure tree from an open binary :term:`file
@@ -256,8 +259,7 @@ in the top-level :mod:`email` package namespace.
256259
Removed the *strict* argument. Added the *policy* keyword.
257260

258261

259-
.. function:: message_from_string(s, _class=None, *, \
260-
policy=policy.compat32)
262+
.. function:: message_from_string(s, _class=None, *, policy=policy.compat32)
261263

262264
Return a message object structure from a string. This is equivalent to
263265
``Parser().parsestr(s)``. *_class* and *policy* are interpreted as
@@ -267,15 +269,15 @@ in the top-level :mod:`email` package namespace.
267269
Removed the *strict* argument. Added the *policy* keyword.
268270

269271

270-
.. function:: message_from_file(fp, _class=None, *, \
271-
policy=policy.compat32)
272+
.. function:: message_from_file(fp, _class=None, *, policy=policy.compat32)
272273

273274
Return a message object structure tree from an open :term:`file object`.
274275
This is equivalent to ``Parser().parse(fp)``. *_class* and *policy* are
275276
interpreted as with the :class:`~email.parser.Parser` class constructor.
276277

277278
.. versionchanged:: 3.3
278279
Removed the *strict* argument. Added the *policy* keyword.
280+
.. versionchanged:: 3.6 _class defaults to the policy ``message_factory``.
279281

280282

281283
Here's an example of how you might use :func:`message_from_bytes` at an

Doc/library/email.policy.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ added matters. To illustrate::
221221
The *mangle_from_* parameter.
222222

223223

224+
.. attribute:: message_factory
225+
226+
A factory function for constructing a new empty message object. Used
227+
by the parser when building messages. Defaults to
228+
:class:`~email.message.Message`.
229+
230+
.. versionadded:: 3.6
231+
224232
The following :class:`Policy` method is intended to be called by code using
225233
the email library to create policy instances with custom settings:
226234

@@ -368,6 +376,9 @@ added matters. To illustrate::
368376
on the type of the field. The parsing and folding algorithm fully implement
369377
:rfc:`2047` and :rfc:`5322`.
370378

379+
The default value for the :attr:`~email.policy.Policy.message_factory`
380+
attribute is :class:`~email.message.EmailMessage`.
381+
371382
In addition to the settable attributes listed above that apply to all
372383
policies, this policy adds the following additional attributes:
373384

Doc/whatsnew/3.6.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,13 @@ The :mod:`email.mime` classes now all accept an optional *policy* keyword.
598598
The :class:`~email.generator.DecodedGenerator` now supports the *policy*
599599
keyword.
600600

601+
There is a new :mod:`~email.policy` attribute,
602+
:attr:`~email.policy.Policy.message_factory`, that controls what class is used
603+
by default when the parser creates new message objects. For the
604+
:attr:`email.policy.compat32` policy this is :class:`~email.message.Message`,
605+
for the new policies it is :class:`~email.message.EmailMessage`.
606+
(Contributed by R. David Murray in :issue:`20476`.)
607+
601608

602609
encodings
603610
---------

Lib/email/_policybase.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,17 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta):
154154
them. This is used when the message is being
155155
serialized by a generator. Default: True.
156156
157+
message_factory -- the class to use to create new message objects.
158+
157159
"""
158160

159161
raise_on_defect = False
160162
linesep = '\n'
161163
cte_type = '8bit'
162164
max_line_length = 78
163165
mangle_from_ = False
166+
# XXX To avoid circular imports, this is set in email.message.
167+
message_factory = None
164168

165169
def handle_defect(self, obj, defect):
166170
"""Based on policy, either raise defect or call register_defect.

Lib/email/feedparser.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import re
2525

2626
from email import errors
27-
from email import message
2827
from email._policybase import compat32
2928
from collections import deque
3029
from io import StringIO
@@ -148,13 +147,7 @@ def __init__(self, _factory=None, *, policy=compat32):
148147
self.policy = policy
149148
self._old_style_factory = False
150149
if _factory is None:
151-
# What this should be:
152-
#self._factory = policy.default_message_factory
153-
# but, because we are post 3.4 feature freeze, fix with temp hack:
154-
if self.policy is compat32:
155-
self._factory = message.Message
156-
else:
157-
self._factory = message.EmailMessage
150+
self._factory = policy.message_factory
158151
else:
159152
self._factory = _factory
160153
try:

Lib/email/message.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@
44

55
"""Basic message object for the email package object model."""
66

7-
__all__ = ['Message']
7+
__all__ = ['Message', 'EmailMessage']
88

99
import re
1010
import uu
1111
import quopri
12-
import warnings
1312
from io import BytesIO, StringIO
1413

1514
# Intrapackage imports
1615
from email import utils
1716
from email import errors
18-
from email._policybase import compat32
17+
from email._policybase import Policy, compat32
1918
from email import charset as _charset
2019
from email._encoded_words import decode_b
2120
Charset = _charset.Charset
@@ -1163,3 +1162,6 @@ def set_content(self, *args, **kw):
11631162
super().set_content(*args, **kw)
11641163
if 'MIME-Version' not in self:
11651164
self['MIME-Version'] = '1.0'
1165+
1166+
# Set message_factory on Policy here to avoid a circular import.
1167+
Policy.message_factory = Message

Lib/email/policy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from email.utils import _has_surrogates
88
from email.headerregistry import HeaderRegistry as HeaderRegistry
99
from email.contentmanager import raw_data_manager
10+
from email.message import EmailMessage
1011

1112
__all__ = [
1213
'Compat32',
@@ -82,6 +83,7 @@ class EmailPolicy(Policy):
8283
8384
"""
8485

86+
message_factory = EmailMessage
8587
utf8 = False
8688
refold_source = 'long'
8789
header_factory = HeaderRegistry()

Lib/test/test_email/test_parser.py

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import io
22
import email
33
import unittest
4-
from email.message import Message
4+
from email.message import Message, EmailMessage
55
from email.policy import default
66
from test.test_email import TestEmailBase
77

@@ -39,38 +39,71 @@ def test_only_split_on_cr_lf(self):
3939
# The unicode line splitter splits on unicode linebreaks, which are
4040
# more numerous than allowed by the email RFCs; make sure we are only
4141
# splitting on those two.
42-
msg = self.parser(
43-
"Next-Line: not\x85broken\r\n"
44-
"Null: not\x00broken\r\n"
45-
"Vertical-Tab: not\vbroken\r\n"
46-
"Form-Feed: not\fbroken\r\n"
47-
"File-Separator: not\x1Cbroken\r\n"
48-
"Group-Separator: not\x1Dbroken\r\n"
49-
"Record-Separator: not\x1Ebroken\r\n"
50-
"Line-Separator: not\u2028broken\r\n"
51-
"Paragraph-Separator: not\u2029broken\r\n"
52-
"\r\n",
53-
policy=default,
54-
)
55-
self.assertEqual(msg.items(), [
56-
("Next-Line", "not\x85broken"),
57-
("Null", "not\x00broken"),
58-
("Vertical-Tab", "not\vbroken"),
59-
("Form-Feed", "not\fbroken"),
60-
("File-Separator", "not\x1Cbroken"),
61-
("Group-Separator", "not\x1Dbroken"),
62-
("Record-Separator", "not\x1Ebroken"),
63-
("Line-Separator", "not\u2028broken"),
64-
("Paragraph-Separator", "not\u2029broken"),
65-
])
66-
self.assertEqual(msg.get_payload(), "")
42+
for parser in self.parsers:
43+
with self.subTest(parser=parser.__name__):
44+
msg = parser(
45+
"Next-Line: not\x85broken\r\n"
46+
"Null: not\x00broken\r\n"
47+
"Vertical-Tab: not\vbroken\r\n"
48+
"Form-Feed: not\fbroken\r\n"
49+
"File-Separator: not\x1Cbroken\r\n"
50+
"Group-Separator: not\x1Dbroken\r\n"
51+
"Record-Separator: not\x1Ebroken\r\n"
52+
"Line-Separator: not\u2028broken\r\n"
53+
"Paragraph-Separator: not\u2029broken\r\n"
54+
"\r\n",
55+
policy=default,
56+
)
57+
self.assertEqual(msg.items(), [
58+
("Next-Line", "not\x85broken"),
59+
("Null", "not\x00broken"),
60+
("Vertical-Tab", "not\vbroken"),
61+
("Form-Feed", "not\fbroken"),
62+
("File-Separator", "not\x1Cbroken"),
63+
("Group-Separator", "not\x1Dbroken"),
64+
("Record-Separator", "not\x1Ebroken"),
65+
("Line-Separator", "not\u2028broken"),
66+
("Paragraph-Separator", "not\u2029broken"),
67+
])
68+
self.assertEqual(msg.get_payload(), "")
69+
70+
class MyMessage(EmailMessage):
71+
pass
72+
73+
def test_custom_message_factory_on_policy(self):
74+
for parser in self.parsers:
75+
with self.subTest(parser=parser.__name__):
76+
MyPolicy = default.clone(message_factory=self.MyMessage)
77+
msg = parser("To: foo\n\ntest", policy=MyPolicy)
78+
self.assertIsInstance(msg, self.MyMessage)
79+
80+
def test_factory_arg_overrides_policy(self):
81+
for parser in self.parsers:
82+
with self.subTest(parser=parser.__name__):
83+
MyPolicy = default.clone(message_factory=self.MyMessage)
84+
msg = parser("To: foo\n\ntest", Message, policy=MyPolicy)
85+
self.assertNotIsInstance(msg, self.MyMessage)
86+
self.assertIsInstance(msg, Message)
87+
88+
# Play some games to get nice output in subTest. This code could be clearer
89+
# if staticmethod supported __name__.
90+
91+
def message_from_file(s, *args, **kw):
92+
f = io.StringIO(s)
93+
return email.message_from_file(f, *args, **kw)
6794

6895
class TestParser(TestParserBase, TestEmailBase):
69-
parser = staticmethod(email.message_from_string)
96+
parsers = (email.message_from_string, message_from_file)
97+
98+
def message_from_bytes(s, *args, **kw):
99+
return email.message_from_bytes(s.encode(), *args, **kw)
100+
101+
def message_from_binary_file(s, *args, **kw):
102+
f = io.BytesIO(s.encode())
103+
return email.message_from_binary_file(f, *args, **kw)
70104

71105
class TestBytesParser(TestParserBase, TestEmailBase):
72-
def parser(self, s, *args, **kw):
73-
return email.message_from_bytes(s.encode(), *args, **kw)
106+
parsers = (message_from_bytes, message_from_binary_file)
74107

75108

76109
if __name__ == '__main__':

0 commit comments

Comments
 (0)