Source code for secop_ophyd.GenNodeCode

import inspect
import re
from importlib import import_module, reload
from inspect import Signature
from pathlib import Path


[docs] class Method: def __init__(self, cmd_name: str, description: str, cmd_sign: Signature) -> None: self.sig_str: str raw_sig_str: str = str(cmd_sign) raw_sig_str = raw_sig_str.replace("typing.", "") if "self" in raw_sig_str: self.sig_str = raw_sig_str else: self.sig_str = "(self, " + raw_sig_str[1:] self.name: str = cmd_name self.description: str = description def __str__(self) -> str: code = "" code += " @abstractmethod \n" code += f" def {self.name}{self.sig_str}:\n" code += f' """{self.description}"""' return code
[docs] class GenNodeCode: """Generates A Python Class for a given SECoP_Node_Device instance. This allows autocompletiion and type hinting in IDEs, this is needed since the attributes of the generated Ophyd devices are only known at runtime. """ ModName: str = "genNodeClass" node_mod = None module_folder_path: Path | None = None def __init__(self, path: str | None = None, log=None): """Instantiates GenNodeCode, internally all atrribues on a node and module level are collected. Additionally all the needed imports are collected in a dict """ # prevent circular import from secop_ophyd.SECoPDevices import ( SECoPBaseDevice, SECoPCommunicatorDevice, SECoPMoveableDevice, SECoPNodeDevice, SECoPReadableDevice, SECoPTriggerableDevice, SECoPWritableDevice, ) self.dimport: dict[str, set[str]] = {} self.dmodule: dict = {} self.dnode: dict = {} self.import_string: str = "" self.mod_cls_string: str = "" self.node_cls_string: str = "" self.log = log if path is not None: self.module_folder_path = Path(path) # resulting Class is supposed to be abstract self.add_import("abc", "ABC") self.add_import("abc", "abstractmethod") self.add_import("typing", "Any") mod_path = self.ModName if self.module_folder_path is not None: str_path = str(self.module_folder_path) rep_slash = str_path.replace("/", ".").replace("\\", ".") mod_path = f"{rep_slash}.{self.ModName}" try: self.node_mod = import_module(mod_path) except ModuleNotFoundError: if self.log is None: print("no code generated yet, building from scratch") else: self.log.info("no code generated yet, building from scratch") return modules = inspect.getmembers(self.node_mod) results = filter(lambda m: inspect.isclass(m[1]), modules) for class_symbol, class_obj in results: module = class_obj.__module__ if module == self.ModName: # Node Classes def get_attrs(source: str) -> list[tuple[str, str]]: source_list: list[str] = source.split("\n") # remove first line source_list.pop(0) source_list.pop() # remove whitespace source_list = [attr.replace(" ", "") for attr in source_list] # split at colon attrs: list[tuple[str, str]] = [] for attr in source_list: parts = attr.split(":", maxsplit=1) if len(parts) == 2: attrs.append((parts[0], parts[1])) return attrs if issubclass(class_obj, SECoPNodeDevice): attrs = get_attrs(inspect.getsource(class_obj)) bases = [base.__name__ for base in class_obj.__bases__] self.add_node_class(class_symbol, bases, attrs) continue if issubclass( class_obj, ( SECoPBaseDevice, SECoPCommunicatorDevice, SECoPMoveableDevice, SECoPReadableDevice, SECoPWritableDevice, SECoPTriggerableDevice, ), ): attributes = [] for attr_name, attr in class_obj.__annotations__.items(): attributes.append((attr_name, attr.__name__)) methods = [] for method_name, method in class_obj.__dict__.items(): if callable(method) and not method_name.startswith("__"): method_source = inspect.getsource(method) match = re.search( r"\s*def\s+\w+\s*\(.*\).*:\s*", method_source ) if match: function_body = method_source[match.end() :] description_list = function_body.split('"""', 2) description = description_list[1] else: raise Exception( "could not extract description function body" ) methods.append( Method( method_name, description, inspect.signature(method) ) ) bases = [base.__name__ for base in class_obj.__bases__] self.add_mod_class(class_symbol, bases, attributes, methods) continue else: self.add_import(module, class_symbol)
[docs] def add_import(self, module: str, class_str: str): """adds an Import to the import dict :param module: Python Module of the dict :type module: str :param class_str: Class that is to be imported :type class_str: str """ if self.dimport.get(module): self.dimport[module].add(class_str) else: self.dimport[module] = {class_str}
[docs] def add_mod_class( self, module_cls: str, bases: list[str], attrs: list[tuple[str, str]], cmd_plans: list[Method], ): """adds module class to the module dict :param module_cls: name of the new Module class :type module_cls: str :param bases: bases the new class is derived from :type bases: list[str] :param attrs: list of attributes of the class :type attrs: tuple[str, str] """ self.dmodule[module_cls] = {"bases": bases, "attrs": attrs, "plans": cmd_plans}
[docs] def add_node_class( self, node_cls: str, bases: list[str], attrs: list[tuple[str, str]] ): self.dnode[node_cls] = {"bases": bases, "attrs": attrs}
def _write_imports_string(self): self.import_string = "" # Collect imports required for type hints (Node) for mod, cls_set in self.dimport.items(): if len(cls_set) == 1 and list(cls_set)[0] == "": self.import_string += f"import {mod} \n" continue cls_string = ", ".join(cls_set) self.import_string += f"from {mod} import {cls_string} \n" self.import_string += "\n\n" def _write_mod_cls_string(self): self.mod_cls_string = "" for mod_cls, cls_dict in self.dmodule.items(): # Generate the Python code for each Module class bases = ", ".join(cls_dict["bases"]) self.mod_cls_string += f"class {mod_cls}({bases}):\n" # write Attributes for attr_name, attr_type in cls_dict["attrs"]: self.mod_cls_string += f" {attr_name}: {attr_type}\n" self.mod_cls_string += "\n" # write abstract methods for plan in cls_dict["plans"]: self.mod_cls_string += str(plan) self.mod_cls_string += "\n\n" def _write_node_cls_string(self): self.node_cls_string = "" for node_cls, cls_dict in self.dnode.items(): # Generate the Python code for each Module class bases = ", ".join(cls_dict["bases"]) self.node_cls_string += f"class {node_cls}({bases}):\n" for attr_name, attr_type in cls_dict["attrs"]: self.node_cls_string += f" {attr_name}: {attr_type}\n" self.node_cls_string += "\n\n"
[docs] def write_gen_node_class_file(self): self._write_imports_string() self._write_mod_cls_string() self._write_node_cls_string() code = "" code += self.import_string code += self.mod_cls_string code += self.node_cls_string # Write the generated code to a .py file if self.module_folder_path is None: filep = Path(f"{self.ModName}.py") else: filep = self.module_folder_path / f"{self.ModName}.py" with open(filep, "w") as file: file.write(code) # Reload the Module after its source has been edited if self.node_mod is not None: reload(self.node_mod)