Source code for masterpiece.application

"""
Author: Juha Meskanen
Date: 2024-10-26
"""

import argparse
import os
import sys
from typing import List, Type, Optional, Union

from typing_extensions import override

from .format import Format
from .argmaestro import ArgMaestro
from .plugmaster import PlugMaster
from .composite import Composite
from .masterpiece import MasterPiece
from .log import Log
from .treevisualizer import TreeVisualizer


[docs] class Application(Composite): """Masterpiece application class. Implements startup argument parsing, plugin management and initialization of class attributes through class specific configuration files. """ plugins: List[Type[MasterPiece]] = [] serialization_file: str = "" serialization_format: str = "JsonFormat" plugin_groups = ["masterpiece"] # plugin group, for Python's module discovery color: str = "yellow" log_level: int = 0 _plugmaster: Optional[PlugMaster] = None _argmaestro: Optional[ArgMaestro] = None _init: bool = False # initialize class configuration files _app_id: str = "masterpiece" # default application id _config = "config" # default configuration def __init__(self, name: str, payload: Optional[MasterPiece] = None) -> None: """Instantiates and initializes. By default, the application log filename is set to the same as the application name. Args: name (str): The name of the application, determining the default log filename. payload (MasterPiece): Playload object associated with this object. """ super().__init__(name, payload)
[docs] @classmethod def get_plugmaster(cls) -> Optional[PlugMaster]: """Fetch the plugmaster object reponsible for plugin management. Returns: PlugMaster: object """ return cls._plugmaster
[docs] @classmethod def set_plugmaster(cls, plugmaster: PlugMaster) -> None: """Set the plugmaster object reponsible for plugin management. Args: plugmaster (PlugMaster): object managing plugins """ cls._plugmaster = plugmaster
[docs] @classmethod def get_argmaestro(cls) -> Optional[ArgMaestro]: """Fetch the plugmaster object reponsible for plugin management. Returns: PlugMaster: object """ return cls._argmaestro
[docs] @classmethod def set_argmaestro(cls, argmaestro: ArgMaestro) -> None: """Set the argmaestro object reponsible for plugin management. Args: argmaestro (PlugMaster): object managing plugins """ cls._argmaestro = argmaestro
[docs] @classmethod def get_configuration_filename(cls, name: str) -> str: """Generate the user specific file name of the configuration file based on the class name. Args: name (str): object managing plugins """ return os.path.join( os.path.expanduser("~"), "." + cls._app_id, cls._config, name )
[docs] @classmethod def save_configuration(cls) -> None: """Create class configuration file, if configuration is enabled and if the file does not exist yet. See --config startup argument. """ filename: str = "undefined" if cls.serialization_format == "": cls.log_warning(f"No serialization format specified, configuration skipped") return cls.log_info(f"Saving configuration") format_class: Type[MasterPiece] = MasterPiece.factory()[ cls.serialization_format ] if format_class is not None and issubclass(format_class, Format): file_ext: str = format_class.file_extension for name, ctor in MasterPiece.factory().items(): if ctor is not None: try: filename = cls.get_configuration_filename(name) + file_ext with open(filename, "w", encoding="utf-8") as f: format = format_class(f) format.save_configuration(ctor) cls.log_info(f"Configuration file {filename} saved") except Exception as e: cls.log_error(f"Error in saving {name}:{filename} {e}")
[docs] @classmethod def load_configuration(cls) -> None: """Load class attributes from a configuration file.""" filename: str = "undefined" if cls.serialization_format == "": cls.log_warning(f"No serialization format specified, configuration skipped") return cls.log_info(f"Loading configuration using {cls.serialization_format}") cls.parse_args() format_class: Type[MasterPiece] = MasterPiece.factory()[ cls.serialization_format ] if format_class is not None and issubclass(format_class, Format): file_ext: str = format_class.file_extension for name, ctor in MasterPiece.factory().items(): if ctor is not None: filename = cls.get_configuration_filename(name) + file_ext try: with open(filename, "r", encoding="utf-8") as f: format = format_class(f) format.load_configuration(ctor) cls.log_info(f"Configuration file {filename} loaded") except Exception as e: cls.log_error(f"Error reading {filename}, {e}")
[docs] @classmethod def parse_args(cls) -> None: """Register classes with ArgMaestro.""" cls._argmaestro = ArgMaestro() for c, clazz in MasterPiece.factory().items(): if clazz is not None: cls._argmaestro.add_class_arguments(clazz) else: cls.log_error(f"None entry in the class factory for {c}") cls._argmaestro.parse_args()
[docs] @classmethod def init_app_id(cls, app_id: str = "myapp") -> None: """ Initialize application id. Parses initial startup that depend on application id. Must be called before any classes are instanced. Arguments: -a, --app (str): Application ID. -c, --config (str): Configuration name, empty string for no configuration -i, --init (bool): Whether to create class configuration files if not already created. """ Application._app_id = app_id parser = argparse.ArgumentParser(add_help=False) parser.add_argument("-a", "--app", type=str, help="Application ID") parser.add_argument("-c", "--config", type=str, help="Configuration") parser.add_argument( "-l", "--log_level", type=str, help="Log level - DEBUG, INFO, WARNING, ERROR", ) parser.add_argument( "-i", "--init", action="store_true", help="Create class configuration files", ) args, remaining_argv = parser.parse_known_args() sys.argv = [sys.argv[0]] + remaining_argv MasterPiece.set_log(Log(app_id, Log.parse_level(args.log_level))) if args.config: Application._config = args.config if args.app: Application._app_id = args.app if args.init: Application._init = args.init cls.log_info("--init requested, creating class configuration files") cls.log_info(f"Configuration files in ~/.{cls._app_id}/{cls._config}") if cls._init: cls.log_info( "--init specified, class configuration files will be created upon exit" )
[docs] @classmethod def register_plugin_group(cls, name: str) -> None: """Registers a new plugin group within the application. Only plugins that match the registered groups will be loaded. By default, all 'masterpiece' plugins are included. Frameworks and apps built on the MasterPiece framework can define more group names, enabling plugins to be developed for any those as well. Args: name (str): The name of the plugin group to be registered """ if not name in cls.plugin_groups: cls.plugin_groups.append(name)
[docs] @classmethod def load_plugins(cls) -> None: """Loads and initializes all plugins for instantiation. This method corresponds to importing Python modules with import clauses.""" if cls._plugmaster is None: cls._plugmaster = PlugMaster(cls.get_app_id()) for g in cls.plugin_groups: cls._plugmaster.load(g)
[docs] def instantiate_plugin_by_name(self, name: str) -> Union[MasterPiece, None]: """Installs the plugin by name, that is, instantiates the plugin class and inserts the instance as child to the application. Args: name (str): name of the plugin class """ if self._plugmaster is None: return None return self._plugmaster.instantiate_class_by_name(self, name)
[docs] def install_plugins(self) -> None: """Installs plugins into the application by invoking the `install()` method of each loaded plugin module. **Note:** This method is intended for testing and debugging purposes only. In a typical use case, the application should handle the instantiation of classes and manage their attributes as needed. """ if self._plugmaster is None: self._plugmaster = PlugMaster(self.get_app_id()) self._plugmaster.install(self)
[docs] def deserialize(self) -> None: """Deserialize instances from the startup file specified by 'serialization_file' class attribute, or '--file' startup argument. """ if self.serialization_file != "" and self.serialization_format != "": self.info( f"Deserializing masterpieces from {self.serialization_file} using {self.serialization_format}" ) format_class: Type[MasterPiece] = MasterPiece.factory()[ self.serialization_format ] if issubclass(format_class, Format): with open(self.serialization_file, "r", encoding="utf-8") as f: format = format_class(f) format.deserialize(self) self.info(f"File {self.serialization_file} successfully read") else: raise TypeError( f"{self.serialization_format} is not a subclass of Format" ) else: self.warning( f"No deserialization this time, --serialization_file not specified" )
[docs] def serialize(self) -> None: """Serialize application state to the file specified by 'serialization_file' class attribute'. """ if self.serialization_file != "": self.info( f"Saving masterpieces to {self.serialization_file} of type {self.serialization_format}" ) format_class: Type[MasterPiece] = MasterPiece.factory()[ self.serialization_format ] if issubclass(format_class, Format): with open(self.serialization_file, "w", encoding="utf-8") as f: format = format_class(f) format.serialize(self) self.info(f"File {self.serialization_file} successfully written") else: raise TypeError( f"{self.serialization_format} is not a subclass of Format" ) else: self.warning("No serialization this time, --serialization_file not set")
[docs] @classmethod def get_app_id(cls) -> str: """Fetch the application id. Application id determines the folder in which the configuration files for classes are held. Note that a single package can ship with more than just one executable application, all with the same application id. ..todo: Application id '_app_id' is prefixed with '_' to signal that it is a private attribute (python) and that should not be serialized (masterpiece). Isn't there something like @transient in Python? App id needs to be accessed outside, which is why this get_app_id() method is needed. Returns: str: application id determign application registry for class attribute serialization """ return cls._app_id
@override def run(self) -> None: if self._init: self.save_configuration() else: super().run() @override def run_forever(self) -> None: if self._init: self.save_configuration() else: self.load_configuration() super().run_forever()
[docs] def print(self) -> None: """ Print the instance hierarchy of the application using `TreeVisualizer` """ visualizer1 = TreeVisualizer(self.color) visualizer1.print_tree(self)