MainModel¶
- class poriscope.models.main_model.MainModel(app_config: Dict[str, Any])¶
Bases:
QObjectApp-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.
- ALLOWED_BASE_CLASSES: Dict[str, type] = {'MetaController': <class 'poriscope.utils.MetaController.MetaController'>, 'MetaDatabaseLoader': <class 'poriscope.utils.MetaDatabaseLoader.MetaDatabaseLoader'>, 'MetaDatabaseWriter': <class 'poriscope.utils.MetaDatabaseWriter.MetaDatabaseWriter'>, 'MetaEventFinder': <class 'poriscope.utils.MetaEventFinder.MetaEventFinder'>, 'MetaEventFitter': <class 'poriscope.utils.MetaEventFitter.MetaEventFitter'>, 'MetaEventLoader': <class 'poriscope.utils.MetaEventLoader.MetaEventLoader'>, 'MetaFilter': <class 'poriscope.utils.MetaFilter.MetaFilter'>, 'MetaModel': <class 'poriscope.utils.MetaModel.MetaModel'>, 'MetaReader': <class 'poriscope.utils.MetaReader.MetaReader'>, 'MetaView': <class 'poriscope.utils.MetaView.MetaView'>, 'MetaWriter': <class 'poriscope.utils.MetaWriter.MetaWriter'>}¶
The plugin families the app recognises, and the base class that identifies each. A file is a plugin when it subclasses one of these. A class attribute rather than rebuilt per call, since refresh_available_plugins makes discovery a repeated operation and the mapping never varies.
- REPLACE_ATTEMPTS = 5¶
How many times a session write retries a rename Windows briefly refuses.
- load_plugin(plugin_key: str, folder: str | Path, allowed_base_classes: Tuple[type, ...]) type | None¶
Import one plugin file and return its class if it subclasses an allowed base.
The file
<plugin_key>.pyinfolderis executed as a module withimportlib, one plugin at a time, and the class of the same name is taken from it. Thesimple-plugin-loaderpackage was considered and rejected because it loads every plugin at once. Any failure - a missing file, an import error, no class of that name, or a class outsideallowed_base_classes- is logged and yieldsNonerather than raising.- Parameters:
- Returns:
The plugin class, or
Noneif it could not be loaded or is not a plugin.- Return type:
Optional[type]
- load_session(file_name: str | Path | None = None) Dict[str, Any] | None¶
Read a saved session, or None if the file is missing, unreadable, or not a session.
A session maps each key to an entry carrying
metaclassandsubclass, as every writer of the history produces; anything else - a tab action history from the same folder, a config file - is refused here, before the caller resets the workspace to apply it. The caller reports a refusal to the user.
- populate_available_plugins() Tuple[Dict[str, Dict[str, type]], Dict[str, List[str]]]¶
Find every plugin the app can offer, keyed by the family it belongs to.
Walks the shipped plugin tree and then the user’s plugin folder, importing each file to see what it subclasses.
Plugin names are unique across the whole app rather than per family, so the duplicate check here is keyed by name alone. Built-ins are walked first, so a user file of the same name is the one rejected - without that, it silently replaced the shipped plugin and there was no way to tell which had run.
- 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_modulerather than throughsys.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.
- replace_class_names_with_classes(d: ~typing.Any, class_dict: ~typing.Mapping[str, ~typing.Any] = {'bool': <class 'bool'>, 'float': <class 'float'>, 'int': <class 'int'>, 'str': <class 'str'>}) None¶
Turn the type names written into session JSON back into real types.
Only under the keys that actually hold a type. Matching on the string alone converted any setting whose value happened to read “float” into <class ‘float’>, so a plugin configured with Event Type: “float” came back corrupted. The key is what distinguishes a serialised type from a string that looks like one, because the save side only ever writes a type name in place of a type.
A name the map does not know is left exactly as written.
Only dict values are walked, matching the save side.
- Parameters:
d (Any) – The settings tree, edited in place.
class_dict (Mapping[str, Any]) – The names to restore, and the types to restore them to.
- replace_classes_with_class_names(d: Any) None¶
Replace every type in a settings tree with its name, so it can be written as JSON.
A plugin setting carries a real type under Type - float, str and so on - and JSON cannot hold one, so it is written as its name and turned back by replace_class_names_with_classes on load.
This converts a type under any key, not only the expected ones, so that no save can fail on a value json.dump cannot serialise. It warns about the unexpected ones, because the load side will not convert those back: the asymmetry is deliberate, and the warning is what stops it being silent.
Only dict values are walked. A list nested in a dict is not visited - the branch that once claimed to do so was unreachable from every caller, and nothing needs it: Options holds plugin names, not types.
- Parameters:
d (Any) – The settings tree, edited in place.
- reset_app_config() Dict[str, Any]¶
Restore the three stored settings to their defaults and persist them.
Only
config/config.jsonis 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_configroutes 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 Exceptionrather thanexcept OSErrorbecausejson.dumpalso raisesTypeErrorfor a value it cannot serialize.The level depends on who asked for the save. An autosave -
save_fileisNone, which includes the app-shutdown path - logs at WARNING, whichQtHandlerdoes not raise a dialog for; a modal dialog duringaboutToQuitwould be its own bug. It also emitsadd_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.
- 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.