Source code for magicclass.core

from __future__ import annotations
from functools import wraps as functools_wraps
import inspect
from weakref import WeakValueDictionary
from typing import Any, TYPE_CHECKING

from ._gui.class_gui import (
    ClassGuiBase,
    ClassGui,
    FrameClassGui,
    GroupBoxClassGui,
    MainWindowClassGui,
    SubWindowsClassGui,
    ScrollableClassGui,
    DraggableClassGui,
    ButtonClassGui,
    CollapsibleClassGui,
    HCollapsibleClassGui,
    SplitClassGui,
    TabbedClassGui,
    StackedClassGui,
    ToolBoxClassGui,
    ListClassGui,
)
from ._gui._base import (
    PopUpMode,
    ErrorMode,
    defaults,
    MagicTemplate,
    check_override,
    convert_attributes,
)
from ._gui import ContextMenuGui, MenuGui, ToolBarGui
from ._app import get_app
from .types import WidgetType
from . import _register  # activate type registration things.

if TYPE_CHECKING:
    from .stylesheets import StyleSheet
    from ._gui import MenuGuiBase
    from ._gui._function_gui import FunctionGuiPlus
    from .types import WidgetTypeStr, PopUpModeStr, ErrorModeStr
    from .help import HelpWidget
    from macrokit import Macro

_BASE_CLASS_SUFFIX = "_Base"

_TYPE_MAP = {
    WidgetType.none: ClassGui,
    WidgetType.scrollable: ScrollableClassGui,
    WidgetType.draggable: DraggableClassGui,
    WidgetType.split: SplitClassGui,
    WidgetType.collapsible: CollapsibleClassGui,
    WidgetType.hcollapsible: HCollapsibleClassGui,
    WidgetType.button: ButtonClassGui,
    WidgetType.toolbox: ToolBoxClassGui,
    WidgetType.tabbed: TabbedClassGui,
    WidgetType.stacked: StackedClassGui,
    WidgetType.list: ListClassGui,
    WidgetType.groupbox: GroupBoxClassGui,
    WidgetType.frame: FrameClassGui,
    WidgetType.subwindows: SubWindowsClassGui,
    WidgetType.mainwindow: MainWindowClassGui,
}


