plugins: Refactor plugin identification and access

This change moves all plugin handling code into the kas.plugins module.
New accessor functions `plugins.get(name)` and `plugins.all()` are
provided to wrap the plugins dictionary so that the kas main function
doesn't need to worry about how this is accessed. Plugins are loaded at
runtime rather than at parse time by calling `plugins.load()` which
gives us an improved ability to handle errors.

The `@kasplugin` decorator is removed as it modified and attribute on
the kasplugin function itself when a plugin module was loaded. Importing
a module should not result in changes to a variable in a different
module as it leads to an initialization code flow which is difficult to
reason about. Instead, plugin modules should now list the plugins which
they introduce in a `__KAS_PLUGINS__` list which will be walked at
runtime by `plugins.load()`.

Signed-off-by: Paul Barker <pbarker@konsulko.com>
Signed-off-by: Jan Kiszka <jan.kiszka@siemens.com>
This commit is contained in:
Paul Barker 2020-10-16 19:31:47 +02:00 committed by Jan Kiszka
parent 7b9bce8e46
commit 5e3ca820b3
4 changed files with 44 additions and 24 deletions

View File

@ -42,15 +42,7 @@ except ImportError:
HAVE_COLORLOG = False
from . import __version__, __file_version__, __compatible_file_version__
# Import kas plugins
# Since they are added by decorators, they don't need to be called,
# just imported.
from .plugins import kasplugin
from .plugins import build
from .plugins import shell
__all__ = ['build', 'shell']
from . import plugins
__license__ = 'MIT'
__copyright__ = 'Copyright (c) Siemens AG, 2017-2018'
@ -131,7 +123,7 @@ def kas_get_argparser():
subparser = parser.add_subparsers(help='sub command help', dest='cmd')
for plugin in getattr(kasplugin, 'plugins', {}).values():
for plugin in plugins.all():
plugin_parser = subparser.add_parser(plugin.name, help=plugin.helpmsg)
setup_parser_common_args(plugin_parser)
plugin.setup_parser(plugin_parser)
@ -144,6 +136,7 @@ def kas(argv):
The actual main entry point of kas.
"""
create_logger()
plugins.load()
parser = kas_get_argparser()
args = parser.parse_args(argv)
@ -159,9 +152,10 @@ def kas(argv):
loop.add_signal_handler(sig, interruption)
atexit.register(_atexit_handler)
if args.cmd:
plugin = getattr(kasplugin, 'plugins', {})[args.cmd]
plugin().run(args)
plugin_class = plugins.get(args.cmd)
if plugin_class:
plugin = plugin_class()
plugin.run(args)
else:
parser.print_help()

View File

@ -23,13 +23,37 @@
This module contains and manages kas plugins
"""
PLUGINS = {}
def kasplugin(plugin_class):
def register_plugins(mod):
"""
A decorator that registers kas plugins
Register all kas plugins found in a module
"""
if not hasattr(kasplugin, 'plugins'):
setattr(kasplugin, 'plugins', {})
getattr(kasplugin, 'plugins').update({
plugin_class.name: plugin_class
})
for plugin in getattr(mod, '__KAS_PLUGINS__', []):
PLUGINS[plugin.name] = plugin
def load():
"""
Import all kas plugins
"""
from . import build
from . import shell
register_plugins(build)
register_plugins(shell)
def get(name):
"""
Lookup a kas plugin class by name
"""
return PLUGINS.get(name, None)
def all():
"""
Get a list of all loaded kas plugin classes
"""
return PLUGINS.values()

View File

@ -35,13 +35,11 @@ from kas.libcmds import (Macro, Command, SetupDir, CleanupSSHAgent,
WriteBBConfig, SetupHome, ReposApplyPatches,
Loop, InitSetupRepos, FinishSetupRepos,
SetupReposStep)
from kas.plugins import kasplugin
__license__ = 'MIT'
__copyright__ = 'Copyright (c) Siemens AG, 2017-2018'
@kasplugin
class Build:
"""
This class implements the build plugin for kas.
@ -135,3 +133,6 @@ class BuildCommand(Command):
sys.exit(ret)
else:
run_cmd(cmd, cwd=ctx.build_dir)
__KAS_PLUGINS__ = [Build]

View File

@ -35,13 +35,11 @@ from kas.libcmds import (Macro, Command, SetupDir, SetupEnviron,
CleanupSSHAgent, SetupSSHAgent,
Loop, InitSetupRepos, FinishSetupRepos,
SetupReposStep)
from kas.plugins import kasplugin
__license__ = 'MIT'
__copyright__ = 'Copyright (c) Siemens AG, 2017-2018'
@kasplugin
class Shell:
"""
Implements a kas plugin that opens a shell within the kas environment.
@ -129,3 +127,6 @@ class ShellCommand(Command):
if ret != 0:
logging.error('Shell returned non-zero exit status %d', ret)
sys.exit(ret)
__KAS_PLUGINS__ = [Shell]