from test.test_importlib import abc, util

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

import errno
import os
import py_compile
import stat
import sys
import tempfile
from test.support.import_helper import make_legacy_pyc
import unittest


class FinderTests(abc.FinderTests):

    """For a top-level module, it should just be found directly in the
    directory being searched. This is true for a directory with source
    [top-level source], bytecode [top-level bc], or both [top-level both].
    There is also the possibility that it is a package [top-level package], in
    which case there will be a directory with the module name and an
    __init__.py file. If there is a directory without an __init__.py an
    ImportWarning is returned [empty dir].

    For sub-modules and sub-packages, the same happens as above but only use
    the tail end of the name [sub module] [sub package] [sub empty].

    When there is a conflict between a package and module having the same name
    in the same directory, the package wins out [package over module]. This is
    so that imports of modules within the package can occur rather than trigger
    an import error.

    When there is a package and module with the same name, always pick the
    package over the module [package over module]. This is so that imports from
    the package have the possibility of succeeding.

    """

    def get_finder(self, root):
        loader_details = [(self.machinery.SourceFileLoader,
                            self.machinery.SOURCE_SUFFIXES),
                          (self.machinery.SourcelessFileLoader,
                            self.machinery.BYTECODE_SUFFIXES)]
        return self.machinery.FileFinder(root, *loader_details)

    def import_(self, root, module):
        finder = self.get_finder(root)
        return self._find(finder, module, loader_only=True)

    def run_test(self, test, create=None, *, compile_=None, unlink=None):
        """Test the finding of 'test' with the creation of modules listed in
        'create'.

        Any names listed in 'compile_' are byte-compiled. Modules
        listed in 'unlink' have their source files deleted.

        """
        if create is None:
            create = {test}
        if (compile_ or unlink) and sys.implementation.cache_tag is None:
            raise unittest.SkipTest('requires sys.implementation.cache_tag')
        with util.create_modules(*create) as mapping:
            if compile_:
                for name in compile_:
                    py_compile.compile(mapping[name])
            if unlink:
                for name in unlink:
                    os.unlink(mapping[name])
                    try:
                        make_legacy_pyc(mapping[name])
                    except OSError as error:
                        # Some tests do not set compile_=True so the source
                        # module will not get compiled and there will be no
                        # PEP 3147 pyc file to rename.
                        if error.errno != errno.ENOENT:
                            raise
            loader = self.import_(mapping['.root'], test)
            self.assertHasAttr(loader, 'exec_module')
            return loader

    def test_module(self):
        # [top-level source]
        self.run_test('top_level')
        # [top-level bc]
        self.run_test('top_level', compile_={'top_level'},
                      unlink={'top_level'})
        # [top-level both]
        self.run_test('top_level', compile_={'top_level'})

    # [top-level package]
    def test_package(self):
        # Source.
        self.run_test('pkg', {'pkg.__init__'})
        # Bytecode.
        self.run_test('pkg', {'pkg.__init__'}, compile_={'pkg.__init__'},
                unlink={'pkg.__init__'})
        # Both.
        self.run_test('pkg', {'pkg.__init__'}, compile_={'pkg.__init__'})

    # [sub module]
    def test_module_in_package(self):
        with util.create_modules('pkg.__init__', 'pkg.sub') as mapping:
            pkg_dir = os.path.dirname(mapping['pkg.__init__'])
            loader = self.import_(pkg_dir, 'pkg.sub')
            self.assertHasAttr(loader, 'exec_module')

    # [sub package]
    def test_package_in_package(self):
        context = util.create_modules('pkg.__init__', 'pkg.sub.__init__')
        with context as mapping:
            pkg_dir = os.path.dirname(mapping['pkg.__init__'])
            loader = self.import_(pkg_dir, 'pkg.sub')
            self.assertHasAttr(loader, 'exec_module')

    # [package over modules]
    def test_package_over_module(self):
        name = '_temp'
        loader = self.run_test(name, {'{0}.__init__'.format(name), name})
        self.assertIn('__init__', loader.get_filename(name))

    def test_failure(self):
        with util.create_modules('blah') as mapping:
            nothing = self.import_(mapping['.root'], 'sdfsadsadf')
            self.assertEqual(nothing, self.NOT_FOUND)

    def test_empty_string_for_dir(self):
        # The empty string from sys.path means to search in the cwd.
        finder = self.machinery.FileFinder('', (self.machinery.SourceFileLoader,
            self.machinery.SOURCE_SUFFIXES))
        with open('mod.py', 'w', encoding='utf-8') as file:
            file.write("# test file for importlib")
        try:
            loader = self._find(finder, 'mod', loader_only=True)
            self.assertHasAttr(loader, 'exec_module')
        finally:
            os.unlink('mod.py')

    def test_invalidate_caches(self):
        # invalidate_caches() should reset the mtime.
        finder = self.machinery.FileFinder('', (self.machinery.SourceFileLoader,
            self.machinery.SOURCE_SUFFIXES))
        finder._path_mtime = 42
        finder.invalidate_caches()
        self.assertEqual(finder._path_mtime, -1)

    # Regression test for http://bugs.python.org/issue14846
    def test_dir_removal_handling(self):
        mod = 'mod'
        with util.create_modules(mod) as mapping:
            finder = self.get_finder(mapping['.root'])
            found = self._find(finder, 'mod', loader_only=True)
            self.assertIsNotNone(found)
        found = self._find(finder, 'mod', loader_only=True)
        self.assertEqual(found, self.NOT_FOUND)

    @unittest.skipUnless(sys.platform != 'win32',
            'os.chmod() does not support the needed arguments under Windows')
    def test_no_read_directory(self):
        # Issue #16730
        tempdir = tempfile.TemporaryDirectory()
        self.enterContext(tempdir)
        # Since we muck with the permissions, we want to set them back to
        # their original values to make sure the directory can be properly
        # cleaned up.
        original_mode = os.stat(tempdir.name).st_mode
        self.addCleanup(os.chmod, tempdir.name, original_mode)
        os.chmod(tempdir.name, stat.S_IWUSR | stat.S_IXUSR)
        finder = self.get_finder(tempdir.name)
        found = self._find(finder, 'doesnotexist')
        self.assertEqual(found, self.NOT_FOUND)

    def test_ignore_file(self):
        # If a directory got changed to a file from underneath us, then don't
        # worry about looking for submodules.
        with tempfile.NamedTemporaryFile() as file_obj:
            finder = self.get_finder(file_obj.name)
            found = self._find(finder, 'doesnotexist')
            self.assertEqual(found, self.NOT_FOUND)


class FinderTestsPEP451(FinderTests):

    NOT_FOUND = None

    def _find(self, finder, name, loader_only=False):
        spec = finder.find_spec(name)
        return spec.loader if spec is not None else spec


(Frozen_FinderTestsPEP451,
 Source_FinderTestsPEP451
 ) = util.test_both(FinderTestsPEP451, machinery=machinery)


class FinderTestsPEP420(FinderTests):

    NOT_FOUND = (None, [])

    def _find(self, finder, name, loader_only=False):
        spec = finder.find_spec(name)
        if spec is None:
            return self.NOT_FOUND
        if loader_only:
            return spec.loader
        return spec.loader, spec.submodule_search_locations


(Frozen_FinderTestsPEP420,
 Source_FinderTestsPEP420
 ) = util.test_both(FinderTestsPEP420, machinery=machinery)


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