cpython: Don't use metaclasses when class decorators can do the job.

http://hg.python.org/cpython/rev/01b72be1ce0c changeset: 77273:01b72be1ce0c user: R David Murray <rdmurray@bitdance.com> date: Thu May 31 18:00:45 2012 -0400 summary: 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. files: Lib/email/_policybase.py | 39 ++--- 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, 63 insertions(+), 64 deletions(-) diff --git a/Lib/email/_policybase.py b/Lib/email/_policybase.py --- a/Lib/email/_policybase.py +++ b/Lib/email/_policybase.py @@ -91,31 +91,25 @@ 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 _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 __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) +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 - @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): +class Policy(_PolicyBase, metaclass=abc.ABCMeta): r"""Controls for how messages are interpreted and formatted. @@ -264,6 +258,7 @@ raise NotImplementedError +@_extend_docstrings class Compat32(Policy): """+ diff --git a/Lib/email/policy.py b/Lib/email/policy.py --- 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 @@ 'HTTP', ] +@_extend_docstrings class EmailPolicy(Policy): """+ diff --git a/Lib/test/test_email/__init__.py b/Lib/test/test_email/__init__.py --- a/Lib/test/test_email/__init__.py +++ b/Lib/test/test_email/__init__.py @@ -73,10 +73,8 @@ '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 @@ 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 @@ 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 --- a/Lib/test/test_email/test_generator.py +++ b/Lib/test/test_email/test_generator.py @@ -4,10 +4,11 @@ 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 --- a/Lib/test/test_email/test_headerregistry.py +++ b/Lib/test/test_email/test_headerregistry.py @@ -4,7 +4,7 @@ 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 @@ 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 --- a/Lib/test/test_email/test_pickleable.py +++ b/Lib/test/test_email/test_pickleable.py @@ -6,9 +6,11 @@ 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 @@ 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 -- Repository URL: http://hg.python.org/cpython
participants (1)
-
r.david.murray