mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-10 02:37:59 +01:00
154 lines
6.0 KiB
Python
154 lines
6.0 KiB
Python
import inspect
|
|
import os
|
|
import re
|
|
from typing import NamedTuple
|
|
|
|
from qtpy.QtCore import QObject
|
|
|
|
from bec_widgets.utils.name_utils import pascal_to_snake
|
|
|
|
EXCLUDED_PLUGINS = ["BECConnector", "BECDockArea", "BECDock", "BECFigure"]
|
|
|
|
|
|
class PluginFilenames(NamedTuple):
|
|
register: str
|
|
plugin: str
|
|
pyproj: str
|
|
|
|
|
|
def plugin_filenames(name: str) -> PluginFilenames:
|
|
return PluginFilenames(f"register_{name}.py", f"{name}_plugin.py", f"{name}.pyproject")
|
|
|
|
|
|
class DesignerPluginInfo:
|
|
def __init__(self, plugin_class):
|
|
self.plugin_class = plugin_class
|
|
self.plugin_name_pascal = plugin_class.__name__
|
|
self.plugin_name_snake = pascal_to_snake(self.plugin_name_pascal)
|
|
self.widget_import = f"from {plugin_class.__module__} import {self.plugin_name_pascal}"
|
|
plugin_module = (
|
|
".".join(plugin_class.__module__.split(".")[:-1]) + f".{self.plugin_name_snake}_plugin"
|
|
)
|
|
self.plugin_import = f"from {plugin_module} import {self.plugin_name_pascal}Plugin"
|
|
|
|
# first sentence / line of the docstring is used as tooltip
|
|
self.plugin_tooltip = (
|
|
plugin_class.__doc__.split("\n")[0].strip().replace('"', "'")
|
|
if plugin_class.__doc__
|
|
else self.plugin_name_pascal
|
|
)
|
|
|
|
self.base_path = os.path.dirname(inspect.getfile(plugin_class))
|
|
|
|
|
|
class DesignerPluginGenerator:
|
|
def __init__(self, widget: type):
|
|
self._excluded = False
|
|
self.widget = widget
|
|
self.info = DesignerPluginInfo(widget)
|
|
if widget.__name__ in EXCLUDED_PLUGINS:
|
|
|
|
self._excluded = True
|
|
return
|
|
|
|
self.templates: dict[str, str] = {}
|
|
self.template_path = os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)), "plugin_templates"
|
|
)
|
|
|
|
@property
|
|
def filenames(self):
|
|
return plugin_filenames(self.info.plugin_name_snake)
|
|
|
|
def run(self, validate=True):
|
|
if self._excluded:
|
|
print(f"Plugin {self.widget.__name__} is excluded from generation.")
|
|
return
|
|
if validate:
|
|
self._check_class_validity()
|
|
self._load_templates()
|
|
self._write_templates()
|
|
|
|
def _check_class_validity(self):
|
|
|
|
# Check if the widget is a QWidget subclass
|
|
if not issubclass(self.widget, QObject):
|
|
return
|
|
|
|
# Check if the widget class has parent as the first argument. This is a strict requirement of Qt!
|
|
signature = list(inspect.signature(self.widget.__init__).parameters.values())
|
|
if len(signature) == 1 or signature[1].name != "parent":
|
|
raise ValueError(
|
|
f"Widget class {self.widget.__name__} must have parent as the first argument."
|
|
)
|
|
|
|
base_cls = [val for val in self.widget.__bases__ if issubclass(val, QObject)]
|
|
if not base_cls:
|
|
raise ValueError(
|
|
f"Widget class {self.widget.__name__} must inherit from a QObject subclass."
|
|
)
|
|
|
|
# Check if the widget class calls the super constructor with parent argument
|
|
init_source = inspect.getsource(self.widget.__init__)
|
|
cls_init_found = (
|
|
bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent=parent") > 0)
|
|
or bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent)") > 0)
|
|
or bool(init_source.find(f"{base_cls[0].__name__}.__init__(self, parent,") > 0)
|
|
)
|
|
super_init_found = (
|
|
bool(
|
|
init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent=parent") > 0
|
|
)
|
|
or bool(init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent,") > 0)
|
|
or bool(init_source.find(f"super({base_cls[0].__name__}, self).__init__(parent)") > 0)
|
|
)
|
|
if issubclass(self.widget.__bases__[0], QObject) and not super_init_found:
|
|
super_init_found = (
|
|
bool(init_source.find("super().__init__(parent=parent") > 0)
|
|
or bool(init_source.find("super().__init__(parent,") > 0)
|
|
or bool(init_source.find("super().__init__(parent)") > 0)
|
|
)
|
|
|
|
# for the new style classes, we only have one super call. We can therefore check if the
|
|
# number of __init__ calls is 2 (the class itself and the super class)
|
|
num_inits = re.findall(r"__init__", init_source)
|
|
if len(num_inits) == 2 and not super_init_found:
|
|
super_init_found = bool(
|
|
init_source.find("super().__init__(parent=parent") > 0
|
|
or init_source.find("super().__init__(parent,") > 0
|
|
or init_source.find("super().__init__(parent)") > 0
|
|
)
|
|
|
|
if not cls_init_found and not super_init_found:
|
|
raise ValueError(
|
|
f"Widget class {self.widget.__name__} must call the super constructor with parent."
|
|
)
|
|
|
|
def _write_file(self, name: str, contents: str):
|
|
with open(os.path.join(self.info.base_path, name), "w", encoding="utf-8") as f:
|
|
f.write(contents)
|
|
|
|
def _format(self, name: str):
|
|
return self.templates[name].format(**self.info.__dict__)
|
|
|
|
def _write_templates(self):
|
|
self._write_file(self.filenames.register, self._format("register"))
|
|
self._write_file(self.filenames.plugin, self._format("plugin"))
|
|
pyproj = str({"files": [f"{self.info.plugin_class.__module__.split('.')[-1]}.py"]})
|
|
self._write_file(self.filenames.pyproj, pyproj)
|
|
|
|
def _load_templates(self):
|
|
for file in os.listdir(self.template_path):
|
|
if not file.endswith(".template"):
|
|
continue
|
|
with open(os.path.join(self.template_path, file), "r", encoding="utf-8") as f:
|
|
self.templates[file.split(".")[0]] = f.read()
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
# from bec_widgets.widgets.bec_queue.bec_queue import BECQueue
|
|
from bec_widgets.widgets.utility.spinner import SpinnerWidget
|
|
|
|
generator = DesignerPluginGenerator(SpinnerWidget)
|
|
generator.run(validate=False)
|