# pylint: disable=missing-module-docstring from __future__ import annotations import argparse import inspect import os import sys import black import isort from bec_widgets.utils.generate_designer_plugin import DesignerPluginGenerator from bec_widgets.utils.plugin_utils import BECClassContainer, get_rpc_classes if sys.version_info >= (3, 11): from typing import get_overloads else: print( "Python version is less than 3.11, using dummy function for get_overloads. " "If you want to use the real function 'typing.get_overloads()', please use Python 3.11 or later." ) def get_overloads(_obj): """ Dummy function for Python versions before 3.11. """ return [] class ClientGenerator: def __init__(self): self.header = """# This file was automatically generated by generate_cli.py\n import enum from typing import Literal, Optional, overload from bec_widgets.cli.client_utils import RPCBase, rpc_call, BECGuiClientMixin # pylint: skip-file""" self.content = "" def generate_client(self, class_container: BECClassContainer): """ Generate the client for the published classes. Args: class_container: The class container with the classes to generate the client for. """ rpc_top_level_classes = class_container.rpc_top_level_classes rpc_top_level_classes.sort(key=lambda x: x.__name__) connector_classes = class_container.connector_classes connector_classes.sort(key=lambda x: x.__name__) self.write_client_enum(rpc_top_level_classes) for cls in connector_classes: self.content += "\n\n" self.generate_content_for_class(cls) def write_client_enum(self, published_classes: list[type]): """ Write the client enum to the content. """ self.content += """ class Widgets(str, enum.Enum): \"\"\" Enum for the available widgets. \"\"\" """ for cls in published_classes: self.content += f'{cls.__name__} = "{cls.__name__}"\n ' def generate_content_for_class(self, cls): """ Generate the content for the class. Args: cls: The class for which to generate the content. """ class_name = cls.__name__ # Generate the content if cls.__name__ == "BECDockArea": self.content += f""" class {class_name}(RPCBase, BECGuiClientMixin):""" else: self.content += f""" class {class_name}(RPCBase):""" if not cls.USER_ACCESS: self.content += """... """ for method in cls.USER_ACCESS: obj = getattr(cls, method) if isinstance(obj, property): self.content += """ @property @rpc_call""" sig = str(inspect.signature(obj.fget)) doc = inspect.getdoc(obj.fget) else: sig = str(inspect.signature(obj)) doc = inspect.getdoc(obj) overloads = get_overloads(obj) for overload in overloads: sig_overload = str(inspect.signature(overload)) self.content += f""" @overload def {method}{str(sig_overload)}: ... """ self.content += """ @rpc_call""" self.content += f""" def {method}{str(sig)}: \"\"\" {doc} \"\"\"""" def write(self, file_name: str): """ Write the content to a file, automatically formatted with black. Args: file_name(str): The name of the file to write to. """ # Combine header and content, then format with black full_content = self.header + "\n" + self.content try: formatted_content = black.format_str(full_content, mode=black.FileMode(line_length=100)) except black.NothingChanged: formatted_content = full_content isort.Config( profile="black", line_length=100, multi_line_output=3, include_trailing_comma=True, known_first_party=["bec_widgets"], ) formatted_content = isort.code(formatted_content) with open(file_name, "w", encoding="utf-8") as file: file.write(formatted_content) def main(): """ Main entry point for the script, controlled by command line arguments. """ parser = argparse.ArgumentParser(description="Auto-generate the client for RPC widgets") parser.add_argument("--core", action="store_true", help="Whether to generate the core client") args = parser.parse_args() if args.core: current_path = os.path.dirname(__file__) client_path = os.path.join(current_path, "client.py") rpc_classes = get_rpc_classes("bec_widgets") generator = ClientGenerator() generator.generate_client(rpc_classes) generator.write(client_path) for cls in rpc_classes.plugins: plugin = DesignerPluginGenerator(cls) if not hasattr(plugin, "info"): continue # if the class directory already has a register, plugin and pyproject file, skip if os.path.exists( os.path.join(plugin.info.base_path, f"register_{plugin.info.plugin_name_snake}.py") ): continue if os.path.exists( os.path.join(plugin.info.base_path, f"{plugin.info.plugin_name_snake}_plugin.py") ): continue if os.path.exists( os.path.join(plugin.info.base_path, f"{plugin.info.plugin_name_snake}.pyproject") ): continue plugin.run() if __name__ == "__main__": # pragma: no cover sys.argv = ["generate_cli.py", "--core"] main()