import importlib

from tests import mock_importlib

import sys
from test.test_support import run_unittest
import unittest


class ImportHelper(unittest.TestCase):
    
    """Common helper methods for testing the Importer class."""
    
    def setUp(self):
        """Store a copy of the 'sys' attribute pertaining to imports."""
        # Don't backup sys.modules since dict is cached.
        # Don't clear sys.modules as it can wreak havoc
        # (e.g., losing __builtin__).
        self.old_sys_modules = sys.modules.copy()
        self.old_meta_path = sys.meta_path[:]
        sys.meta_path = []
        self.old_sys_path = sys.path[:]
        sys.path = []
        self.old_path_hooks = sys.path_hooks[:]
        sys.path_hooks = []
        self.old_path_importer_cache = sys.path_importer_cache.copy()
        sys.path_importer_cache.clear()
        self.default_importer = mock_importlib.PassImporter()
        self.importer = importlib.Import(self.default_importer, tuple())
        
    def tearDown(self):
        """Restore backup of import-related attributes in 'sys'."""
        sys.modules.clear()
        sys.modules.update(self.old_sys_modules)
        sys.meta_path = self.old_meta_path
        sys.path = self.old_sys_path
        sys.path_hooks = self.old_path_hooks
        sys.path_importer_cache = self.old_path_importer_cache

    def clear_sys_modules(*modules):
        for module in modules:
            try:
                del sys.modules[module]
            except KeyError:
                pass


class ImportHelper2(ImportHelper):

    """Create mock modules."""

    def setUp(self):
        ImportHelper.setUp(self)
        self.parent_name = '<parent>'
        self.child_name = '<child>'
        self.full_child_name = '.'.join([self.parent_name, self.child_name])
        self.parent_module = mock_importlib.MockModule(self.parent_name)
        self.child_module = mock_importlib.MockModule(self.full_child_name)
        setattr(self.parent_module, self.child_name, self.child_module)

class ImportNameResolutionTests(ImportHelper2):

    """Test the absolute name resolution for relative imports."""
    
    def test_classic_relative_import_in_package(self):
        # Importing from within a package's __init__ file should lead to a
        # resolved import name of the package name tacked on to the name of the
        # module being imported.
        resolved_name = self.importer._classic_resolve_name(self.child_name,
                                                            self.parent_name,
                                                            True)
        self.failUnlessEqual(resolved_name, self.full_child_name)

    def test_classic_relative_import_in_module(self):
        # Importing within a module in a package should lead to the importer's
        # module name being removed and replaced with the name of what is to be
        # imported.
        calling_from = self.parent_name + '.' + '<calling from>'
        resolved_name = self.importer._classic_resolve_name(self.child_name,
                                                                calling_from,
                                                                False)
        self.failUnlessEqual(resolved_name, self.full_child_name)

    def test_relative_import_in_package_for_a_module(self):
        # Trying to import a single level within a package within it's __init__
        # module should stay within the package.
        # ``from .child_name import ...`` in a package.
        resolved_name = self.importer._resolve_name(self.child_name,
                                                    self.parent_name, True, 1)
        self.failUnlessEqual(resolved_name, self.full_child_name)

    def test_relative_import_in_module_for_a_module(self):
        # Importing from within a module in a package should try to import from
        # within the same directory as the module requesting the import.
        # ``from .child_name import ...`` in a package module.
        calling_from = self.parent_name + '.' + '<calling from>'
        resolved_name = self.importer._resolve_name(self.child_name,
                                                    calling_from, False, 1)
        self.failUnlessEqual(resolved_name, self.full_child_name)

    def test_relative_import_deep_in_package(self):
        # Calling from a deep point in the package should still work.
        depth = 10
        name_extension = (str(x) for x in xrange(10))
        calling_from = self.parent_name + '.' + '.'.join(name_extension)
        resolved_name = self.importer._resolve_name(self.child_name,
                calling_from, False, depth)
        self.failUnlessEqual(resolved_name, self.full_child_name)

    def test_attempt_to_escape_out_of_package_init(self):
        # Attempting to go too high out of a package in its __init__ file
        # should raise ImportError.
        # ``from ..child_name import ...`` in a top-level package.
        self.failUnlessRaises(ImportError, self.importer._resolve_name,
                self.child_name, self.parent_name, True, 2)

    def test_attempt_to_escape_out_of_package_module(self):
        # Attempting to go too high in the package from a module should raise
        # ImportError.
        # ``from ..child_name import ...`` in a top-level package module.
        calling_from = self.parent_name + '.' + '<calling from>'
        self.failUnlessRaises(ImportError, self.importer._resolve_name,
                                self.child_name, calling_from, False, 2)

    def test_relative_import_in_top_level(self):
        # Attempting a relative import in a top-level location should raise
        # ImportError.
        # ``from .child_name import ...`` outside of a package.
        self.failUnlessRaises(ImportError, self.importer._resolve_name,
                                self.child_name, self.parent_name, False, 1)
                                
    def test_relative_import_in_package_init(self):
        # ``from . import ...`` in a package.
        resolved_name = self.importer._resolve_name('', self.parent_name, True,
                                                    1)
        self.failUnlessEqual(resolved_name, self.parent_name)
        
    def test_relative_import_in_package_module(self):
        # ``from . import ...`` in a package module.
        resolved_name = self.importer._resolve_name('', self.full_child_name,
                                                    False, 1)
        self.failUnlessEqual(resolved_name, self.parent_name)

    def test_relative_import_redirection(self):
        # Having a relative module name resolve to a name that has a value of
        # None in sys.modules should redirect to import an absolute import for
        # the specified name.
        module_name = '<to import>'
        pkg_name = '<pkg>'
        resolved_relative_name = module_name + '.' + pkg_name
        expected_module = mock_importlib.MockModule(module_name)
        sys.modules[resolved_relative_name] = None
        sys.modules[module_name] = expected_module
        importing_globals = {'__name__':pkg_name, '__path__':['some path']}
        imported = self.importer(module_name, importing_globals, level=-1)
        self.failUnless(imported is expected_module)


