MainModel

class poriscope.models.main_model.MainModel(app_config: Dict[str, Any])

Bases: QObject

App-shell model: owns app configuration (loaded from/saved to config.json), discovers and holds every available plugin class under poriscope/plugins/ and the user plugin folder, and persists/restores session and tab-action history.

clear_cache() None

Truncate the app’s log file (flushing any buffered log data first).

get_plugin_data(plugin_key: str) Dict[str, Any]

Fetches plugin data from the local application data JSON file.

Args:

plugin_key (str): The key representing the plugin to retrieve data for.

Returns:

dict: Plugin data if available, otherwise returns an empty dictionary.

load_plugin(plugin_key: str, folder: str | Path, allowed_base_classes: Tuple[type, ...]) type | None

Dynamically loads a plugin, ensuring it is a subclass of a supported abstract class.

Args:

plugin_key (str): The key representing the plugin to load.

Returns:

plugin_class (type): The loaded plugin class, or None if loading fails.

Note:

This method uses dynamic module loading as described in the Python documentation: https://docs.python.org/3/library/importlib.html

The simple-plugin-loader package was initially considered but was found to be unsuitable for on-demand loading as it loads all plugins upon execution: https://pypi.org/project/simple-plugin-loader/

populate_available_plugins() Tuple[Dict[str, Dict[str, type]], Dict[str, List[str]]]

Get a dict of available plugin names, keyed by base class. Each entry in the dict is a list of plugin class names. Built at runtime by searching plugin directories.

refresh_available_plugins() None

Re-scan the plugin directories and replace the cached results.

The scan otherwise runs once, in the constructor, so a user who points the app at a different plugin folder sees no change until the next launch. Each plugin file is loaded fresh via importlib.util.spec_from_file_location/exec_module rather than through sys.modules, so an edited file’s new code is always picked up on the next scan - but that also means every scan hands back a new class object, never the one a previous scan produced. This does not break anything already instantiated: an instance keeps working through its own __class__ reference regardless of what this cache holds, it just does not become an instance of the freshly-scanned class - the two are distinct objects until nothing references the older one any more. Files that have since been deleted simply are not walked, and so drop out.

Callers are responsible for propagating the new lists - the controllers and the view each hold a copy taken at construction.

reset_app_config() Dict[str, Any]

Restore the three stored settings to their defaults and persist them.

Only config/config.json is touched. Saved sessions, plugin history and log files are left alone.

This writes the file and updates the in-memory config, but does not apply the values to anything already running: reverting the parent folder has to reach live data plugins, and reverting the log level has to reconfigure the logger. MainController.reset_app_config routes the returned values back through the same paths a manual edit uses, so those side effects are not duplicated here.

Returns:

The defaults that were applied, so the caller can act on them.

Return type:

Dict[str, Any]

save_session(plugin_history: Dict[str, Any], save_file: str | Path | None = None) None

Write the plugin history to disk as JSON

A write failure is logged rather than raised. Every caller is a Qt slot, and PySide6 does not tolerate an exception escaping a slot invoked from C++, so a read-only or otherwise unwritable destination would take the process down. except Exception rather than except OSError because json.dump also raises TypeError for a value it cannot serialize.

The level depends on who asked for the save. An autosave - save_file is None, which includes the app-shutdown path - logs at WARNING, which QtHandler does not raise a dialog for; a modal dialog during aboutToQuit would be its own bug. It also emits add_text_to_display, so the status panel says autosaving has stopped: the failure is rare and non-blocking, but a user whose work is no longer being persisted needs to know they are operating unprotected. A save to a path the user chose logs at ERROR instead, and therefore does raise a dialog, so that a Save Session which did not happen is not mistaken for one that did.

Parameters:
  • plugin_history (Dict[str, Any]) – the session state to persist

  • save_file (Optional[Union[str, Path]]) – path to write to, or None to use the default session file

save_tab_actions(plugin_history: Dict[str, Any], save_file: str | Path | None = None) None

Write the tab action history to disk as JSON

Failures are handled exactly as in save_session(), and for the same reasons: logged rather than raised because the callers are Qt slots, at WARNING plus a status-panel message for an autosave, and at ERROR for a save to a path the user chose.

Parameters:
  • plugin_history (Dict[str, Any]) – the tab action history to persist

  • save_file (Optional[Union[str, Path]]) – path to write to, or None to use the default session file