diff --git a/pyaml/configuration/config_exception.py b/pyaml/configuration/config_exception.py new file mode 100644 index 00000000..87e19f7b --- /dev/null +++ b/pyaml/configuration/config_exception.py @@ -0,0 +1,48 @@ +from typing import Union +from pyaml.exception import PyAMLException + + +class PyAMLConfigException(PyAMLException): + """Exception raised for custom error scenarios. + + Attributes: + message -- explanation of the error + """ + + def __init__(self, config_key = None, parent_exception:Union["PyAMLConfigException", "PyAMLException"] = None): + self.parent_keys = [] + self.config_key = config_key + self.parent_exception = parent_exception + message = "An exception occurred while building object." + if parent_exception is not None: + if isinstance(self.parent_exception, PyAMLConfigException) and parent_exception.config_key is not None: + self.parent_keys.append(parent_exception.config_key) + self.parent_keys.extend(parent_exception.parent_keys) + if config_key is not None: + message = f"An exception occurred while building key '{config_key}.{parent_exception.get_keys()}': {parent_exception.get_original_message()}" + else: + message = f"An exception occurred while building object in '{parent_exception.get_keys()}': {parent_exception.get_original_message()}" + else: + if config_key is not None: + message = f"An exception occurred while building key '{config_key}': {parent_exception.message}" + else: + message = f"An exception occurred while building object: {parent_exception.message}" + super().__init__(message) + + def get_keys(self) -> str: + keys = "" + if self.config_key is not None: + if len(self.parent_keys)>0: + keys = ".".join(self.parent_keys) + keys += "." + keys += self.config_key + return keys + + def get_original_message(self): + if self.parent_exception is not None: + if isinstance(self.parent_exception, PyAMLConfigException): + return self.parent_exception.get_original_message() + else: + return self.parent_exception.message + else: + return self.message \ No newline at end of file diff --git a/pyaml/configuration/factory.py b/pyaml/configuration/factory.py index 4dabbba9..585a4d7c 100644 --- a/pyaml/configuration/factory.py +++ b/pyaml/configuration/factory.py @@ -2,6 +2,9 @@ import importlib import pprint as pp import traceback + +from .config_exception import PyAMLConfigException +from ..exception import PyAMLException from ..lattice.element import Element #TODO: @@ -13,23 +16,26 @@ def buildObject(d:dict): """Build an object from the dict""" if not isinstance(d,dict): - raise Exception("Unecpted object " + str(d)) + raise PyAMLException("Unexpected object " + str(d)) if not "type" in d: - raise Exception("No type specified for " + str(type(d)) + ":" + str(d)) - typeStr = d["type"] + raise PyAMLException("No type specified for " + str(type(d)) + ":" + str(d)) + type_str = d["type"] del d["type"] - module = importlib.import_module(typeStr) + try: + module = importlib.import_module(type_str) + except ModuleNotFoundError as ex: + raise PyAMLException(f"Module referenced in type cannot be founded: '{type_str}'") from ex # Get the config object config_cls = getattr(module, "ConfigModel", None) if config_cls is None: - raise ValueError(f"ConfigModel class '{typeStr}.ConfigModel' not found") + raise ValueError(f"ConfigModel class '{type_str}.ConfigModel' not found") # Get the class name cls_name = getattr(module, "PYAMLCLASS", None) if cls_name is None: - raise ValueError(f"PYAMLCLASS definition not found in '{typeStr}'") + raise ValueError(f"PYAMLCLASS definition not found in '{type_str}'") try: @@ -40,7 +46,7 @@ def buildObject(d:dict): elem_cls = getattr(module, cls_name, None) if elem_cls is None: raise ValueError( - f"Unknown element class '{typeStr}.{cls_name}'" + f"Unknown element class '{type_str}.{cls_name}'" ) obj = elem_cls(cfg) @@ -51,7 +57,7 @@ def buildObject(d:dict): print(traceback.format_exc()) print(e) - print(typeStr) + print(type_str) pp.pprint(d) #Fatal quit() @@ -61,40 +67,55 @@ def depthFirstBuild(d): """Main factory function (Depth-first factory)""" if isinstance(d,list): - # list can be a list of objects or a list of native types l = [] - for e in d: + for index, e in enumerate(d): if isinstance(e,dict) or isinstance(e,list): - obj = depthFirstBuild(e) - l.append(obj) + try: + obj = depthFirstBuild(e) + l.append(obj) + except PyAMLException as pyaml_ex: + raise PyAMLConfigException(f"[{index}]", pyaml_ex) from pyaml_ex + except Exception as ex: + raise PyAMLConfigException(f"[{index}]") from ex else: l.append(e) return l elif isinstance(d,dict): - for key, value in d.items(): if isinstance(value,dict) or isinstance(value,list): - obj = depthFirstBuild(value) - # Replace the inner dict by the object itself - d[key]=obj - - # We are now on leaf (no nested object), we can construt - obj = buildObject(d) + try: + obj = depthFirstBuild(value) + # Replace the inner dict by the object itself + d[key]=obj + except PyAMLException as pyaml_ex: + raise PyAMLConfigException(key, pyaml_ex) from pyaml_ex + except Exception as ex: + raise PyAMLConfigException(key) from ex + + # We are now on leaf (no nested object), we can construct + try: + obj = buildObject(d) + except PyAMLException as pyaml_ex: + raise PyAMLConfigException(None, pyaml_ex) from pyaml_ex + except Exception as ex: + raise PyAMLException("An exception occurred while building object") from ex return obj + raise PyAMLException("Unexpected element found.") + def register_element(elt): if isinstance(elt,Element): name = str(elt) if name in _ALL_ELEMENTS: - raise Exception(f"element {name} already defined") + raise PyAMLException(f"element {name} already defined") _ALL_ELEMENTS[name] = elt def get_element(name:str): if name not in _ALL_ELEMENTS: - raise Exception(f"element {name} not defined") + raise PyAMLException(f"element {name} not defined") return _ALL_ELEMENTS[name] def clear(): diff --git a/pyaml/exception.py b/pyaml/exception.py index 8c57c751..35ceaeff 100644 --- a/pyaml/exception.py +++ b/pyaml/exception.py @@ -6,5 +6,5 @@ class PyAMLException(Exception): """ def __init__(self, message): + super().__init__(message) self.message = message - super().__init__(self.message) diff --git a/tests/conftest.py b/tests/conftest.py index 2fd7eca4..aec077e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,31 +13,37 @@ def install_test_package(request): The test must provide a dictionary as parameter with: - 'name': name of the installable package (used for pip uninstall) - - 'path': relative path to the package folder (e.g. 'tests/mon_dir') + - 'path': relative path to the package folder (e.g. 'tests/my_dir'). Optional, replaced by package name if absent. Example: -------- @pytest.mark.parametrize("install_test_package", [{ - "name": "mon_package", - "path": "tests/mon_dir" + "name": "my_package", + "path": "tests/my_dir" }], indirect=True) def test_x(install_test_package): ... """ info = request.param package_name = info["name"] - package_path = pathlib.Path(info["path"]).resolve() - - if not package_path.exists(): - raise FileNotFoundError(f"Package path not found: {package_path}") + package_path = None + if info["path"] is not None: + package_path = pathlib.Path(info["path"]).resolve() + if not package_path.exists(): + raise FileNotFoundError(f"Package path not found: {package_path}") if not ((package_path / "pyproject.toml").exists() or (package_path / "setup.py").exists()): raise RuntimeError(f"No pyproject.toml or setup.py found in {package_path}") # Install package - subprocess.check_call([ - sys.executable, "-m", "pip", "install", "--quiet", "--editable", str(package_path) - ]) + if package_path is not None: + subprocess.check_call([ + sys.executable, "-m", "pip", "install", "--quiet", "--editable", str(package_path) + ]) + else: + subprocess.check_call([ + sys.executable, "-m", "pip", "install", "--quiet", "--editable", str(package_name) + ]) # Test the import. import importlib # Ensure its path is importable diff --git a/tests/control/__init__.py b/tests/control/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py new file mode 100644 index 00000000..d271102a --- /dev/null +++ b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/__init__.py @@ -0,0 +1,11 @@ +""" +PyAML Dummy control system for tests. +""" + +__title__ = "pyAML Dummy Control System" +__description__ = "PyAML Dummy Control System" +__url__ = "https://github.com/python-accelerator-middle-layer/pyaml" +__version__ = "0.0.0" +__author__ = "pyAML collaboration" +__author_email__ = "" +__all__ = [__version__] diff --git a/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py new file mode 100644 index 00000000..812a22de --- /dev/null +++ b/tests/control/dummy-cs-pyaml/dummy-cs-pyaml/dummy_controlsystem.py @@ -0,0 +1,37 @@ +from pydantic import BaseModel +from pyaml.control.controlsystem import ControlSystem + +PYAMLCLASS = "DummyControlSystem" + +class ConfigModel(BaseModel): + """ + Configuration model for a Tango Control System. + + Attributes + ---------- + name : str + Name of the control system. + """ + name: str + +class DummyControlSystem(ControlSystem): + + def __init__(self, cfg: ConfigModel): + super().__init__() + self._cfg = cfg + + + def init_cs(self): + pass + + + def name(self) -> str: + """ + Return the name of the control system. + + Returns + ------- + str + Name of the control system. + """ + return self._cfg.name diff --git a/tests/dummy_cs/tango/tango/pyaml/attribute.py b/tests/dummy_cs/tango/tango/pyaml/attribute.py index 5b56cb6f..a2da8ec0 100644 --- a/tests/dummy_cs/tango/tango/pyaml/attribute.py +++ b/tests/dummy_cs/tango/tango/pyaml/attribute.py @@ -9,15 +9,12 @@ class ConfigModel(BaseModel): unit: str = "" class Attribute(DeviceAccess): - def __init__(self, cfg: ConfigModel): - super().__init__(cfg) - """ Class that implements a default device class that just prints out values (Debugging purpose) """ - def __init__(self, cfg: ConfigModel): + super().__init__() self._cfg = cfg self._setpoint = cfg.attribute self._readback = cfg.attribute @@ -31,7 +28,6 @@ def measure_name(self) -> str: return self._readback def set(self, value: float): - print(f"{self._setpoint}: set {value}") self._cache = value def set_and_wait(self, value: float):