# Copyright (c) Twisted Matrix Laboratories. # See LICENSE for details. """ Tests for miscellaneous behaviors of the top-level L{twisted} package (ie, for the code in C{twisted/__init__.py}. """ from __future__ import division, absolute_import import sys import twisted from types import ModuleType from twisted.python._setup import _checkPythonVersion from twisted.python.compat import _PY3 from twisted.python import reflect from twisted.trial.unittest import TestCase # This is somewhat generally useful and should probably be part of a public API # somewhere. See #5977. class SetAsideModule(object): """ L{SetAsideModule} is a context manager for temporarily removing a module from C{sys.modules}. @ivar name: The name of the module to remove. """ def __init__(self, name): self.name = name def _unimport(self, name): """ Find the given module and all of its hierarchically inferior modules in C{sys.modules}, remove them from it, and return whatever was found. """ modules = dict([ (moduleName, module) for (moduleName, module) in list(sys.modules.items()) if (moduleName == self.name or moduleName.startswith(self.name + "."))]) for name in modules: del sys.modules[name] return modules def __enter__(self): self.modules = self._unimport(self.name) def __exit__(self, excType, excValue, traceback): self._unimport(self.name) sys.modules.update(self.modules) def _install(modules): """ Take a mapping defining a package and turn it into real C{ModuleType} instances in C{sys.modules}. Consider these example:: a = {"foo": "bar"} b = {"twisted": {"__version__": "42.6"}} c = {"twisted": {"plugin": {"getPlugins": stub}}} C{_install(a)} will place an item into C{sys.modules} with C{"foo"} as the key and C{"bar" as the value. C{_install(b)} will place an item into C{sys.modules} with C{"twisted"} as the key. The value will be a new module object. The module will have a C{"__version__"} attribute with C{"42.6"} as the value. C{_install(c)} will place an item into C{sys.modules} with C{"twisted"} as the key. The value will be a new module object with a C{"plugin"} attribute. An item will also be placed into C{sys.modules} with the key C{"twisted.plugin"} which refers to that module object. That module will have an attribute C{"getPlugins"} with a value of C{stub}. @param modules: A mapping from names to definitions of modules. The names are native strings like C{"twisted"} or C{"unittest"}. Values may be arbitrary objects. Any value which is not a dictionary will be added to C{sys.modules} unmodified. Any dictionary value indicates the value is a new module and its items define the attributes of that module. The definition of this structure is recursive, so a value in the dictionary may be a dictionary to trigger another level of processing. @return: L{None} """ result = {} _makePackages(None, modules, result) sys.modules.update(result) def _makePackages(parent, attributes, result): """ Construct module objects (for either modules or packages). @param parent: L{None} or a module object which is the Python package containing all of the modules being created by this function call. Its name will be prepended to the name of all created modules. @param attributes: A mapping giving the attributes of the particular module object this call is creating. @param result: A mapping which is populated with all created module names. This is suitable for use in updating C{sys.modules}. @return: A mapping of all of the attributes created by this call. This is suitable for populating the dictionary of C{parent}. @see: L{_install}. """ attrs = {} for (name, value) in list(attributes.items()): if parent is None: if isinstance(value, dict): module = ModuleType(name) module.__dict__.update(_makePackages(module, value, result)) result[name] = module else: result[name] = value else: if isinstance(value, dict): module = ModuleType(parent.__name__ + '.' + name) module.__dict__.update(_makePackages(module, value, result)) result[parent.__name__ + '.' + name] = module attrs[name] = module else: attrs[name] = value return attrs class RequirementsTests(TestCase): """ Tests for the import-time requirements checking. @ivar unsupportedPythonVersion: The newest version of Python 2.x which is not supported by Twisted. @type unsupportedPythonVersion: C{tuple} @ivar supportedPythonVersion: The oldest version of Python 2.x which is supported by Twisted. @type supportedPythonVersion: C{tuple} @ivar Py3unsupportedPythonVersion: The newest version of Python 3.x which is not supported by Twisted. @type Py3unsupportedPythonVersion: C{tuple} @ivar Py3supportedPythonVersion: The oldest version of Python 3.x which is supported by Twisted. @type supportedPythonVersion: C{tuple} """ unsupportedPythonVersion = (2, 6) supportedPythonVersion = (2, 7) Py3unsupportedPythonVersion = (3, 3) Py3supportedPythonVersion = (3, 4) def setUp(self): """ Save the original value of C{sys.version_info} so it can be restored after the tests mess with it. """ self.version = sys.version_info def tearDown(self): """ Restore the original values saved in L{setUp}. """ sys.version_info = self.version def test_oldPython(self): """ L{_checkPythonVersion} raises L{ImportError} when run on a version of Python that is too old. """ sys.version_info = self.unsupportedPythonVersion with self.assertRaises(ImportError) as raised: _checkPythonVersion() self.assertEqual("Twisted requires Python %d.%d or later." % self.supportedPythonVersion, str(raised.exception)) def test_newPython(self): """ L{_checkPythonVersion} returns L{None} when run on a version of Python that is sufficiently new. """ sys.version_info = self.supportedPythonVersion self.assertIsNone(_checkPythonVersion()) def test_oldPythonPy3(self): """ L{_checkPythonVersion} raises L{ImportError} when run on a version of Python that is too old. """ sys.version_info = self.Py3unsupportedPythonVersion with self.assertRaises(ImportError) as raised: _checkPythonVersion() self.assertEqual("Twisted on Python 3 requires Python %d.%d or later." % self.Py3supportedPythonVersion, str(raised.exception)) def test_newPythonPy3(self): """ L{_checkPythonVersion} returns L{None} when run on a version of Python that is sufficiently new. """ sys.version_info = self.Py3supportedPythonVersion self.assertIsNone(_checkPythonVersion()) class MakePackagesTests(TestCase): """ Tests for L{_makePackages}, a helper for populating C{sys.modules} with fictional modules. """ def test_nonModule(self): """ A non-C{dict} value in the attributes dictionary passed to L{_makePackages} is preserved unchanged in the return value. """ modules = {} _makePackages(None, dict(reactor='reactor'), modules) self.assertEqual(modules, dict(reactor='reactor')) def test_moduleWithAttribute(self): """ A C{dict} value in the attributes dictionary passed to L{_makePackages} is turned into a L{ModuleType} instance with attributes populated from the items of that C{dict} value. """ modules = {} _makePackages(None, dict(twisted=dict(version='123')), modules) self.assertIsInstance(modules, dict) self.assertIsInstance(modules['twisted'], ModuleType) self.assertEqual('twisted', modules['twisted'].__name__) self.assertEqual('123', modules['twisted'].version) def test_packageWithModule(self): """ Processing of the attributes dictionary is recursive, so a C{dict} value it contains may itself contain a C{dict} value to the same effect. """ modules = {} _makePackages(None, dict(twisted=dict(web=dict(version='321'))), modules) self.assertIsInstance(modules, dict) self.assertIsInstance(modules['twisted'], ModuleType) self.assertEqual('twisted', modules['twisted'].__name__) self.assertIsInstance(modules['twisted'].web, ModuleType) self.assertEqual('twisted.web', modules['twisted'].web.__name__) self.assertEqual('321', modules['twisted'].web.version) class OldSubprojectDeprecationBase(TestCase): """ Base L{TestCase} for verifying each former subproject has a deprecated C{__version__} and a removed C{_version.py}. """ subproject = None def test_deprecated(self): """ The C{__version__} attribute of former subprojects is deprecated. """ module = reflect.namedAny("twisted.{}".format(self.subproject)) self.assertEqual(module.__version__, twisted.__version__) warningsShown = self.flushWarnings() self.assertEqual(1, len(warningsShown)) self.assertEqual( "twisted.{}.__version__ was deprecated in Twisted 16.0.0: " "Use twisted.__version__ instead.".format(self.subproject), warningsShown[0]['message']) def test_noversionpy(self): """ Former subprojects no longer have an importable C{_version.py}. """ with self.assertRaises(AttributeError): reflect.namedAny( "twisted.{}._version".format(self.subproject)) if _PY3: subprojects = ["conch", "web", "names"] else: subprojects = ["mail", "conch", "runner", "web", "words", "names", "news", "pair"] for subproject in subprojects: class SubprojectTestCase(OldSubprojectDeprecationBase): """ See L{OldSubprojectDeprecationBase}. """ subproject = subproject newName = subproject.title() + "VersionDeprecationTests" SubprojectTestCase.__name__ = newName if _PY3: SubprojectTestCase.__qualname__= ".".join( OldSubprojectDeprecationBase.__qualname__.split()[0:-1] + [newName]) globals().update({subproject.title() + "VersionDeprecationTests": SubprojectTestCase}) del SubprojectTestCase del newName del OldSubprojectDeprecationBase