from .. import util

importlib = util.import_importlib('importlib')
machinery = util.import_importlib('importlib.machinery')

import os
import sys
import tempfile
from types import ModuleType
import unittest
import warnings
import zipimport


class FinderTests:

    """Tests for PathFinder."""

    find = None
    check_found = None

    def test_failure(self):
        # Test None returned upon not finding a suitable loader.
        module = '<test module>'
        with util.import_state():
            self.assertIsNone(self.find(module))

    def test_sys_path(self):
        # Test that sys.path is used when 'path' is None.
        # Implicitly tests that sys.path_importer_cache is used.
        module = '<test module>'
        path = '<test path>'
        importer = util.mock_spec(module)
        with util.import_state(path_importer_cache={path: importer},
                               path=[path]):
            found = self.find(module)
            self.check_found(found, importer)

    def test_path(self):
        # Test that 'path' is used when set.
        # Implicitly tests that sys.path_importer_cache is used.
        module = '<test module>'
        path = '<test path>'
        importer = util.mock_spec(module)
        with util.import_state(path_importer_cache={path: importer}):
            found = self.find(module, [path])
            self.check_found(found, importer)

    def test_empty_list(self):
        # An empty list should not count as asking for sys.path.
        module = 'module'
        path = '<test path>'
        importer = util.mock_spec(module)
        with util.import_state(path_importer_cache={path: importer},
                               path=[path]):
            self.assertIsNone(self.find('module', []))

    def test_path_hooks(self):
        # Test that sys.path_hooks is used.
        # Test that sys.path_importer_cache is set.
        module = '<test module>'
        path = '<test path>'
        importer = util.mock_spec(module)
        hook = util.mock_path_hook(path, importer=importer)
        with util.import_state(path_hooks=[hook]):
            found = self.find(module, [path])
            self.check_found(found, importer)
            self.assertIn(path, sys.path_importer_cache)
            self.assertIs(sys.path_importer_cache[path], importer)

    def test_empty_path_hooks(self):
        # Test that if sys.path_hooks is empty a warning is raised,
        # sys.path_importer_cache gets None set, and PathFinder returns None.
        path_entry = 'bogus_path'
        with util.import_state(path_importer_cache={}, path_hooks=[],
                               path=[path_entry]):
            with warnings.catch_warnings(record=True) as w:
                warnings.simplefilter('always', ImportWarning)
                warnings.simplefilter('ignore', DeprecationWarning)
                self.assertIsNone(self.find('os'))
                self.assertIsNone(sys.path_importer_cache[path_entry])
                self.assertEqual(len(w), 1)
                self.assertTrue(issubclass(w[-1].category, ImportWarning))

    def test_path_importer_cache_empty_string(self):
        # The empty string should create a finder using the cwd.
        path = ''
        module = '<test module>'
        importer = util.mock_spec(module)
        hook = util.mock_path_hook(os.getcwd(), importer=importer)
        with util.import_state(path=[path], path_hooks=[hook]):
            found = self.find(module)
            self.check_found(found, importer)
            self.assertIn(os.getcwd(), sys.path_importer_cache)

    def test_None_on_sys_path(self):
        # Putting None in sys.path[0] caused an import regression from Python
        # 3.2: http://bugs.python.org/issue16514
        new_path = sys.path[:]
        new_path.insert(0, None)
        new_path_importer_cache = sys.path_importer_cache.copy()
        new_path_importer_cache.pop(None, None)
        new_path_hooks = [zipimport.zipimporter,
                          self.machinery.FileFinder.path_hook(
                              *self.importlib._bootstrap_external._get_supported_file_loaders())]
        missing = object()
        email = sys.modules.pop('email', missing)
        try:
            with util.import_state(meta_path=sys.meta_path[:],
                                   path=new_path,
                                   path_importer_cache=new_path_importer_cache,
                                   path_hooks=new_path_hooks):
                module = self.importlib.import_module('email')
                self.assertIsInstance(module, ModuleType)
        finally:
            if email is not missing:
                sys.modules['email'] = email

    def test_finder_with_find_module(self):
        class TestFinder:
            def find_module(self, fullname):
                return self.to_return
        failing_finder = TestFinder()
        failing_finder.to_return = None
        path = 'testing path'
        with util.import_state(path_importer_cache={path: failing_finder}):
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", ImportWarning)
                self.assertIsNone(
                    self.machinery.PathFinder.find_spec('whatever', [path]))
        success_finder = TestFinder()
        success_finder.to_return = __loader__
        with util.import_state(path_importer_cache={path: success_finder}):
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", ImportWarning)
                spec = self.machinery.PathFinder.find_spec('whatever', [path])
        self.assertEqual(spec.loader, __loader__)

    def test_finder_with_find_loader(self):
        class TestFinder:
            loader = None
            portions = []
            def find_loader(self, fullname):
                return self.loader, self.portions
        path = 'testing path'
        with util.import_state(path_importer_cache={path: TestFinder()}):
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", ImportWarning)
                self.assertIsNone(
                    self.machinery.PathFinder.find_spec('whatever', [path]))
        success_finder = TestFinder()
        success_finder.loader = __loader__
        with util.import_state(path_importer_cache={path: success_finder}):
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", ImportWarning)
                spec = self.machinery.PathFinder.find_spec('whatever', [path])
        self.assertEqual(spec.loader, __loader__)

    def test_finder_with_find_spec(self):
        class TestFinder:
            spec = None
            def find_spec(self, fullname, target=None):
                return self.spec
        path = 'testing path'
        with util.import_state(path_importer_cache={path: TestFinder()}):
            self.assertIsNone(
                    self.machinery.PathFinder.find_spec('whatever', [path]))
        success_finder = TestFinder()
        success_finder.spec = self.machinery.ModuleSpec('whatever', __loader__)
        with util.import_state(path_importer_cache={path: success_finder}):
            got = self.machinery.PathFinder.find_spec('whatever', [path])
        self.assertEqual(got, success_finder.spec)

    def test_deleted_cwd(self):
        # Issue #22834
        old_dir = os.getcwd()
        self.addCleanup(os.chdir, old_dir)
        new_dir = tempfile.mkdtemp()
        try:
            os.chdir(new_dir)
            try:
                os.rmdir(new_dir)
            except OSError:
                # EINVAL on Solaris, EBUSY on AIX, ENOTEMPTY on Windows
                self.skipTest("platform does not allow "
                              "the deletion of the cwd")
        except:
            os.chdir(old_dir)
            os.rmdir(new_dir)
            raise

        with util.import_state(path=['']):
            # Do not want FileNotFoundError raised.
            self.assertIsNone(self.machinery.PathFinder.find_spec('whatever'))

    def test_invalidate_caches_finders(self):
        # Finders with an invalidate_caches() method have it called.
        class FakeFinder:
            def __init__(self):
                self.called = False

            def invalidate_caches(self):
                self.called = True

        cache = {'leave_alone': object(), 'finder_to_invalidate': FakeFinder()}
        with util.import_state(path_importer_cache=cache):
            self.machinery.PathFinder.invalidate_caches()
        self.assertTrue(cache['finder_to_invalidate'].called)

    def test_invalidate_caches_clear_out_None(self):
        # Clear out None in sys.path_importer_cache() when invalidating caches.
        cache = {'clear_out': None}
        with util.import_state(path_importer_cache=cache):
            self.machinery.PathFinder.invalidate_caches()
        self.assertEqual(len(cache), 0)