class ImportFromListTests(ImportHelper2):

    """Test conditions based on fromlist."""
    
    def test_fromlist_relative_import(self):
        # Any items specified in fromlist while importing a package needs to be
        # checked as to whether it is a pre-existing attribute or should be
        # considered a declaration for a relative import.
        module_name = '<module name>'
        pkg_name = '<pkg name>'
        full_module_name = pkg_name + '.' + module_name
        # Already have package imported.
        pkg_module = mock_importlib.MockModule(pkg_name, pkg_list=['some path'])
        sys.modules[pkg_name] = pkg_module
        # Make sure implicit import succeeds.
        succeed = mock_importlib.SucceedImporter()
        sys.meta_path.append(succeed)
        # Import the package with a fromlist of the module.
        module = self.importer._return_module(pkg_name, '',
                                                fromlist=[module_name])
        self.failUnless(hasattr(module, module_name))
        fromlist_module = getattr(module, module_name)
        self.failUnlessEqual(fromlist_module.__name__, full_module_name)

    def test_fromlist_nonexistent(self):
        # If something listed in a fromlist does not exist the import
        # should still succeed.
        pkg_name = '<pkg name>'
        pkg_module = mock_importlib.MockModule(pkg_name, pkg_list=['some path'])
        sys.modules[pkg_name] = pkg_module
        nonexistent_attr = 'asdfsdfd'
        module = self.importer._return_module(pkg_name, '',
                                                fromlist=[nonexistent_attr])
        self.failUnless(not hasattr(module, nonexistent_attr))

    def test_fromlist_existing(self):
        # A value in fromlist that already exists should not lead to a relative
        # import.
        pkg_name = '<pkg name>'
        pkg_module = mock_importlib.MockModule(pkg_name, pkg_list=['some path'])
        attr = 'some_attr'
        setattr(pkg_module, attr, None)
        sys.modules[pkg_name] = pkg_module
        failing_import = mock_importlib.ErrorImporter()
        sys.meta_path.append(failing_import)
        module = self.importer(pkg_name, fromlist=[attr])
        self.failUnless(hasattr(module, attr))
        self.failUnless(not hasattr(failing_import, 'find_request'))

    def test_fromlist_nonpackage(self):
        # No implicit imports of values in fromlist should be done if a module
        # is what is being imported specifically.
        module_name = '<module>'
        module = mock_importlib.MockModule(module_name)
        sys.modules[module_name] = module
        failing_import = mock_importlib.ErrorImporter()
        sys.meta_path.append(failing_import)
        imported_module = self.importer(module_name, fromlist=['sadfsdd'])
        self.failUnless(not hasattr(failing_import, 'find_request'))

    def test_fromlist_relative_import_all(self):
        # When '*' is passed in for fromlist, __all__ should be used for the
        # possibility of a relative import.
        module_name = '<module name>'
        pkg_name = '<pkg name>'
        full_module_name = pkg_name + '.' + module_name
        pkg_module = mock_importlib.MockModule(pkg_name, pkg_list=['some path'],
                                                __all__=[module_name])
        sys.modules[pkg_name] = pkg_module
        succeed = mock_importlib.SucceedImporter()
        sys.meta_path.append(succeed)
        # Also tests that fromlist can be a tuple and still work.
        module = self.importer(pkg_name, fromlist=('*',))
        self.failUnless(hasattr(module, module_name))
        relative_module = getattr(module, module_name)
        self.failUnlessEqual(relative_module.__name__, full_module_name)
        
    def test_empty_fromlist(self):
        # An empty fromlist means that the root module is returned.
        sys.modules[self.parent_name] = self.parent_module
        sys.modules[self.full_child_name] = self.child_module
        module = self.importer._return_module(self.full_child_name, '', [])
        self.failUnless(module is self.parent_module)

    def test_nonempty_fromlist(self):
        # A fromlist with values should return the specified module.
        sys.modules[self.parent_name] = self.parent_module
        sys.modules[self.full_child_name] = self.child_module
        test_attr_name = 'test_attr'
        setattr(self.child_module, test_attr_name, None)
        module = self.importer._return_module(self.full_child_name, '',
                                                [test_attr_name])
        self.failUnless(module is self.child_module)
        
    def test_relative_import_empty_fromlist(self):
        # If a relative import (with no dot) is performed with an empty fromlist
        # then the module specified in the relative name is returned.
        sys.modules[self.full_child_name] = self.child_module
        sys.modules[self.parent_name] = self.parent_module
        module = self.importer._return_module(self.full_child_name,
                                                self.child_name, [])
        self.failUnless(module is self.child_module)
        
    def test_deep_relative_import_empty_fromlist(self):
        # If the relative name of a module has a dot, return the module that
        # resolves to the absolute name of the module up to the first dot.
        sys.modules[self.full_child_name] = self.child_module
        sys.modules[self.parent_name] = self.parent_module
        extra_depth = '.baz'
        absolute_name = self.full_child_name + extra_depth
        relative_name = self.child_name + extra_depth
        module = self.importer._return_module(absolute_name, relative_name, [])
        self.failUnless(module is self.child_module)


