Source code for mdadash.backend.widgets.base
"""
Base Class for Widgets and Widget Manager
"""
from abc import ABC, abstractmethod
from contextlib import contextmanager
from uuid import uuid1
import IPython
import MDAnalysis as mda
[docs]
class WidgetBase(ABC):
"""WidgetBase
This is the base class for all widgets.
"""
def __init_subclass__(cls, **kwargs):
"""Register any derived class with the WidgetManager"""
super().__init_subclass__(**kwargs)
WidgetManager.register_class(cls)
[docs]
class WidgetManager:
"""WidgetManager
This is the manager that manager all widgets.
"""
_classes = {}
_instances = {}
def __init__(self):
pass
@property
def classes(self):
"""Dictionary of registered widget classes keyed by widget name"""
return self._classes
@property
def instances(self):
"""Dictionary of widget instances keyed by widget uuid"""
return self._instances
[docs]
@classmethod
def register_class(cls, widget_class: WidgetBase) -> None:
"""Register widget class
Parameters
----------
widget_class
A widget class that is derived from WidgetBase
"""
cls._validate_widget_class(widget_class)
widget_name = widget_class.name
if widget_name in cls._classes:
raise ValueError(f"Widget name '{widget_name}' already registered")
cls._classes[widget_name] = widget_class
@classmethod
def _validate_widget_class(cls, widget_class: WidgetBase) -> None:
"""Internal method to validate a widget class"""
if not issubclass(widget_class, WidgetBase):
raise ValueError(f"{widget_class} is not a widget class")
if not hasattr(widget_class, "name"):
raise ValueError("name not specified in widget class")
if widget_class.run.__qualname__ == "WidgetBase.run":
raise ValueError("run method not found in class")
# TODO: add more validations
[docs]
def add_widget_instance(self, widget_name: str) -> str | None:
"""Add widget instance
Add a widget instance based on the widget name already
registered with the manager.
Parameters
----------
widget_name: str
Name of the widget class registered
Returns
-------
uuid of instance added or None
"""
if widget_name in self.classes:
widget_class = self.classes[widget_name]
uuid = str(uuid1())
self.instances[uuid] = widget_class()
return uuid
return None
[docs]
def delete_widget_instance(self, uuid: str) -> str | None:
"""Remove widget instance
Remove widget instanced based on uuid returned during
the instance creation using :meth:`add_widget_instance`
Parameters
----------
uuid: str
The uuid of the instance to be removed
Returns
-------
uuid of instance deleted or None
"""
if uuid in self.instances:
del self.instances[uuid]
return uuid
return None
[docs]
def run_widgets(self, u: mda.Universe) -> None:
"""Run widget instances
Parameters
----------
u: mda.Universe
The universe object to use for analysis
"""
for uuid, widget in self.instances.items():
with _widget_uuid_in_metadata(uuid):
widget.run(u)
@contextmanager
def _widget_uuid_in_metadata(uuid: str):
"""Internal: Add uuid in content metadata sent from kernel"""
session = IPython.get_ipython().kernel.session
original_send = session.send
def patched_send(stream, msg_type_or_msg, *args, **kwargs):
msg_type = None
content = None
if isinstance(msg_type_or_msg, str):
msg_type = msg_type_or_msg
if len(args) > 0 and isinstance(args[0], dict):
content = args[0]
elif isinstance(msg_type_or_msg, dict):
msg_type = msg_type_or_msg.get("msg_type") or msg_type_or_msg.get(
"header", {}
).get("msg_type")
content = msg_type_or_msg.get("content", msg_type_or_msg)
# Add widget uuid to metadata
if msg_type == "display_data" and content is not None:
content["metadata"]["widget_uuid"] = uuid
return original_send(stream, msg_type_or_msg, *args, **kwargs)
session.send = patched_send
try:
yield
finally:
session.send = original_send