# Source code for nose2.session

    import argparse
    import logging
    import os
    import pathlib
    from configparser import ConfigParser

    from nose2 import config, events, util
    from nose2._toml import load_toml

    log = logging.getLogger(__name__)
    __unittest = True

    class Session:
        """Configuration session.

        Encapsulates all configuration for a given test run.

        .. attribute :: argparse

           An instance of :class:`argparse.ArgumentParser`. Plugins can
           use this directly to add arguments and argument groups, but
           *must* do so in their ``__init__`` methods.

        .. attribute :: pluginargs

           The argparse argument group in which plugins (by default) place
           their command-line arguments. Plugins can use this directly to
           add arguments, but *must* do so in their ``__init__`` methods.

        .. attribute :: hooks

           The :class:`nose2.events.PluginInterface` instance contains
           all available plugin methods and hooks.

        .. attribute :: plugins

           The list of loaded -- but not necessarily *active* -- plugins.

        .. attribute :: verbosity

           Current verbosity level. Default: 1.

        .. attribute :: startDir

           Start directory of test run. Test discovery starts
           here. Default: current working directory.

        .. attribute :: topLevelDir

           Top-level directory of test run. This directory is added to
           sys.path. Default: starting directory.

        .. attribute :: libDirs

           Names of code directories, relative to starting
           directory. Default: ['lib', 'src']. These directories are added
           to sys.path and discovery if the exist.

        .. attribute :: testFilePattern

           Pattern used to discover test module files. Default: test*.py

        .. attribute :: testMethodPrefix

           Prefix used to discover test methods and functions: Default: 'test'.

        .. attribute :: unittest

           The config section for nose2 itself.

        """

        configClass = config.Config

        def __init__(self):
            self.argparse = argparse.ArgumentParser(prog="nose2", add_help=False)
            self.pluginargs = self.argparse.add_argument_group(
                "plugin arguments", "Command-line arguments added by plugins:"
            )
            self.config = ConfigParser()
            self.hooks = events.PluginInterface()
            self.plugins = []
            # this will be reset later, whenever handleCfgArgs happens, but it
            # starts at 1 so that it always has a non-negative integer value
            self.verbosity = 1
            self.startDir = None
            self.topLevelDir = None
            self.testResult = None
            self.testLoader = None
            self.logLevel = logging.WARN
            self.configCache = {}

        def get(self, section):
            """Get a config section.

            :param section: The section name to retrieve.
            :returns: instance of self.configClass.

            """
            # If section exists in cache, return cached version
            if section in self.configCache:
                return self.configCache[section]

            # If section doesn't exist in cache, parse config file
            # (and cache result)
            items = []
            if self.config.has_section(section):
                items = self.config.items(section)
            self.configCache[section] = self.configClass(items)
            return self.configCache[section]

        def loadConfigFiles(self, *filenames):
            """Load config files.

            :param filenames: Names of config files to load.

            Loads all names files that exist into ``self.config``.

            """
            for filename in filenames:
                path = pathlib.Path(filename)
                if not path.exists():
                    continue

                # handle pyproject.toml case
                if path.name == "pyproject.toml":
                    toml_config = load_toml(filename)
                    if not isinstance(toml_config.get("tool"), dict):
                        continue
                    tool_table = toml_config["tool"]
                    if not isinstance(tool_table.get("nose2"), dict):
                        continue
                    self.config.read_dict(tool_table["nose2"])

                # else, use the config parser to read config data
                else:
                    self.config.read(path)

        def loadPlugins(self, modules=None, exclude=None):
            """Load plugins.

            :param modules: List of module names from which to load plugins.

            """
            # plugins set directly
            if modules is None:
                modules = []
            if exclude is None:
                exclude = []
            # plugins mentioned in config file(s)
            cfg = self.unittest
            more_plugins = cfg.as_list("plugins", [])
            cfg_exclude = cfg.as_list("exclude-plugins", [])
            exclude.extend(cfg_exclude)
            exclude = set(exclude)
            all_ = (set(modules) | set(more_plugins)) - exclude
            all_ = sorted(all_)
            log.debug("Loading plugin modules: %s", all_)
            for module in all_:
                self.loadPluginsFromModule(util.module_from_name(module))
            self.hooks.pluginsLoaded(events.PluginsLoadedEvent(self.plugins))

        def loadPluginsFromModule(self, module):
            """Load plugins from a module.

            :param module: A python module containing zero or more plugin
                           classes.

            """
            avail = []
            for _, item in util.iter_attrs(module):
                if not isinstance(item, type):
                    continue
                if item == events.Plugin:
                    continue
                if issubclass(item, events.Plugin):
                    avail.append(item)
            for cls in avail:
                log.debug("Plugin is available: %s", cls)
                plugin = cls(session=self)
                if plugin not in self.plugins:
                    self.plugins.append(plugin)
                for method in self.hooks.preRegistrationMethods:
                    if hasattr(plugin, method):
                        self.hooks.register(method, plugin)

        def registerPlugin(self, plugin):
            """Register a plugin.

            :param plugin: A `nose2.events.Plugin` instance.

            Register the plugin with all methods it implements.

            """
            log.debug("Register active plugin %s", plugin)
            if plugin not in self.plugins:
                self.plugins.append(plugin)
            for method in self.hooks.methods:
                if hasattr(plugin, method):
                    log.debug("Register method %s for plugin %s", method, plugin)
                    self.hooks.register(method, plugin)

        def setVerbosity(self, args_verbosity, args_verbose, args_quiet):
            """
            Determine verbosity from various (possibly conflicting) sources of info

            :param args_verbosity: The --verbosity argument value
            :param args_verbose: count of -v options
            :param args_quiet: count of -q options

            start with config, override with any given --verbosity, then adjust
            up/down with -vvv -qq, etc
            """
            self.verbosity = self.unittest.as_int("verbosity", 1)
            if args_verbosity is not None:
                self.verbosity = args_verbosity
            # adjust up or down, depending on the difference of these counts
            self.verbosity += args_verbose - args_quiet
            # floor the value at 0 -- verbosity is always a non-negative integer
            self.verbosity = max(self.verbosity, 0)

        def setStartDir(self, args_start_dir=None):
            """
            start dir comes from config and may be overridden by an argument
            """
            self.startDir = self.unittest.as_str("start-dir", ".")
            if args_start_dir is not None:
                self.startDir = args_start_dir

        def prepareSysPath(self):
            """Add code directories to sys.path"""
            tld = self.topLevelDir
            sd = self.startDir
            if tld is None:
                tld = sd
            tld = os.path.abspath(tld)
            util.ensure_importable(tld)
            for libdir in self.libDirs:
                libdir = os.path.abspath(os.path.join(tld, libdir))
                if os.path.exists(libdir):
                    util.ensure_importable(libdir)

        # convenience properties
        @property
        def libDirs(self):
            return self.unittest.as_list("code-directories", ["lib", "src"])

        @property
        def testFilePattern(self):
            return self.unittest.as_str("test-file-pattern", "test*.py")

        @property
        def testMethodPrefix(self):
            return self.unittest.as_str("test-method-prefix", "test")

        @property
        def unittest(self):
            return self.get("unittest")

        def isPluginLoaded(self, pluginName):
            """Returns ``True`` if a given plugin is loaded.

            :param pluginName: the name of the plugin module:
                               e.g. "nose2.plugins.layers".

            """
            for plugin in self.plugins:
                if pluginName == plugin.__class__.__module__:
                    return True
            return False