class ImportMiscTests(ImportHelper2):

    """Test miscellaneous parts of the Import class.
    
    Tests of the default values for __init__ are handled in the integration
    tests.

    """

    def test_sys_module_return(self):
        # A module found in sys.modules should be returned immediately.
        sys.modules[self.parent_name] = self.parent_module
        module = self.importer._import_module(self.parent_name)
        self.failUnless(module is self.parent_module)
        
    def test_sys_module_None(self):
        # If sys.modules contains None for a module name, then raise ImportError.
        module_name = '<module>'
        sys.modules[module_name] = None
        self.failUnlessRaises(ImportError, self.importer._import_module,
                                module_name)
        
    def test_parent_missing(self):
        # An import should fail if a parent module cannot be found.
        sys.modules[self.full_child_name] = self.child_module
        self.failUnlessRaises(ImportError, self.importer, self.full_child_name)
        
    def test_parent_imported(self):
        # If a parent module is already imported, importing a child module
        # should work (along with setting the attribute on the parent for the
        # child module).
        delattr(self.parent_module, self.child_name)
        succeed_importer = mock_importlib.SucceedImporter()
        sys.meta_path.append(succeed_importer)
        sys.modules[self.parent_name] = self.parent_module
        self.importer(self.full_child_name)
        self.failUnless(hasattr(self.parent_module, self.child_name))
        self.failUnlessEqual(getattr(self.parent_module, self.child_name),
                                sys.modules[self.full_child_name])

    def test_parent_not_imported(self):
        # If a parent module is not imported yet, it should be imported.
        # The attribute on the parent module for the child module should also
        # be set.
        delattr(self.parent_module, self.child_name)
        succeed_importer = mock_importlib.SucceedImporter()
        sys.meta_path.append(succeed_importer)
        self.importer(self.full_child_name)
        self.failUnless(self.parent_name in sys.modules)
        self.failUnless(self.full_child_name in sys.modules)
        self.failUnless(hasattr(sys.modules[self.parent_name], self.child_name))
        self.failUnlessEqual(getattr(sys.modules[self.parent_name],
                                        self.child_name),
                                sys.modules[self.full_child_name])

    def test_None_not_set_for_import_failure(self):
        # If an import that is tried both relative and absolute fails there
        # should not be an entry of None for the resolved relative name.
        module_name = '<should fail>'
        pkg_name = '<non-existent package>'
        resolved_name = module_name + '.' + pkg_name
        importing_globals = {'__name__':pkg_name, '__path__':['path']}
        self.failUnlessRaises(ImportError, self.importer, module_name,
                                importing_globals, {}, [], -1)
        self.failUnless(resolved_name not in sys.modules)
        
    def test_empty_string(self):
        # An empty string should raise ValueError if level is not > 0.
        for level in (-1, 0):
            self.failUnlessRaises(ValueError, self.importer, '', {}, {}, level)

    def test_module_from_cache(self):
        # If a value is in sys.modules it should be returned (no matter the
        # object type), else return False.
        value = "a 'module'"
        mod_name = "module name"
        sys.modules[mod_name] = value
        returned = self.importer.module_from_cache(mod_name)
        self.failUnless(returned is value)
        returned = self.importer.module_from_cache(mod_name + 'asdfeddf')
        self.failUnless(returned is False)

    def test_post_import(self):
        # Post-import processing should do nothing but return the module
        # unscathed.
        module = "mod"
        self.failUnless(self.importer.post_import(module) is module)

          
