# Released under the MIT License. See LICENSE for details.
#
"""Plugin related functionality."""
from __future__ import annotations
import logging
import importlib.util
from typing import TYPE_CHECKING, override
import _babase
from babase._appsubsystem import AppSubsystem
if TYPE_CHECKING:
from typing import Any
import babase
class PluginSubsystem(AppSubsystem):
"""Subsystem for plugin handling in the app.
Category: **App Classes**
Access the single shared instance of this class at `ba.app.plugins`.
"""
AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY = 'Auto Enable New Plugins'
AUTO_ENABLE_NEW_PLUGINS_DEFAULT = True
def __init__(self) -> None:
super().__init__()
# Info about plugins that we are aware of. This may include
# plugins discovered through meta-scanning as well as plugins
# registered in the app-config. This may include plugins that
# cannot be loaded for various reasons or that have been
# intentionally disabled.
self.plugin_specs: dict[str, babase.PluginSpec] = {}
# The set of live active plugin objects.
self.active_plugins: list[babase.Plugin] = []
[docs]
@override
def on_app_running(self) -> None:
# Load up our plugins and go ahead and call their on_app_running
# calls.
self.load_plugins()
for plugin in self.active_plugins:
try:
plugin.on_app_running()
except Exception:
from babase import _error
_error.print_exception('Error in plugin on_app_running()')
[docs]
@override
def on_app_suspend(self) -> None:
for plugin in self.active_plugins:
try:
plugin.on_app_suspend()
except Exception:
from babase import _error
_error.print_exception('Error in plugin on_app_suspend()')
[docs]
@override
def on_app_unsuspend(self) -> None:
for plugin in self.active_plugins:
try:
plugin.on_app_unsuspend()
except Exception:
from babase import _error
_error.print_exception('Error in plugin on_app_unsuspend()')
[docs]
@override
def on_app_shutdown(self) -> None:
for plugin in self.active_plugins:
try:
plugin.on_app_shutdown()
except Exception:
from babase import _error
_error.print_exception('Error in plugin on_app_shutdown()')
[docs]
@override
def on_app_shutdown_complete(self) -> None:
for plugin in self.active_plugins:
try:
plugin.on_app_shutdown_complete()
except Exception:
from babase import _error
_error.print_exception(
'Error in plugin on_app_shutdown_complete()'
)
[docs]
def load_plugins(self) -> None:
"""(internal)"""
# Load plugins from any specs that are enabled & able to.
for _class_path, plug_spec in sorted(self.plugin_specs.items()):
plugin = plug_spec.attempt_load_if_enabled()
if plugin is not None:
self.active_plugins.append(plugin)
class PluginSpec:
"""Represents a plugin the engine knows about.
Category: **App Classes**
The 'enabled' attr represents whether this plugin is set to load.
Getting or setting that attr affects the corresponding app-config
key. Remember to commit the app-config after making any changes.
The 'attempted_load' attr will be True if the engine has attempted
to load the plugin. If 'attempted_load' is True for a PluginSpec
but the 'plugin' attr is None, it means there was an error loading
the plugin. If a plugin's api-version does not match the running
app, if a new plugin is detected with auto-enable-plugins disabled,
or if the user has explicitly disabled a plugin, the engine will not
even attempt to load it.
"""
def __init__(self, class_path: str, loadable: bool):
self.class_path = class_path
self.loadable = loadable
self.attempted_load = False
self.plugin: Plugin | None = None
@property
def enabled(self) -> bool:
"""Whether the user wants this plugin to load."""
plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {})
assert isinstance(plugstates, dict)
val = plugstates.get(self.class_path, {}).get('enabled', False) is True
return val
@enabled.setter
def enabled(self, val: bool) -> None:
plugstates: dict[str, dict] = _babase.app.config.setdefault(
'Plugins', {}
)
assert isinstance(plugstates, dict)
plugstate = plugstates.setdefault(self.class_path, {})
plugstate['enabled'] = val
[docs]
def attempt_load_if_enabled(self) -> Plugin | None:
"""Possibly load the plugin and log any errors."""
from babase._general import getclass
from babase._language import Lstr
assert not self.attempted_load
assert self.plugin is None
if not self.enabled:
return None
self.attempted_load = True
if not self.loadable:
return None
try:
cls = getclass(self.class_path, Plugin, True)
except Exception as exc:
_babase.getsimplesound('error').play()
_babase.screenmessage(
Lstr(
resource='pluginClassLoadErrorText',
subs=[
('${PLUGIN}', self.class_path),
('${ERROR}', str(exc)),
],
),
color=(1, 0, 0),
)
logging.exception(
"Error loading plugin class '%s'.", self.class_path
)
return None
try:
self.plugin = cls()
return self.plugin
except Exception as exc:
from babase import _error
_babase.getsimplesound('error').play()
_babase.screenmessage(
Lstr(
resource='pluginInitErrorText',
subs=[
('${PLUGIN}', self.class_path),
('${ERROR}', str(exc)),
],
),
color=(1, 0, 0),
)
logging.exception(
"Error initing plugin class: '%s'.", self.class_path
)
return None
class Plugin:
"""A plugin to alter app behavior in some way.
Category: **App Classes**
Plugins are discoverable by the meta-tag system
and the user can select which ones they want to enable.
Enabled plugins are then called at specific times as the
app is running in order to modify its behavior in some way.
"""
[docs]
def on_app_running(self) -> None:
"""Called when the app reaches the running state."""
[docs]
def on_app_suspend(self) -> None:
"""Called when the app enters the suspended state."""
[docs]
def on_app_unsuspend(self) -> None:
"""Called when the app exits the suspended state."""
[docs]
def on_app_shutdown(self) -> None:
"""Called when the app is beginning the shutdown process."""
[docs]
def on_app_shutdown_complete(self) -> None:
"""Called when the app has completed the shutdown process."""
[docs]
def has_settings_ui(self) -> bool:
"""Called to ask if we have settings UI we can show."""
return False
[docs]
def show_settings_ui(self, source_widget: Any | None) -> None:
"""Called to show our settings UI."""