Source code for pyaml.configuration.factory

# PyAML factory (construct AML objects from config files)
import fnmatch
import importlib
from threading import Lock
from typing import TypedDict, get_type_hints

from pydantic import ValidationError

from ..common.element import Element
from ..common.exception import PyAMLConfigException
from .unbound_element import UnboundElement


[docs] class PyAMLFactory: """Singleton factory to build PyAML elements with future compatibility logic.""" _instance = None _lock = Lock() def __new__(cls): """ No matter how many times you call PyAMLFactory(), it will be created only once. """ with cls._lock: if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._elements = {} cls._instance._strategies = [] return cls._instance
[docs] def handle_validation_error(self, e, type_str: str, location_str: str, field_locations: dict): # Handle pydantic errors globalMessage = "" for err in e.errors(): msg = err["msg"] field = "" if len(err["loc"]) == 2: field, fieldIdx = err["loc"] message = f"'{field}.{fieldIdx}': {msg}" else: field = err["loc"][0] message = f"'{field}': {msg}" if field_locations and field in field_locations: file, line, col = field_locations[field] loc = f"{file} at line {line}, colum {col}" message += f" {loc}" globalMessage += message globalMessage += ", " # Discard pydantic stack trace raise PyAMLConfigException(f"{globalMessage} for object: '{type_str}' {location_str}") from None
[docs] def get_field_type(self, config_cls, field_name) -> type: # Get type of a pydantic ConfigModel field if config_cls is None: return None type_hints = get_type_hints(config_cls) return type_hints[field_name] if field_name in type_hints else None
[docs] def get_infos(self, d, ignore_external: bool): # Retrieve informations of object to be constructed location = d["__location__"] if "__location__" in d else None field_locations = d["__fieldlocations__"] if "__fieldlocations__" in d else None location_str = "" if location: file, line, col = location location_str = f"{file} at line {line}, column {col}." if not isinstance(d, dict): raise PyAMLConfigException(f"Unexpected object {str(d)} {location_str}") if "type" not in d: raise PyAMLConfigException(f"No type specified for {str(type(d))}:{str(d)} {location_str}") module_str = d["type"] class_str = d["class"] if "class" in d else None validation_class_str = d["validation_class"] if "validation_class" in d else "ConfigModel" # Import the module try: module = importlib.import_module(module_str) except ModuleNotFoundError as ex: if not ignore_external: # Discard module not found stack trace raise PyAMLConfigException( "Module referenced in type cannot be found:" + f"'{module_str}' {location_str}" ) from None else: return None # Get the object class name if class_str is None: class_str = getattr(module, "PYAMLCLASS", None) if class_str is None: raise PyAMLConfigException( f"PYAMLCLASS definition not found or class not specified in '{module_str}' {location_str}" ) # Get the validation class config_cls = getattr(module, validation_class_str, None) if config_cls is None: raise PyAMLConfigException(f"No validation class for '{module.__name__}.{class_str}' {location_str}") return (module, config_cls, class_str, field_locations, location_str)
[docs] def build_object(self, d: dict, ignore_external: bool = False): """Build an object from the dict""" (module, config_cls, class_str, field_locations, location_str) = self.get_infos(d, ignore_external) # Clean up dict d.pop("__location__", None) d.pop("__fieldlocations__", None) d.pop("type") d.pop("class", None) d.pop("validation_class", None) control_modes = d.pop("control_modes", None) # Validate the model try: cfg = config_cls.model_validate(d) except ValidationError as e: self.handle_validation_error(e, module.__name__, location_str, field_locations) elem_cls = getattr(module, class_str, None) if elem_cls is None: raise PyAMLConfigException(f"Unknown element class '{module.__name__}.{class_str}' {location_str}") if control_modes is None: # Immediate contruction of the object try: obj = elem_cls(cfg) self.register_element(obj) except Exception as e: raise PyAMLConfigException(f"{str(e)} when creating '{module.__name__}.{class_str}' {location_str}") from e else: # Delayed construction # An UnboundElement will be constructuced during the filling of the ElementHolder try: obj = UnboundElement(elem_cls, module.__name__, control_modes, cfg) self.register_element(obj) except Exception as e: raise PyAMLConfigException( f"{str(e)} when creating unbounded '{module.__name__}.{class_str}' {location_str}" ) from e return obj
[docs] def build_unbound(self, e: UnboundElement, holder) -> Element: try: obj = e._class(holder, e._config) except Exception as ex: raise PyAMLConfigException(f"{str(ex)} when creating '{e._module_name}.{e._class.__name__}'") from ex if not isinstance(obj, Element): raise PyAMLConfigException(f"'{e._module_name}.{e._class.__name__}' is not a sub class of Element") obj._peer = holder return obj
[docs] def depth_first_build(self, d, ignore_external: bool): """ Main factory function (Depth-first factory) Parameters ---------- ignore_external: bool Ignore `module not found` and return None when an object cannot be created """ if isinstance(d, list): # list can be a list of objects or a list of native types l = [] for _index, e in enumerate(d): if isinstance(e, dict) or isinstance(e, list): obj = self.depth_first_build(e, ignore_external) l.append(obj) else: l.append(e) return l elif isinstance(d, dict): _, config_cls, *_ = self.get_infos(d, ignore_external) for key, value in d.items(): if not key == "__fieldlocations__": if isinstance(value, dict) or isinstance(value, list): # Get the type of the field fieldType = self.get_field_type(config_cls, key) # Do not recurse dict defined in ConfigModel # pydantic use TypedDict not usable with isinstance if str(fieldType) != "<class 'dict'>": obj = self.depth_first_build(value, ignore_external) # Replace the inner dict by the object itself d[key] = obj # We are now on leaf (no nested object), we can construct return self.build_object(d, ignore_external) raise PyAMLConfigException("Unexpected element found. 'dict' or 'list' expected but got '{d.__class__.__name__}'")
[docs] def register_element(self, elt): if isinstance(elt, Element): name = elt.get_name() if name in self._elements: raise PyAMLConfigException(f"element {name} already defined") self._elements[name] = elt
[docs] def get_element(self, name: str): if name not in self._elements: raise PyAMLConfigException(f"element {name} not defined") return self._elements[name]
[docs] def get_elements_by_name(self, wildcard: str) -> list[Element]: return [e for k, e in self._elements.items() if fnmatch.fnmatch(k, wildcard)]
[docs] def get_elements_by_type(self, type) -> list[Element]: return [e for k, e in self._elements.items() if isinstance(e, type)]
[docs] def clear(self): self._elements.clear()
Factory = PyAMLFactory()