class ImportMetaPathTests(ImportHelper):
    
    """Test meta_path usage."""

    def test_search_meta_path(self):
        # Test search method of sys.meta_path.
        # Should raise ImportError on error.
        self.clear_sys_modules('sys')
        self.failUnlessRaises(ImportError, self.importer._search_meta_path,
                                'sys')
        # Verify call order.
        meta_path = (mock_importlib.PassImporter(),
                        mock_importlib.SucceedImporter())
        sys.meta_path = meta_path
        loader = self.importer._search_meta_path('sys')
        for entry in meta_path:
            self.failUnlessEqual(entry.find_request, ('sys', None))
        self.failUnless(loader is meta_path[-1])
        
    def test_extended_meta_path(self):
        # Default meta_path entries set during initialization should be
        # queried after sys.meta_path.
        self.clear_sys_modules('sys')
        pass_importer = mock_importlib.PassImporter()
        sys.meta_path = [pass_importer]
        succeed_importer = mock_importlib.SucceedImporter()
        importer_ = importlib.Import(extended_meta_path=(succeed_importer,))
        module = importer_._import_module('sys')
        for meta_importer in (pass_importer, succeed_importer):
            self.failUnlessEqual(meta_importer.find_request, ('sys', None))
        self.failUnless(module in succeed_importer.loaded_modules)
        
    def test_parent_path(self):
        # If a parent module has __path__ defined it should be passed as an
        # argument during importing.
        pkg_name = '<pkg>'
        module_name = '<module>'
        test_path = ['<test path>']
        pkg_module = mock_importlib.MockModule(pkg_name)
        pkg_module.__path__ = test_path
        sys.modules[pkg_name] = pkg_module
        succeed_importer = mock_importlib.SucceedImporter()
        sys.meta_path.append(succeed_importer)
        full_module_name = '.'.join([pkg_name, module_name])
        lookup_args = (full_module_name, test_path)
        self.importer(full_module_name)
        self.failUnless(full_module_name in sys.modules)
        self.failUnlessEqual(succeed_importer.find_request, lookup_args)
        self.failUnlessEqual(succeed_importer.load_request, lookup_args[0])


