From 1be413e366be7bed676074b7f395b2a20a10ac47 Mon Sep 17 00:00:00 2001 From: R David Murray Date: Thu, 31 May 2012 18:00:45 -0400 Subject: 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. --- Lib/email/_policybase.py | 45 +++++++++----------- Lib/email/policy.py | 3 +- Lib/test/test_email/__init__.py | 66 +++++++++++++++--------------- Lib/test/test_email/test_generator.py | 5 ++- Lib/test/test_email/test_headerregistry.py | 5 ++- Lib/test/test_email/test_pickleable.py | 9 ++-- 6 files changed, 66 insertions(+), 67 deletions(-) diff --git a/Lib/email/_policybase.py b/Lib/email/_policybase.py index d5e8df9..8106114 100644 --- a/Lib/email/_policybase.py +++ b/Lib/email/_policybase.py @@ -91,31 +91,25 @@ class _PolicyBase: return self.clone(**other.__dict__) -# Conceptually this isn't a subclass of ABCMeta, but since we want Policy to -# use ABCMeta as a metaclass *and* we want it to use this one as well, we have -# to make this one a subclas of ABCMeta. -class _DocstringExtenderMetaclass(abc.ABCMeta): - - def __new__(meta, classname, bases, classdict): - if classdict.get('__doc__') and classdict['__doc__'].startswith('+'): - classdict['__doc__'] = meta._append_doc(bases[0].__doc__, - classdict['__doc__']) - for name, attr in classdict.items(): - if attr.__doc__ and attr.__doc__.startswith('+'): - for cls in (cls for base in bases for cls in base.mro()): - doc = getattr(getattr(cls, name), '__doc__') - if doc: - attr.__doc__ = meta._append_doc(doc, attr.__doc__) - break - return super().__new__(meta, classname, bases, classdict) - - @staticmethod - def _append_doc(doc, added_doc): - added_doc = added_doc.split('\n', 1)[1] - return doc + '\n' + added_doc - - -class Policy(_PolicyBase, metaclass=_DocstringExtenderMetaclass): +def _append_doc(doc, added_doc): + doc = doc.rsplit('\n', 1)[0] + added_doc = added_doc.split('\n', 1)[1] + return doc + '\n' + added_doc + +def _extend_docstrings(cls): + if cls.__doc__ and cls.__doc__.startswith('+'): + cls.__doc__ = _append_doc(cls.__bases__[0].__doc__, cls.__doc__) + for name, attr in cls.__dict__.items(): + if attr.__doc__ and attr.__doc__.startswith('+'): + for c in (c for base in cls.__bases__ for c in base.mro()): + doc = getattr(getattr(c, name), '__doc__') + if doc: + attr.__doc__ = _append_doc(doc, attr.__doc__) + break + return cls + + +class Policy(_PolicyBase, metaclass=abc.ABCMeta): r"""Controls for how messages are interpreted and formatted. @@ -264,6 +258,7 @@ class Policy(_PolicyBase, metaclass=_DocstringExtenderMetaclass): raise NotImplementedError +@_extend_docstrings class Compat32(Policy): """+ diff --git a/Lib/email/policy.py b/Lib/email/policy.py index bfffb45..32cad0d 100644 --- a/Lib/email/policy.py +++ b/Lib/email/policy.py @@ -2,7 +2,7 @@ code that adds all the email6 features. """ -from email._policybase import Policy, Compat32, compat32 +from email._policybase import Policy, Compat32, compat32, _extend_docstrings from email.utils import _has_surrogates from email.headerregistry import HeaderRegistry as HeaderRegistry @@ -17,6 +17,7 @@ __all__ = [ 'HTTP', ] +@_extend_docstrings class EmailPolicy(Policy): """+ diff --git a/Lib/test/test_email/__init__.py b/Lib/test/test_email/__init__.py index bd9d52c..f206ace 100644 --- a/Lib/test/test_email/__init__.py +++ b/Lib/test/test_email/__init__.py @@ -73,10 +73,8 @@ class TestEmailBase(unittest.TestCase): 'item {}'.format(i)) -# Metaclass to allow for parameterized tests -class Parameterized(type): - - """Provide a test method parameterization facility. +def parameterize(cls): + """A test method parameterization class decorator. Parameters are specified as the value of a class attribute that ends with the string '_params'. Call the portion before '_params' the prefix. Then @@ -92,9 +90,10 @@ class Parameterized(type): In a _params dictioanry, the keys become part of the name of the generated tests. In a _params list, the values in the list are converted into a string by joining the string values of the elements of the tuple by '_' and - converting any blanks into '_'s, and this become part of the name. The - full name of a generated test is the portion of the _params name before the - '_params' portion, plus an '_', plus the name derived as explained above. + converting any blanks into '_'s, and this become part of the name. + The full name of a generated test is a 'test_' prefix, the portion of the + test function name after the '_as_' separator, plus an '_', plus the name + derived as explained above. For example, if we have: @@ -123,30 +122,29 @@ class Parameterized(type): be used to select the test individually from the unittest command line. """ - - def __new__(meta, classname, bases, classdict): - paramdicts = {} - for name, attr in classdict.items(): - if name.endswith('_params'): - if not hasattr(attr, 'keys'): - d = {} - for x in attr: - if not hasattr(x, '__iter__'): - x = (x,) - n = '_'.join(str(v) for v in x).replace(' ', '_') - d[n] = x - attr = d - paramdicts[name[:-7] + '_as_'] = attr - testfuncs = {} - for name, attr in classdict.items(): - for paramsname, paramsdict in paramdicts.items(): - if name.startswith(paramsname): - testnameroot = 'test_' + name[len(paramsname):] - for paramname, params in paramsdict.items(): - test = (lambda self, name=name, params=params: - getattr(self, name)(*params)) - testname = testnameroot + '_' + paramname - test.__name__ = testname - testfuncs[testname] = test - classdict.update(testfuncs) - return super().__new__(meta, classname, bases, classdict) + paramdicts = {} + for name, attr in cls.__dict__.items(): + if name.endswith('_params'): + if not hasattr(attr, 'keys'): + d = {} + for x in attr: + if not hasattr(x, '__iter__'): + x = (x,) + n = '_'.join(str(v) for v in x).replace(' ', '_') + d[n] = x + attr = d + paramdicts[name[:-7] + '_as_'] = attr + testfuncs = {} + for name, attr in cls.__dict__.items(): + for paramsname, paramsdict in paramdicts.items(): + if name.startswith(paramsname): + testnameroot = 'test_' + name[len(paramsname):] + for paramname, params in paramsdict.items(): + test = (lambda self, name=name, params=params: + getattr(self, name)(*params)) + testname = testnameroot + '_' + paramname + test.__name__ = testname + testfuncs[testname] = test + for key, value in testfuncs.items(): + setattr(cls, key, value) + return cls diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py index 7e1529b..8917408 100644 --- a/Lib/test/test_email/test_generator.py +++ b/Lib/test/test_email/test_generator.py @@ -4,10 +4,11 @@ import unittest from email import message_from_string, message_from_bytes from email.generator import Generator, BytesGenerator from email import policy -from test.test_email import TestEmailBase, Parameterized +from test.test_email import TestEmailBase, parameterize -class TestGeneratorBase(metaclass=Parameterized): +@parameterize +class TestGeneratorBase: policy = policy.default diff --git a/Lib/test/test_email/test_headerregistry.py b/Lib/test/test_email/test_headerregistry.py index 4a57ff1..fc11fba 100644 --- a/Lib/test/test_email/test_headerregistry.py +++ b/Lib/test/test_email/test_headerregistry.py @@ -4,7 +4,7 @@ import unittest from email import errors from email import policy from email.message import Message -from test.test_email import TestEmailBase, Parameterized +from test.test_email import TestEmailBase, parameterize from email import headerregistry from email.headerregistry import Address, Group @@ -175,7 +175,8 @@ class TestDateHeader(TestHeaderBase): self.assertEqual(m['Date'].datetime, self.dt) -class TestAddressHeader(TestHeaderBase, metaclass=Parameterized): +@parameterize +class TestAddressHeader(TestHeaderBase): example_params = { diff --git a/Lib/test/test_email/test_pickleable.py b/Lib/test/test_email/test_pickleable.py index 09477e0..daa8d25 100644 --- a/Lib/test/test_email/test_pickleable.py +++ b/Lib/test/test_email/test_pickleable.py @@ -6,9 +6,11 @@ import email import email.message from email import policy from email.headerregistry import HeaderRegistry -from test.test_email import TestEmailBase, Parameterized +from test.test_email import TestEmailBase, parameterize -class TestPickleCopyHeader(TestEmailBase, metaclass=Parameterized): + +@parameterize +class TestPickleCopyHeader(TestEmailBase): header_factory = HeaderRegistry() @@ -33,7 +35,8 @@ class TestPickleCopyHeader(TestEmailBase, metaclass=Parameterized): self.assertEqual(str(h), str(header)) -class TestPickleCopyMessage(TestEmailBase, metaclass=Parameterized): +@parameterize +class TestPickleCopyMessage(TestEmailBase): # Message objects are a sequence, so we have to make them a one-tuple in # msg_params so they get passed to the parameterized test method as a -- cgit v0.12