[docs]def magicclass( class_: type | None = None, *, layout: str = "vertical", labels: bool = True, name: str = None, visible: bool | None = None, close_on_run: bool = None, popup_mode: PopUpModeStr | PopUpMode = None, error_mode: ErrorModeStr | ErrorMode = None, widget_type: WidgetTypeStr | WidgetType = WidgetType.none, icon_path: str = None, stylesheet: str | StyleSheet = None, ): """ Decorator that can convert a Python class into a widget. .. code-block:: python @magicclass class C: ... ui = C() ui.show() # open GUI Parameters ---------- class_ : type, optional Class to be decorated. layout : str, "vertical" or "horizontal", default is "vertical" Layout of the main widget. labels : bool, default is True If true, magicgui labels are shown. name : str, optional Name of GUI. visible : bool, optional Initial visibility of GUI. Useful when magic class is nested. close_on_run : bool, default is True If True, magicgui created by every method will be deleted after the method is completed without exceptions, i.e. magicgui is more like a dialog. popup : bool, default is True Deprecated. popup_mode : str or PopUpMode, default is PopUpMode.popup Option of how to popup FunctionGui widget when a button is clicked. error_mode : str or ErrorMode, default is ErrorMode.msgbox Option of how to raise errors during function calls. widget_type : WidgetType or str, optional Widget type of container. icon_path : str, optional Path to the icon image. stylesheet : str or StyleSheet object, optional Set stylesheet to the widget if given. Returns ------- Decorated class or decorator. """ if popup_mode is None: popup_mode = defaults["popup_mode"] if close_on_run is None: close_on_run = defaults["close_on_run"] if error_mode is None: error_mode = defaults["error_mode"] if isinstance(widget_type, str): widget_type = widget_type.lower() widget_type = WidgetType(widget_type) def wrapper(cls) -> type[ClassGui]: if not isinstance(cls, type): raise TypeError(f"magicclass can only wrap classes, not {type(cls)}") class_gui = _TYPE_MAP[widget_type] if not issubclass(cls, MagicTemplate): check_override(cls) # get class attributes first doc = cls.__doc__ sig = inspect.signature(cls) annot = cls.__dict__.get("__annotations__", {}) mod = cls.__module__ qualname = cls.__qualname__ new_attrs = convert_attributes(cls, hide=class_gui.__mro__) oldclass = type(cls.__name__ + _BASE_CLASS_SUFFIX, (cls,), {}) newclass = type(cls.__name__, (class_gui, oldclass), new_attrs) newclass.__signature__ = sig newclass.__doc__ = doc newclass.__module__ = mod newclass.__qualname__ = qualname # concatenate annotations newclass.__annotations__ = class_gui.__annotations__.copy() newclass.__annotations__.update(annot) @functools_wraps(oldclass.__init__) def __init__(self: MagicTemplate, *args, **kwargs): # Without "app = " Jupyter freezes after closing the window! app = get_app() gui_kwargs = dict( layout=layout, labels=labels, name=name or cls.__name__, visible=visible, close_on_run=close_on_run, popup_mode=PopUpMode(popup_mode), error_mode=ErrorMode(error_mode), ) # Inheriting Container's constructor is the most intuitive way. if kwargs and "__init__" not in cls.__dict__: gui_kwargs.update(kwargs) kwargs = {} class_gui.__init__( self, **gui_kwargs, ) # prepare macro macrowidget = self.macro.widget.native macrowidget.setParent(self.native, macrowidget.windowFlags()) self.macro.widget.__magicclass_parent__ = self with self.macro.blocked(): super(oldclass, self).__init__(*args, **kwargs) self._convert_attributes_into_widgets() if widget_type in (WidgetType.collapsible, WidgetType.button): self.text = self.name if icon_path: self.icon_path = icon_path if stylesheet: self.native.setStyleSheet(str(stylesheet)) if hasattr(self, "__post_init__"): with self.macro.blocked(): self.__post_init__() newclass.__init__ = __init__ # Users may want to override repr newclass.__repr__ = oldclass.__repr__ return newclass if class_ is None: return wrapper else: return wrapper(class_)
[docs]def magicmenu( class_: type = None, *, close_on_run: bool = None, popup_mode: str | PopUpMode = None, error_mode: str | ErrorMode = None, labels: bool = True, name: str | None = None, icon_path: str = None, ): """ Decorator that can convert a Python class into a menu bar. """ return _call_magicmenu(**locals(), menugui_class=MenuGui)
[docs]def magiccontext( class_: type = None, *, close_on_run: bool = None, popup_mode: str | PopUpMode = None, error_mode: str | ErrorMode = None, labels: bool = True, name: str | None = None, icon_path: str = None, ): """ Decorator that can convert a Python class into a context menu. """ return _call_magicmenu(**locals(), menugui_class=ContextMenuGui)
[docs]def magictoolbar( class_: type = None, *, close_on_run: bool = None, popup_mode: str | PopUpMode = None, error_mode: str | ErrorMode = None, labels: bool = True, name: str | None = None, icon_path: str = None, ): """ Decorator that can convert a Python class into a menu bar. """ return _call_magicmenu(**locals(), menugui_class=ToolBarGui)
[docs]class MagicClassFactory: """ Factory class that can make any magic-class. """ def __init__( self, name: str, layout: str = "vertical", labels: bool = True, close_on_run: bool = None, popup_mode: str | PopUpMode = None, error_mode: str | ErrorMode = None, widget_type: str | WidgetType = WidgetType.none, icon_path: str = None, attrs: dict[str, Any] | None = None, ): self.name = name self.layout = layout self.labels = labels self.close_on_run = close_on_run self.popup_mode = popup_mode self.error_mode = error_mode self.widget_type = widget_type self.icon_path = icon_path self.attrs = attrs
[docs] def as_magicclass(self) -> ClassGuiBase: _cls = type(self.name, (), self.attrs) cls = magicclass( _cls, layout=self.layout, labels=self.labels, close_on_run=self.close_on_run, name=self.name, popup_mode=self.popup_mode, error_mode=self.error_mode, widget_type=self.widget_type, icon_path=self.icon_path, ) return cls
[docs] def as_magictoolbar(self) -> ToolBarGui: _cls = type(self.name, (), self.attrs) cls = magictoolbar( _cls, close_on_run=self.close_on_run, popup_mode=self.popup_mode, error_mode=self.error_mode, labels=self.labels, icon_path=self.icon_path, ) return cls
[docs] def as_magicmenu(self) -> MenuGui: _cls = type(self.name, (), self.attrs) cls = magicmenu( _cls, close_on_run=self.close_on_run, popup_mode=self.popup_mode, error_mode=self.error_mode, labels=self.labels, icon_path=self.icon_path, ) return cls
[docs] def as_magiccontext(self) -> ContextMenuGui: _cls = type(self.name, (), self.attrs) cls = magiccontext( _cls, close_on_run=self.close_on_run, popup_mode=self.popup_mode, error_mode=self.error_mode, labels=self.labels, icon_path=self.icon_path, ) return cls
def _call_magicmenu( class_: type = None, close_on_run: bool = True, popup_mode: str | PopUpMode = None, error_mode: str | ErrorMode = None, labels: bool = True, name: str = None, icon_path: str = None, menugui_class: type[MenuGuiBase] = None, ): """ Parameters ---------- class_ : type, optional Class to be decorated. close_on_run : bool, default is True If True, magicgui created by every method will be deleted after the method is completed without exceptions, i.e. magicgui is more like a dialog. popup : bool, default is True If True, magicgui created by every method will be poped up, else they will be appended as a part of the main widget. Returns ------- Decorated class or decorator. """ if popup_mode is None: popup_mode = defaults["popup_mode"] if close_on_run is None: close_on_run = defaults["close_on_run"] if error_mode is None: error_mode = defaults["error_mode"] if popup_mode in ( PopUpMode.above, PopUpMode.below, PopUpMode.first, PopUpMode.last, ): raise ValueError(f"Mode {popup_mode.value} is not compatible with Menu.") def wrapper(cls) -> type[menugui_class]: if not isinstance(cls, type): raise TypeError(f"magicclass can only wrap classes, not {type(cls)}") if not issubclass(cls, MagicTemplate): check_override(cls) # get class attributes first doc = cls.__doc__ sig = inspect.signature(cls) mod = cls.__module__ qualname = cls.__qualname__ new_attrs = convert_attributes(cls, hide=menugui_class.__mro__) oldclass = type(cls.__name__ + _BASE_CLASS_SUFFIX, (cls,), {}) newclass = type(cls.__name__, (menugui_class, oldclass), new_attrs) newclass.__signature__ = sig newclass.__doc__ = doc newclass.__module__ = mod newclass.__qualname__ = qualname @functools_wraps(oldclass.__init__) def __init__(self: MagicTemplate, *args, **kwargs): # Without "app = " Jupyter freezes after closing the window! app = get_app() gui_kwargs = dict( close_on_run=close_on_run, popup_mode=PopUpMode(popup_mode), error_mode=ErrorMode(error_mode), labels=labels, name=name or cls.__name__, ) # Inheriting Container's constructor is the most intuitive way. if kwargs and "__init__" not in cls.__dict__: gui_kwargs.update(kwargs) kwargs = {} menugui_class.__init__( self, **gui_kwargs, ) macrowidget = self.macro.widget.native macrowidget.setParent(self.native, macrowidget.windowFlags()) self.macro.widget.__magicclass_parent__ = self with self.macro.blocked(): super(oldclass, self).__init__(*args, **kwargs) self._convert_attributes_into_widgets() if icon_path: self.icon_path = icon_path if hasattr(self, "__post_init__"): with self.macro.blocked(): self.__post_init__() newclass.__init__ = __init__ # Users may want to override repr newclass.__repr__ = oldclass.__repr__ return newclass return wrapper if class_ is None else wrapper(class_) magicmenu.__doc__ += _call_magicmenu.__doc__ magiccontext.__doc__ += _call_magicmenu.__doc__ _HELPS: WeakValueDictionary[int, MagicTemplate] = WeakValueDictionary()
[docs]def build_help(ui: MagicTemplate, parent=None) -> HelpWidget: """ Build a widget for user guide. Once it is built, widget will be cached. Parameters ---------- ui : MagicTemplate Magic class UI object. Returns ------- HelpWidget Help of the input UI. """ ui_id = id(ui) if ui_id in _HELPS.keys(): help_widget = _HELPS[ui_id] else: from .help import HelpWidget if parent is None: parent = ui.native help_widget = HelpWidget(ui, parent=parent) _HELPS[ui_id] = help_widget return help_widget
[docs]def get_function_gui(ui: MagicTemplate, name: str) -> FunctionGuiPlus: """ Get the FunctionGui object hidden beneath push button or menu. This function is a helper function for magicclass. Parameters ---------- ui : MagicTemplate Any of a magic-class instance. name : str Name of method (or strictly speaking, the name of PushButton). Returns ------- FunctionGuiPlus FunctionGui object. """ func = getattr(ui, name) widget = ui[name] if not hasattr(widget, "mgui"): raise TypeError(f"Widget {widget} does not have FunctionGui inside it.") if widget.mgui is not None: return widget.mgui from ._gui._base import _build_mgui, _create_gui_method func = _create_gui_method(ui, func) mgui = _build_mgui(widget, func, ui) return mgui
[docs]def redo(ui: MagicTemplate, index: int = -1) -> None: """ Redo operation on GUI using recorded macro. Parameters ---------- ui : MagicTemplate Target magic-class widget. index : int, default is -1 Which execution will be redone. Any object that support list slicing can be used. By default the last operation will be redone. """ line = ui.macro[index] try: line.eval({"ui": ui}) except Exception as e: msg = e.args[0] msg = f"Caused by >>> {line}. {msg}" raise e return None
[docs]def update_widget_state(ui: MagicTemplate, macro: Macro | str | None = None) -> None: """ Update widget values based on a macro. This helper function works similar to the ``update_widget`` method of ``FunctionGui``. In most cases, this function will be used for restoring a state from a macro recorded before. Value changed signal will not be emitted within this operation. Parameters ---------- ui : MagicTemplate Magic class instance. macro : Macro or str, optional An executable macro or string that dictates how GUI will be updated. """ from macrokit import Head, Expr, Macro if macro is None: macro = ui.macro elif isinstance(macro, str): s = macro macro = Macro() for line in s.split("\n"): macro.append(line) elif not isinstance(macro, Macro): raise TypeError( f"The second argument must be a Macro or str, got {type(macro)}." ) for expr in macro: if expr.head == Head.call: # ui.func(...) ui_f, *arguments = expr.args fname = str(ui_f.args[1]) if fname.startswith("_"): if fname != "_call_with_return_callback": continue args, kwargs = _arguments_to_values(arguments) fgui = get_function_gui(ui, args[0]) else: args, kwargs = _arguments_to_values(arguments) fgui = get_function_gui(ui, fname) with fgui.changed.blocked(): for key, value in kwargs.items(): getattr(fgui, key).value = value elif expr.head == Head.assign: # ui.field.value = ... # ui.vfield = ... expr.eval({}, {str(ui._my_symbol): ui}) return None
def _tuple(*args) -> tuple: return args def _arguments_to_values(arguments) -> tuple[tuple, dict[str, Any]]: from macrokit import Head, Expr, symbol for i, arg in enumerate(arguments): if isinstance(arg, Expr) and arg.head == Head.kw: break args = arguments[:i] kwargs: list[Expr] = arguments[i:] args = Expr(Head.call, [_tuple] + args).eval({symbol(_tuple): _tuple}) kwargs = Expr(Head.call, [dict] + kwargs).eval() return args, kwargs
[docs]class Parameters: def __init__(self): self.__name__ = self.__class__.__name__ self.__qualname__ = self.__class__.__qualname__ sig = [ inspect.Parameter(name="self", kind=inspect.Parameter.POSITIONAL_OR_KEYWORD) ] for name, attr in inspect.getmembers(self): if name.startswith("__") or callable(attr): continue sig.append( inspect.Parameter( name=name, kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, default=attr, ) ) if hasattr(self.__class__, "__annotations__"): annot = self.__class__.__annotations__ for name, t in annot.items(): sig.append( inspect.Parameter( name=name, kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=t, ) ) self.__signature__ = inspect.Signature(sig) def __call__(self, *args) -> None: params = list(self.__signature__.parameters.keys())[1:] for a, param in zip(args, params): setattr(self, param, a)
[docs] def as_dict(self) -> dict[str, Any]: """ Convert parameter fields into a dictionary. .. code-block:: python class params(Parameters): i = 1 j = 2 p = params() p.as_dict() # {"i": 1, "j": 2} """ params = list(self.__signature__.parameters.keys())[1:] return {param: getattr(self, param) for param in params}