class FindModuleTests(FinderTests):
    def find(self, *args, **kwargs):
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", DeprecationWarning)
            return self.machinery.PathFinder.find_module(*args, **kwargs)
    def check_found(self, found, importer):
        self.assertIs(found, importer)


(Frozen_FindModuleTests,
 Source_FindModuleTests
) = util.test_both(FindModuleTests, importlib=importlib, machinery=machinery)


class FindSpecTests(FinderTests):
    def find(self, *args, **kwargs):
        return self.machinery.PathFinder.find_spec(*args, **kwargs)
    def check_found(self, found, importer):
        self.assertIs(found.loader, importer)


(Frozen_FindSpecTests,
 Source_FindSpecTests
 ) = util.test_both(FindSpecTests, importlib=importlib, machinery=machinery)


class PathEntryFinderTests:

    def test_finder_with_failing_find_spec(self):
        # PathEntryFinder with find_module() defined should work.
        # Issue #20763.
        class Finder:
            path_location = 'test_finder_with_find_module'
            def __init__(self, path):
                if path != self.path_location:
                    raise ImportError

            @staticmethod
            def find_module(fullname):
                return None


        with util.import_state(path=[Finder.path_location]+sys.path[:],
                               path_hooks=[Finder]):
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", ImportWarning)
                self.machinery.PathFinder.find_spec('importlib')

    def test_finder_with_failing_find_module(self):
        # PathEntryFinder with find_module() defined should work.
        # Issue #20763.
        class Finder:
            path_location = 'test_finder_with_find_module'
            def __init__(self, path):
                if path != self.path_location:
                    raise ImportError

            @staticmethod
            def find_module(fullname):
                return None


        with util.import_state(path=[Finder.path_location]+sys.path[:],
                               path_hooks=[Finder]):
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", ImportWarning)
                warnings.simplefilter("ignore", DeprecationWarning)
                self.machinery.PathFinder.find_module('importlib')


(Frozen_PEFTests,
 Source_PEFTests
 ) = util.test_both(PathEntryFinderTests, machinery=machinery)


if __name__ == '__main__':
    unittest.main()
