Skip to content

Commit 1be413e

Browse files
committed
Don't use metaclasses when class decorators can do the job.
Thanks to Nick Coghlan for pointing out that I'd forgotten about class decorators.
1 parent 8e0ed33 commit 1be413e

6 files changed

Lines changed: 66 additions & 67 deletions

File tree

Lib/email/_policybase.py

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -91,31 +91,25 @@ def __add__(self, other):
9191
return self.clone(**other.__dict__)
9292

9393

94-
# Conceptually this isn't a subclass of ABCMeta, but since we want Policy to
95-
# use ABCMeta as a metaclass *and* we want it to use this one as well, we have
96-
# to make this one a subclas of ABCMeta.
97-
class _DocstringExtenderMetaclass(abc.ABCMeta):
98-
99-
def __new__(meta, classname, bases, classdict):
100-
if classdict.get('__doc__') and classdict['__doc__'].startswith('+'):
101-
classdict['__doc__'] = meta._append_doc(bases[0].__doc__,
102-
classdict['__doc__'])
103-
for name, attr in classdict.items():
104-
if attr.__doc__ and attr.__doc__.startswith('+'):
105-
for cls in (cls for base in bases for cls in base.mro()):
106-
doc = getattr(getattr(cls, name), '__doc__')
107-
if doc:
108-
attr.__doc__ = meta._append_doc(doc, attr.__doc__)
109-
break
110-
return super().__new__(meta, classname, bases, classdict)
111-
112-
@staticmethod
113-
def _append_doc(doc, added_doc):
114-
added_doc = added_doc.split('\n', 1)[1]
115-
return doc + '\n' + added_doc
116-
117-
118-
class Policy(_PolicyBase, metaclass=_DocstringExtenderMetaclass):
94+
def _append_doc(doc, added_doc):
95+
doc = doc.rsplit('\n', 1)[0]
96+
added_doc = added_doc.split('\n', 1)[1]
97+
return doc + '\n' + added_doc
98+
99+
def _extend_docstrings(cls):
100+
if cls.__doc__ and cls.__doc__.startswith('+'):
101+
cls.__doc__ = _append_doc(cls.__bases__[0].__doc__, cls.__doc__)
102+
for name, attr in cls.__dict__.items():
103+
if attr.__doc__ and attr.__doc__.startswith('+'):
104+
for c in (c for base in cls.__bases__ for c in base.mro()):
105+
doc = getattr(getattr(c, name), '__doc__')
106+
if doc:
107+
attr.__doc__ = _append_doc(doc, attr.__doc__)
108+
break
109+
return cls
110+
111+
112+
class Policy(_PolicyBase, metaclass=abc.ABCMeta):
119113

120114
r"""Controls for how messages are interpreted and formatted.
121115
@@ -264,6 +258,7 @@ def fold_binary(self, name, value):
264258
raise NotImplementedError
265259

266260

261+
@_extend_docstrings
267262
class Compat32(Policy):
268263

269264
"""+

Lib/email/policy.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
code that adds all the email6 features.
33
"""
44

5-
from email._policybase import Policy, Compat32, compat32
5+
from email._policybase import Policy, Compat32, compat32, _extend_docstrings
66
from email.utils import _has_surrogates
77
from email.headerregistry import HeaderRegistry as HeaderRegistry
88

@@ -17,6 +17,7 @@
1717
'HTTP',
1818
]
1919

20+
@_extend_docstrings
2021
class EmailPolicy(Policy):
2122

2223
"""+

Lib/test/test_email/__init__.py

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,8 @@ def assertDefectsEqual(self, actual, expected):
7373
'item {}'.format(i))
7474

7575

