# 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
