Skip to content

cylindra.plugin

This submodule includes functions for plugin management.

CylindraPluginFunction

Source code in cylindra/plugin/function.py
 14
 15
 16
 17
 18
 19
 20
 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
class CylindraPluginFunction(Generic[_P, _R]):
    def __init__(
        self,
        func: Callable[_P, _R],
        name: str | None = None,
        module: str | None = None,
    ):
        if not callable(func):
            raise TypeError("func must be a callable")
        if not hasattr(func, "__name__"):
            raise ValueError("func must have a __name__ attribute.")
        self._func = func
        if name is None:
            name = func.__name__.replace("_", " ").capitalize()
        self._name = name
        if module is None:
            module = func.__module__
        self._is_recordable = _is_recordable(func)
        if module == "__main__" and self._is_recordable:
            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.__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 is not 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,
                )

    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):
        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())
        _method.__signature__ = inspect.Signature(params[1:])
        _method.__name__ = self._name
        _method.__doc__ = getattr(self._func, "__doc__", "")
        upgrade_signature(_method)
        return _method

    def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
        from macrokit import Expr, Symbol
        from magicclass.undo import UndoCallback

        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}"
            )
        # TODO: how to use thread_worker?
        out = self._func(*bound.args, **bound.kwargs)

        # macro recording
        _args = []
        _kwargs = {}
        for name, param in bound.signature.parameters.items():
            if name == self._ui_arg_name:
                _args.append(ui._my_symbol)
            elif param.kind is inspect.Parameter.POSITIONAL_ONLY:
                _args.append(bound.arguments[name])
            else:
                _kwargs[name] = bound.arguments[name]
        fn_expr = Expr("getattr", [Symbol(self._module), self._func.__name__])
        expr = Expr.parse_call(fn_expr, tuple(_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
import_statement()

Make an import statement for the plugin

Source code in cylindra/plugin/function.py
62
63
64
65
66
67
68
69
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
71
72
73
74
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 a function as a plugin function.

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

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
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
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.

    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(func: Callable[_P, _R]) -> CylindraPluginFunction[_P, _R]:
        f = CylindraPluginFunction(func, name=name)
        if not record:
            f._is_recordable = record
        return f

    return _inner if func is None else _inner(func)