class ImportStdPathTests(ImportHelper):
    
    """Test sys.path usage."""

    def test_default_importer_factory(self):
        # Make sure that the object passed in during initialization is used
        # when sys.path_importer_cache has a value of None.
        module_name = '<dummy>'
        self.clear_sys_modules(module_name)
        succeed_importer = mock_importlib.SucceedImporter()
        importer_ = importlib.Import(succeed_importer, tuple())
        sys.meta_path = []
        sys.path = ['<succeed>']
        sys.path_importer_cache['<succeed>'] = None
        module = importer_._import_module(module_name)
        self.failUnlessEqual(succeed_importer.find_request,
                                (module_name, None))
        self.failUnless(module in succeed_importer.loaded_modules)

    def test_search_std_path(self):
        # Test sys.path searching for a loader.
        module_name = '<dummy>'
        self.clear_sys_modules(module_name)
        importer_ = importlib.Import(extended_meta_path=())
        sys.path = []
        pass_importer = mock_importlib.PassImporter.set_on_sys_path()
        succeed_importer = mock_importlib.SucceedImporter.set_on_sys_path()
        sys_path = (pass_importer, succeed_importer)
        module = importer_._import_module(module_name)
        for entry in sys_path:
            self.failUnlessEqual(entry.find_request, (module_name, None))
        self.failUnless(module in succeed_importer.loaded_modules)

    def test_importer_cache_preexisting(self):
        # A pre-existing importer should be returned if it exists in
        # sys.path_importer_cache.
        module_name = '<dummy>'
        self.clear_sys_modules(module_name)
        sys.path = []
        succeed_importer = mock_importlib.SucceedImporter.set_on_sys_path()
        loader = self.importer._search_std_path(module_name)
        self.failUnless(loader is succeed_importer)
        
    def test_importer_cache_from_path_hooks(self):
        # If an entry does not exist for a sys.path entry in the importer cache
        # then sys.path_hooks should be searched and if one is found then cache
        # it.
        module_name = '<dummy>'
        self.clear_sys_modules(module_name)
        path_entry = '<succeed>'
        succeed_importer = mock_importlib.SucceedImporter()
        sys.path = [path_entry]
        sys.path_importer_cache.clear()
        sys.path_hooks = [succeed_importer]
        loader = self.importer._search_std_path(module_name)
        self.failUnless(loader is succeed_importer)
        self.failUnless(sys.path_importer_cache[path_entry] is
                        succeed_importer)
        
    def test_importer_cache_no_path_hooks(self):
        # If an entry does not exist for a sys.path entry in the importer cache
        # and sys.path_hooks has nothing for the entry, None should be set.
        module_name = '<dummy>'
        self.clear_sys_modules(module_name)
        path_entry = '<test>'
        sys.path = [path_entry]
        sys.path_hooks = []
        sys.path_importer_cache.clear()
        self.failUnlessRaises(ImportError, self.importer._search_std_path,
                                module_name)
        self.failUnless(sys.path_importer_cache[path_entry] is None)
        
    def test_searching_package_path(self):
        # If importing in a package then search path is the package's __path__
        # value; otherwise it is sys.path.
        succeed_importer = mock_importlib.SucceedImporter()
        sys.path_hooks.append(succeed_importer)
        search_paths = ['test path']
        module_name = '<pkg>.<dummy>'
        loader = self.importer._search_std_path(module_name, search_paths)
        self.failUnless(loader is succeed_importer)
        self.failUnless(search_paths[0] in succeed_importer.path_entries)
        self.failUnlessEqual(succeed_importer.find_request,
                                (module_name, None))
        self.failUnless(search_paths[0] in sys.path_importer_cache)
        self.clear_sys_modules(module_name)
        del sys.path_importer_cache[search_paths[0]]
        succeed_importer.path_entries = []
        self.importer._import_module(module_name, search_paths)
        self.failUnless(search_paths[0] in succeed_importer.path_entries)
        

def test_main():
    run_unittest(ImportNameResolutionTests, ImportFromListTests,
            ImportMiscTests, ImportMetaPathTests, ImportStdPathTests)



if __name__ == '__main__':
    test_main()