76-
# Metaclass to allow for parameterized tests
77-
class Parameterized(type):
78-
79-
"""Provide a test method parameterization facility.
76+
def parameterize(cls):
77+
"""A test method parameterization class decorator.
8078
8179
Parameters are specified as the value of a class attribute that ends with
8280
the string '_params'. Call the portion before '_params' the prefix. Then
@@ -92,9 +90,10 @@ class Parameterized(type):
9290
In a _params dictioanry, the keys become part of the name of the generated
9391
tests. In a _params list, the values in the list are converted into a
9492
string by joining the string values of the elements of the tuple by '_' and
95-
converting any blanks into '_'s, and this become part of the name. The
96-
full name of a generated test is the portion of the _params name before the
97-
'_params' portion, plus an '_', plus the name derived as explained above.
93+
converting any blanks into '_'s, and this become part of the name.
94+
The full name of a generated test is a 'test_' prefix, the portion of the
95+
test function name after the '_as_' separator, plus an '_', plus the name
96+
derived as explained above.
9897
9998
For example, if we have:
10099
@@ -123,30 +122,29 @@ def example_as_myfunc_input(self, name, count):
123122
be used to select the test individually from the unittest command line.
124123
125124
"""
126-
127-
def __new__(meta, classname, bases, classdict):
128-
paramdicts = {}
129-
for name, attr in classdict.items():
130-
if name.endswith('_params'):
131-
if not hasattr(attr, 'keys'):
132-
d = {}
133-
for x in attr:
134-
if not hasattr(x, '__iter__'):
135-
x = (x,)
136-
n = '_'.join(str(v) for v in x).replace(' ', '_')
137-
d[n] = x
138-
attr = d
139-
paramdicts[name[:-7] + '_as_'] = attr
140-
testfuncs = {}
141-
for name, attr in classdict.items():
142-
for paramsname, paramsdict in paramdicts.items():
143-
if name.startswith(paramsname):
144-
testnameroot = 'test_' + name[len(paramsname):]
145-
for paramname, params in paramsdict.items():
146-
test = (lambda self, name=name, params=params:
147-
getattr(self, name)(*params))
148-
testname = testnameroot + '_' + paramname
149-
test.__name__ = testname
150-
testfuncs[testname] = test
151-
classdict.update(testfuncs)
152-
return super().__new__(meta, classname, bases, classdict)
125+
paramdicts = {}
126+
for name, attr in cls.__dict__.items():
127+
if name.endswith('_params'):
128+
if not hasattr(attr, 'keys'):
129+
d = {}
130+
for x in attr:
131+
if not hasattr(x, '__iter__'):
132+
x = (x,)
133+
n = '_'.join(str(v) for v in x).replace(' ', '_')
134+
d[n] = x
135+
attr = d
136+
paramdicts[name[:-7] + '_as_'] = attr
137+
testfuncs = {}
138+
for name, attr in cls.__dict__.items():
139+
for paramsname, paramsdict in paramdicts.items():
140+
if name.startswith(paramsname):
141+
testnameroot = 'test_' + name[len(paramsname):]
142+
for paramname, params in paramsdict.items():
143+
test = (lambda self, name=name, params=params:
144+
getattr(self, name)(*params))
145+
testname = testnameroot + '_' + paramname
146+
test.__name__ = testname
147+
testfuncs[testname] = test
148+
for key, value in testfuncs.items():
149+
setattr(cls, key, value)
150+
return cls

Lib/test/test_email/test_generator.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
from email import message_from_string, message_from_bytes
55
from email.generator import Generator, BytesGenerator
66
from email import policy
7-
from test.test_email import TestEmailBase, Parameterized
7+
from test.test_email import TestEmailBase, parameterize
88

99

10-
class TestGeneratorBase(metaclass=Parameterized):
10+
@parameterize
11+
class TestGeneratorBase:
1112

1213
policy = policy.default
1314

Lib/test/test_email/test_headerregistry.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from email import errors
55
from email import policy
66
from email.message import Message
7-
from test.test_email import TestEmailBase, Parameterized
7+
from test.test_email import TestEmailBase, parameterize
88
from email import headerregistry
99
from email.headerregistry import Address, Group
1010

@@ -175,7 +175,8 @@ def test_set_date_header_from_datetime(self):
175175
self.assertEqual(m['Date'].datetime, self.dt)
176176

177177

178-
class TestAddressHeader(TestHeaderBase, metaclass=Parameterized):
178+
@parameterize
179+
class TestAddressHeader(TestHeaderBase):
179180

180181
example_params = {
181182

Lib/test/test_email/test_pickleable.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import email.message
77
from email import policy
88
from email.headerregistry import HeaderRegistry
9-
from test.test_email import TestEmailBase, Parameterized
9+
from test.test_email import TestEmailBase, parameterize
1010

11-
class TestPickleCopyHeader(TestEmailBase, metaclass=Parameterized):
11+
12+
@parameterize
13+
class TestPickleCopyHeader(TestEmailBase):
1214

1315
header_factory = HeaderRegistry()
1416

@@ -33,7 +35,8 @@ def header_as_pickle(self, name, value):
3335
self.assertEqual(str(h), str(header))
3436

3537

36-
class TestPickleCopyMessage(TestEmailBase, metaclass=Parameterized):
38+
@parameterize
39+
class TestPickleCopyMessage(TestEmailBase):
3740

3841
# Message objects are a sequence, so we have to make them a one-tuple in
3942
# msg_params so they get passed to the parameterized test method as a

0 commit comments

Comments
 (0)