diff --git a/Lib/test/support.py b/Lib/test/support.py --- a/Lib/test/support.py +++ b/Lib/test/support.py @@ -198,6 +198,76 @@ return fresh_module +def _swap_out_module(test_globals, modname, module, names): + # XXX ensure all the targets already exist? + sys.modules[modname] = module + test_globals[modname] = module + for name in names: + test_globals[name] = getattr(module, name) + + +def conforms_to_pep399(test_globals, module, c_module_name, names=None): + """A class decorator that makes a test case conform to PEP 399. + + Two new subclasses of the decorated class are created and the pair + is returned instead of the original class. Both are also added to + the globals. + + The following happens for the duration of each subclass's tests: + + * the module is replaced in sys.modules with the C or pure Python + version, + * the module is replaced in the globals, + * any of the "names" found in the globals are replaced by the + corresponding module attribute. + + """ + modname = module.__name__ + + def decorator(cls): + if unittest.TestCase not in cls.__bases__: + bases = (cls, unittest.TestCase) + else: + bases = (cls,) + + pymod = import_fresh_module(modname, blocked=[c_module_name]) + cmod = import_fresh_module(modname, fresh=[c_module_name]) + cskipmsg = 'requires the C version of the {} module'.format(modname) + + class PurePythonTests(*bases): + @classmethod + def setUpClass(cls): + _swap_out_module(test_globals, modname, pymod, names) + super(PurePythonTests, cls).setUpClass() + @classmethod + def tearDownClass(cls): + _swap_out_module(test_globals, modname, module, names) + super(PurePythonTests, cls).tearDownClass() + PurePythonTests.__name__ = "PurePython" + cls.__name__ + PurePythonTests.__module__ = test_globals['__name__'] + test_globals[PurePythonTests.__name__] = PurePythonTests + + if cmod: + @unittest.skipUnless(cmod, cskipmsg) + class CPythonTests(*bases): + @classmethod + def setUpClass(cls): + _swap_out_module(test_globals, modname, cmod, names) + super(CPythonTests, cls).setUpClass() + @classmethod + def tearDownClass(cls): + _swap_out_module(test_globals, modname, module, names) + super(CPythonTests, cls).tearDownClass() + CPythonTests.__name__ = "CPython" + cls.__name__ + CPythonTests.__module__ = test_globals['__name__'] + test_globals[CPythonTests.__name__] = CPythonTests + else: + CPythonTests = None + + return (PurePythonTests, CPythonTests) + return decorator + + def get_attribute(obj, name): """Get an attribute, raising SkipTest if AttributeError is raised.""" try: diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -26,6 +26,16 @@ def test_import_fresh_module(self): support.import_fresh_module("ftplib") + def test_conforms_to_pep_399(self): + @support.conforms_to_pep399(globals(), support, '_support', ['TESTFN']) + class SomeTest: + def test_something(self): + self.assertNotEqual('spam', 'ham') + + self.assertIn('PurePythonSomeTest', globals()) + # clean up a side-effect + del globals()['PurePythonSomeTest'] + def test_get_attribute(self): self.assertEqual(support.get_attribute(self, "test_get_attribute"), self.test_get_attribute)