Skip to content

cylindra.plugin

This submodule includes functions for plugin management.

CylindraPluginFunction

Source code in cylindra/plugin/function.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class CylindraPluginFunction(Generic[_P, _R]):
    def __init__(
        self,
        func: Callable[_P, _R],
        name: str | None = None,
        module: str | None = None,
        record: bool = True,
    ):
        from magicclass.utils import thread_worker

        if not callable(func):
            raise TypeError("func must be a callable")
        if not hasattr(func, "__name__"):
            raise ValueError("func must have a __name__ attribute.")
        if name is None:
            name = func.__name__.replace("_", " ").capitalize()
        self._name = name
        if module is None:
            module = func.__module__
        self._is_recordable = record
        if module == "__main__" and record:
            warnings.warn(
                f"Plugin function {func!r} is in the top-level module '__main__', "
                "which means it is only defined during this session. Calls of this "
                "function will be recorded in the macro but the script will not work. "
                "Add 'record=False' to the `register_function` decorator, or define "
                "plugin function in a separate module.",
                UserWarning,
                stacklevel=2,
            )
        self._module = module
        wraps(func)(self)
        self._func = func
        self._action_ref: Callable[[], Action | None] = lambda: None
        self.__signature__ = inspect.signature(func)
        first_arg = next(iter(self.__signature__.parameters.values()))
        self._ui_arg_name = first_arg.name
        # check if the first argument is a CylindraMainWidget
        if first_arg.annotation is not inspect.Parameter.empty:
            from cylindra.widgets import CylindraMainWidget

            if first_arg.annotation not in [CylindraMainWidget, "CylindraMainWidget"]:
                warnings.warn(
                    f"The first argument of a plugin function {func!r} should be a "
                    f"CylindraMainWidget but was {first_arg.annotation!r}.",
                    UserWarning,
                    stacklevel=2,
                )

        if isinstance(self._func, thread_worker):
            if record:
                self._func._set_recorder(self._record_macro)
            else:
                self._func._set_silencer()
            self._func._is_running = self._is_running  # patch method

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}<{self._name}>"

    def import_statement(self) -> str:
        """Make an import statement for the plugin"""
        expr = f"import {self._module}"
        try:
            ast.parse(expr)
        except SyntaxError:
            raise ValueError(f"Invalid import statement: {expr}") from None
        return expr

    def update_module(self, mod: ModuleType):
        """Update the module name of the plugin function"""
        self._module = mod.__name__
        return self

    def as_method(self, ui):
        """As a method bound to the given CylindraMainWidget instance."""
        from magicclass.signature import upgrade_signature

        def _method(*args: _P.args, **kwargs: _P.kwargs) -> _R:
            return self(ui, *args, **kwargs)

        params = list(self.__signature__.parameters.values())
        aopt = getattr(self.__signature__, "additional_options", None)
        _method.__signature__ = inspect.Signature(params[1:])
        _method.__name__ = self._name
        _method.__doc__ = getattr(self._func, "__doc__", "")
        if qualname := getattr(self._func, "__qualname__", None):
            _method.__qualname__ = qualname
        upgrade_signature(_method, additional_options=aopt)
        return _method

    def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
        from magicclass.utils import thread_worker

        from cylindra.widgets import CylindraMainWidget

        bound = self.__signature__.bind(*args, **kwargs)
        bound.apply_defaults()
        ui = bound.arguments[self._ui_arg_name]
        if not isinstance(ui, CylindraMainWidget):
            raise TypeError(
                f"Expected a CylindraMainWidget instance as the first argument "
                f"{self._ui_arg_name!r} but got {ui!r}"
            )
        first_arg, *args = bound.args
        assert first_arg is ui
        if isinstance(self._func, thread_worker):
            out = self._func.__get__(ui)(*args, **bound.kwargs)
        else:
            with ui.macro.blocked():
                out = self._func(*bound.args, **bound.kwargs)

            # macro recording
            if self._is_recordable:
                out = self._record_macro(ui, out, *args, **bound.kwargs)

        return out

    def _record_macro(
        self,
        ui: CylindraMainWidget,
        out,
        *args,
        **kwargs,
    ):
        from magicclass.undo import UndoCallback

        fn_expr = Expr("getattr", [Symbol(self._module), self._func.__name__])
        expr = Expr.parse_call(fn_expr, (ui,) + args, kwargs)
        ui.macro.append(expr)
        ui.macro._last_setval = None
        if self not in ui._plugins_called:
            ui._plugins_called.append(self)
        if isinstance(out, UndoCallback):
            ui.macro._append_undo(out.with_name(str(expr)))
            out = out.return_value
        else:
            ui.macro.clear_undo_stack()
        return out

    def _is_running(self, gui: CylindraMainWidget) -> bool:
        if action := self._action_ref():
            return action.running
        return False

