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
[docs]
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 = {}
[docs]
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]
[docs]
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)
[docs]
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))
[docs]
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)
[docs]
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)
[docs]
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)
[docs]
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
[docs]
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")
[docs]
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