# Source code for nose2.plugins.loader.discovery

    """
    Discovery-based test loader.

    This plugin implements nose2's automatic test module discovery. It
    looks for test modules in packages and directories whose names start
    with ``test``, then fires the :func:`loadTestsFromModule` hook for each
    one to allow other plugins to load the actual tests.

    It also fires :func:`handleFile` for every file that it sees, and
    :func:`matchPath` for every Python module, to allow other plugins to
    load tests from other kinds of files and to influence which modules
    are examined for tests.

    """

    # Adapted from unittest2/loader.py from the unittest2 plugins branch.
    # This module contains some code copied from unittest2/loader.py and other
    # code developed in reference to that module and others within unittest2.
    # unittest2 is Copyright (c) 2001-2010 Python Software Foundation; All
    # Rights Reserved. See: http://docs.python.org/license.html

    import logging
    import os
    import sys
    from fnmatch import fnmatch

    from nose2 import events, util

    __unittest = True
    log = logging.getLogger(__name__)

    class DirectoryHandler:
        def __init__(self, session) -> None:
            self.session = session
            self.event_handled = False

        def handle_dir(self, event, full_path, top_level):
            dirname = os.path.basename(full_path)
            pattern = self.session.testFilePattern

            evt = events.HandleFileEvent(
                event.loader, dirname, full_path, pattern, top_level
            )
            result = self.session.hooks.handleDir(evt)
            if evt.extraTests:
                yield from evt.extraTests
            if evt.handled:
                if result:
                    yield result
                self.event_handled = True
                return

            evt = events.MatchPathEvent(dirname, full_path, pattern)
            result = self.session.hooks.matchDirPath(evt)
            if evt.handled and not result:
                self.event_handled = True

    class Discoverer:
        def loadTestsFromName(self, event):
            """Load tests from module named by event.name"""
            # turn name into path or module name
            # fire appropriate hooks (handle file or load from module)
            if event.module:
                return
            name = event.name
            module = None
            _, top_level_dir = self._getStartDirs()
            try:
                # try name as a dotted module name first
                __import__(name)
                module = sys.modules[name]
            except (KeyboardInterrupt, SystemExit):
                raise
            except BaseException:  # noqa: B036
                # if that fails, try it as a file or directory
                event.extraTests.extend(self._find_tests(event, name, top_level_dir))
            else:
                event.extraTests.extend(
                    self._find_tests_in_module(event, module, top_level_dir)
                )

        def loadTestsFromNames(self, event):
            """Discover tests if no test names specified"""
            log.debug("Received event %s", event)
            if event.names or event.module:
                return
            event.handled = True  # I will handle discovery
            return self._discover(event)

        def _checkIfPathIsOK(self, start_dir):
            if not os.path.isdir(os.path.abspath(start_dir)):
                raise OSError("%s is not a directory" % os.path.abspath(start_dir))

        def _getStartDirs(self):
            start_dir = self.session.startDir
            top_level_dir = self.session.topLevelDir
            if start_dir is None:
                start_dir = "."
            if top_level_dir is None:
                top_level_dir = start_dir

            self._checkIfPathIsOK(start_dir)

            is_not_importable = False
            start_dir = os.path.abspath(start_dir)
            top_level_dir = os.path.abspath(top_level_dir)
            if start_dir != top_level_dir:
                is_not_importable = not os.path.isfile(
                    os.path.join(start_dir, "__init__.py")
                )
            if is_not_importable:
                raise ImportError("Start directory is not importable: %r" % start_dir)
            # this is redundant in some cases, but that's ok
            self.session.prepareSysPath()
            return start_dir, top_level_dir

        def _discover(self, event):
            loader = event.loader
            try:
                start_dir, top_level_dir = self._getStartDirs()
            except (OSError, ImportError):
                return loader.suiteClass(
                    loader.failedLoadTests(self.session.startDir, sys.exc_info())
                )
            log.debug("_discover in %s (%s)", start_dir, top_level_dir)
            tests = list(self._find_tests(event, start_dir, top_level_dir))
            return loader.suiteClass(tests)

        def _find_tests(self, event, start, top_level):
            """Used by discovery. Yields test suites it loads."""
            log.debug("_find_tests(%r, %r)", start, top_level)
            if start == top_level:
                full_path = start
            else:
                full_path = os.path.join(top_level, start)
            if os.path.isdir(start):
                yield from self._find_tests_in_dir(event, full_path, top_level)
            elif os.path.isfile(start):
                yield from self._find_tests_in_file(event, start, full_path, top_level)

        def _find_tests_in_dir(self, event, full_path, top_level):
            if not os.path.isdir(full_path):
                return
            log.debug("find in dir %s (%s)", full_path, top_level)
            dir_handler = DirectoryHandler(self.session)
            yield from dir_handler.handle_dir(event, full_path, top_level)
            if dir_handler.event_handled:
                return
            for path in os.listdir(full_path):
                entry_path = os.path.join(full_path, path)
                if os.path.isfile(entry_path):
                    yield from self._find_tests_in_file(event, path, entry_path, top_level)
                elif os.path.isdir(entry_path):
                    if (
                        "test" in path.lower()
                        or util.ispackage(entry_path)
                        or path in self.session.libDirs
                    ):
                        yield from self._find_tests(event, entry_path, top_level)

        def _find_tests_in_file(
            self, event, filename, full_path, top_level, module_name=None
        ):
            log.debug("find in file %s (%s)", full_path, top_level)
            pattern = self.session.testFilePattern
            loader = event.loader
            evt = events.HandleFileEvent(loader, filename, full_path, pattern, top_level)
            result = self.session.hooks.handleFile(evt)
            if evt.extraTests:
                yield loader.suiteClass(evt.extraTests)

            if evt.handled:
                if result:
                    yield result
                return

            if not util.valid_module_name(filename):
                # valid Python identifiers only
                return

            evt = events.MatchPathEvent(filename, full_path, pattern)
            result = self.session.hooks.matchPath(evt)
            if evt.handled:
                if not result:
                    return
            elif not self._match_path(filename, full_path, pattern):
                return

            if module_name is None:
                module_name, package_path = util.name_from_path(full_path)
                util.ensure_importable(package_path)
            try:
                module = util.module_from_name(module_name)
            except BaseException:  # noqa: B036
                yield loader.failedImport(module_name)
            else:
                mod_file = os.path.abspath(getattr(module, "__file__", full_path))
                realpath = os.path.splitext(mod_file)[0]
                fullpath_noext = os.path.splitext(full_path)[0]
                if realpath.lower() != fullpath_noext.lower():
                    module_dir = os.path.dirname(realpath)
                    mod_name = os.path.splitext(os.path.basename(full_path))[0]
                    expected_dir = os.path.dirname(full_path)
                    msg = (
                        "%r module incorrectly imported from %r. "
                        "Expected %r. Is this module globally installed?"
                    )
                    raise ImportError(msg % (mod_name, module_dir, expected_dir))
                yield loader.loadTestsFromModule(module)

        def _find_tests_in_module(self, event, module, top_level_dir):
            # only called from loadTestsFromName
            yield event.loader.loadTestsFromModule(module)
            # may be a package; recurse into __path__ if so
            pkgpath = getattr(module, "__path__", None)
            if pkgpath:
                for entry in pkgpath:
                    full_path = os.path.abspath(os.path.join(top_level_dir, entry))
                    yield from self._find_tests_in_dir(event, full_path, top_level_dir)

        def _match_path(self, path, full_path, pattern):
            # override this method to use alternative matching strategy
            return fnmatch(path, pattern)

    class DiscoveryLoader(events.Plugin, Discoverer):
        """Loader plugin that can discover tests"""

        alwaysOn = True
        configSection = "discovery"

        def registerInSubprocess(self, event):
            event.pluginClasses.append(self.__class__)

        def loadTestsFromName(self, event):
            """Load tests from module named by event.name"""
            return Discoverer.loadTestsFromName(self, event)

        def loadTestsFromNames(self, event):
            """Discover tests if no test names specified"""
            return Discoverer.loadTestsFromNames(self, event)