# Source code for nose2.plugins.loader.testclasses

    r"""
    Load tests from classes that are *not* :class:`unittest.TestCase` subclasses.

    This plugin responds to :func:`loadTestsFromModule` by adding test
    cases for test methods found in classes in the module that are *not*
    subclasses of :class:`unittest.TestCase`, but whose names (lowercased)
    match the configured test method prefix.

    Test class methods that are generators or have param lists are not
    loaded here, but by the :class:`nose2.plugins.loader.generators.Generators` and
    :class:`nose2.plugins.loader.parameters.Parameters` plugins.

    This plugin also implements :func:`loadTestsFromName` to enable
    loading tests from dotted class and method names passed on the command
    line.

    This plugin makes two additional plugin hooks available for other
    test loaders to use:

    .. function :: loadTestsFromTestClass(self, event)

       :param event: A :class:`LoadFromTestClassEvent` instance

       Plugins can use this hook to load tests from a class that is not a
       :class:`unittest.TestCase` subclass. To prevent other plugins from
       loading tests from the test class, set ``event.handled`` to ``True`` and
       return a test suite. Plugins can also append tests to
       ``event.extraTests``. Usually, that's what you want, since
       it allows other plugins to load their tests from the test
       case as well.

    .. function :: getTestMethodNames(self, event)

       :param event: A :class:`GetTestMethodNamesEvent` instance

       Plugins can use this hook to limit or extend the list of test case
       names that will be loaded from a class that is not a
       :class:`unittest.TestCase` subclass by the standard nose2 test
       loader plugins (and other plugins that respect the results of the
       hook). To force a specific list of names, set ``event.handled`` to
       ``True`` and return a list: this exact list will be the only test case
       names loaded from the test case. Plugins can also extend the list
       of names by appending test names to ``event.extraNames``, and
       exclude names by appending test names to ``event.excludedNames``.

    About Test Classes
    ------------------

    Test classes are classes that look test-like but are not subclasses of
    :class:`unittest.TestCase`. Test classes support all of the same test
    types and fixtures as test cases.

    To "look test-like" a class must have a name that, lowercased, matches
    the configured test method prefix -- "test" by default. Test classes
    must also be able to be instantiated without arguments.

    What are they useful for? Mostly the case where a test class can't for
    some reason subclass :class:`unittest.TestCase`. Otherwise, test class
    tests and test cases are functionally equivalent in nose2, and test
    cases have broader support and all of those helpful *assert\** methods
    -- so when in doubt, you should use a :class:`unittest.TestCase`.

    Here's an example of a test class::

      class TestSomething(object):

          def test(self):
              assert self.something(), "Something failed!"

    """

    import inspect
    import sys
    import unittest

    from nose2 import events, util

    __unittest = True

    class TestClassLoader(events.Plugin):
        """Loader plugin that loads test functions"""

        alwaysOn = True
        configSection = "test-classes"

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

        def register(self):
            """Install extra hooks

            Adds the new plugin hooks:

            - loadTestsFromTestClass
            - getTestMethodNames

            """
            super().register()
            self.addMethods("loadTestsFromTestClass", "getTestMethodNames")

        def loadTestsFromModule(self, event):
            """Load test classes from event.module"""
            module = event.module
            for name, obj in util.iter_attrs(module):
                if (
                    isinstance(obj, type)
                    and not issubclass(obj, unittest.TestCase)
                    and not issubclass(obj, unittest.TestSuite)
                    and name.lower().startswith(self.session.testMethodPrefix)
                ):
                    event.extraTests.append(self._loadTestsFromTestClass(event, obj))

        def loadTestsFromName(self, event):
            """Load tests from event.name if it names a test class/method"""
            name = event.name
            module = event.module
            try:
                result = util.test_from_name(name, module)
            except (AttributeError, ImportError):
                event.handled = True
                return event.loader.failedLoadTests(name, sys.exc_info())
            if result is None:
                return
            parent, obj, name, index = result
            if isinstance(obj, type) and not issubclass(obj, unittest.TestCase):
                # name is a test case class
                event.extraTests.append(self._loadTestsFromTestClass(event, obj))
            elif (
                isinstance(parent, type)
                and not issubclass(parent, unittest.TestCase)
                and not inspect.isgeneratorfunction(obj)
                and not hasattr(obj, "paramList")
            ):
                # name is a single test method
                event.extraTests.append(
                    util.transplant_class(MethodTestCase(parent), parent.__module__)(
                        obj.__name__
                    )
                )

        def _loadTestsFromTestClass(self, event, cls):
            # ... fire event for others to load from
            evt = LoadFromTestClassEvent(event.loader, cls)
            result = self.session.hooks.loadTestsFromTestClass(evt)
            if evt.handled:
                loaded_suite = result or event.loader.suiteClass()
            else:
                names = self._getTestMethodNames(event, cls)
                try:
                    loaded_suite = event.loader.suiteClass(
                        [
                            util.transplant_class(MethodTestCase(cls), cls.__module__)(name)
                            for name in names
                        ]
                    )
                except BaseException:  # noqa: B036
                    return event.loader.suiteClass(
                        event.loader.failedLoadTests(cls.__name__, sys.exc_info())
                    )
            if evt.extraTests:
                loaded_suite.addTests(evt.extraTests)
            # ... add extra tests
            return loaded_suite

        def _getTestMethodNames(self, event, cls):
            # ... give others a chance to modify list
            excluded = set()

            def isTestMethod(attrname, cls=cls, excluded=excluded):
                # FIXME allow plugs to change prefix
                prefix = self.session.testMethodPrefix
                return (
                    attrname.startswith(prefix)
                    and attrname not in excluded
                    and callable(getattr(cls, attrname))
                )

            evt = GetTestMethodNamesEvent(event.loader, cls, isTestMethod)
            result = self.session.hooks.getTestMethodNames(evt)
            if evt.handled:
                test_names = result or []
            else:
                excluded.update(evt.excludedNames)

                test_names = [entry for entry in dir(cls) if isTestMethod(entry)]

            if event.loader.sortTestMethodsUsing:
                test_names.sort(key=event.loader.sortTestMethodsUsing)
            return test_names

    # to prevent unit2 discover from running this as a test, need to
    # hide it inside of a factory func. ugly!
    def MethodTestCase(cls):
        class _MethodTestCase(unittest.TestCase):
            def __init__(self, method):
                self.method = method
                self._name = f"{cls.__module__}.{cls.__name__}.{method}"
                self.obj = cls()
                unittest.TestCase.__init__(self, "runTest")

            if util.has_class_fixtures(cls):

                @classmethod
                def setUpClass(klass):
                    if hasattr(cls, "setUpClass"):
                        cls.setUpClass()

                @classmethod
                def tearDownClass(klass):
                    if hasattr(cls, "tearDownClass"):
                        cls.tearDownClass()

            def setUp(self):
                if hasattr(self.obj, "setUp"):
                    self.obj.setUp()

            def tearDown(self):
                if hasattr(self.obj, "tearDown"):
                    self.obj.tearDown()

            def __repr__(self):
                return self._name

            id = __str__ = __repr__

            def runTest(self):
                getattr(self.obj, self.method)()

            def shortDescription(self):
                doc = getattr(self.obj, self.method).__doc__
                return doc and doc.split("\n")[0].strip() or None

        return _MethodTestCase

    #
    # Event classes
    #

    class LoadFromTestClassEvent(events.LoadFromTestCaseEvent):
        """Bare subclass of :class:`nose2.events.LoadFromTestCaseEvent`"""

    class GetTestMethodNamesEvent(events.GetTestCaseNamesEvent):
        """Bare subclass of :class:`nose2.events.GetTestCaseNamesEvent`"""