as_method(ui)

As a method bound to the given CylindraMainWidget instance.

Source code in cylindra/plugin/function.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def as_method(self, ui):
    """As a method bound to the given CylindraMainWidget instance."""
    from magicclass.signature import upgrade_signature

    def _method(*args: _P.args, **kwargs: _P.kwargs) -> _R:
        return self(ui, *args, **kwargs)

    params = list(self.__signature__.parameters.values())
    aopt = getattr(self.__signature__, "additional_options", None)
    _method.__signature__ = inspect.Signature(params[1:])
    _method.__name__ = self._name
    _method.__doc__ = getattr(self._func, "__doc__", "")
    if qualname := getattr(self._func, "__qualname__", None):
        _method.__qualname__ = qualname
    upgrade_signature(_method, additional_options=aopt)
    return _method

import_statement()

Make an import statement for the plugin

Source code in cylindra/plugin/function.py
80
81
82
83
84
85
86
87
def import_statement(self) -> str:
    """Make an import statement for the plugin"""
    expr = f"import {self._module}"
    try:
        ast.parse(expr)
    except SyntaxError:
        raise ValueError(f"Invalid import statement: {expr}") from None
    return expr

update_module(mod)

Update the module name of the plugin function

Source code in cylindra/plugin/function.py
89
90
91
92
def update_module(self, mod: ModuleType):
    """Update the module name of the plugin function"""
    self._module = mod.__name__
    return self

register_function(func=None, *, record=True, name=None)

register_function(func: Callable[_P, _R], *, record: bool = True, name: str | None = None) -> CylindraPluginFunction[_P, _R]
register_function(func: Literal[None], *, record: bool = True, name: str | None = None) -> Callable[..., CylindraPluginFunction[_P, _R]]

Register a function as a plugin function.

The registered function will be added to the plugin menu when the module is installed as a plugin.

magicclass decorators, such as setup_function_gui and impl_preview, are compatible with this function.

Parameters:

Name Type Description Default
func callable

The plugin function. If the function is to be called in the GUI, its signature must be interpretable for magicgui. The first argument of the function must be the CylindraMainWidget instance.

None
record bool

If False, the function will not be recorded in the macro.

True
name str

Name to display in the menu. If None, the capitalized function name will be used.

None
Source code in cylindra/plugin/core.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def register_function(func=None, *, record=True, name=None):
    """Register a function as a plugin function.

    The registered function will be added to the plugin menu when the module is
    installed as a plugin.

    `magicclass` decorators, such as `setup_function_gui` and `impl_preview`,
    are compatible with this function.

    Parameters
    ----------
    func : callable, optional
        The plugin function. If the function is to be called in the GUI, its signature
        must be interpretable for `magicgui`. The first argument of the function must be
        the `CylindraMainWidget` instance.
    record : bool, default True
        If False, the function will not be recorded in the macro.
    name : str, optional
        Name to display in the menu. If None, the capitalized function name will be
        used.
    """

    def _inner(fn: Callable[_P, _R]) -> CylindraPluginFunction[_P, _R]:
        return CylindraPluginFunction(fn, name=name, record=record)

    return _inner if func is None else _inner(func)