utils.py 4.87 KB
Newer Older
1
2
3
4
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
Benoit Formet's avatar
Benoit Formet committed
5
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
6
7
# Distributed under the GNU LGPLv3. See LICENSE for more info.

8
from os import error
9
import re
10
from importlib.util import find_spec
11
from importlib import import_module
12

13
# ---- Alias defined for default BLISS controllers
14
15
16
_ALIAS_TO_MODULE_NAME = {"Lima": "bliss.controllers.lima.lima_base"}


17
18
19
20
def camel_case_to_snake_style(name):
    s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
    return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()

21
22

def find_class(cfg_node, base_path="bliss.controllers"):
23
24
25
    return find_class_and_node(cfg_node, base_path)[0]


26
def resolve_module_name(class_name, node, base_path):
27
    if "package" in node:
28
        result = node["package"]
29
    elif "module" in node:
30
31
        module_name = node["module"]
        result = "%s.%s" % (base_path, module_name)
32
    elif base_path == "bliss.controllers" and class_name in _ALIAS_TO_MODULE_NAME:
33
34
        # For BLISS base class, there is alias to the right module
        # In order to allow to move them without changing the configuration
35
        result = _ALIAS_TO_MODULE_NAME.get(class_name)
36
37
    else:
        # discover module and class name
38
        result = "%s.%s" % (base_path, class_name.lower())
39
40
    return result

41

42
def find_class_and_node(cfg_node, base_path="bliss.controllers"):
43
44
    class_name, node = cfg_node.get_inherited_value_and_node("class")
    if class_name is None:
45
        raise KeyError("class")
46
    module_name = resolve_module_name(class_name, node, base_path)
47
48
    try:
        module = __import__(module_name, fromlist=[""])
49
    except ModuleNotFoundError as e:
50
51
        if find_spec(module_name) is not None:
            raise e
52
        module_name = "%s.%s" % (base_path, camel_case_to_snake_style(class_name))
53
54
55
        try:
            module = __import__(module_name, fromlist=[""])

56
57
58
59
60
61
62
63
64
        except ModuleNotFoundError as e2:
            if find_spec(module_name) is not None:
                raise e2
            else:
                msg = "CONFIG COULD NOT FIND CLASS!"
                msg += "\nWITH CONFIG  MODULE NAME: " + e.msg
                msg += "\nWITH DEFAULT MODULE NAME: " + e2.msg
                msg += f"\nCHECK THAT MODULE NAME BEGINS AFTER '{base_path}'\n"
                raise ModuleNotFoundError(msg)
65

66
    try:
67
        klass = getattr(module, class_name)
68
    except AttributeError:
69
        klass = getattr(module, class_name.title())
70

71
    return klass, node
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140


def find_top_class_and_node(cfg_node, base_paths=None):

    if base_paths is None:
        base_paths = ["bliss.controllers", "bliss.controllers.motors"]

    node = cfg_node.get_top_key_node("class")
    class_name = node["class"]

    candidates = set()
    errors = []
    for base_path in base_paths:
        module = None

        # --- Find module and try import -----------
        module_name = resolve_module_name(class_name, node, base_path)
        try:
            module = import_module(module_name)
        except ModuleNotFoundError as e:
            errors.append(e)

            module_name = "%s.%s" % (base_path, camel_case_to_snake_style(class_name))
            try:
                module = import_module(module_name)
            except ModuleNotFoundError as e2:
                errors.append(e2)

        if module is not None:
            # --- Find class and try import -----------
            if hasattr(module, class_name):
                klass = getattr(module, class_name)
                candidates.add((module, klass))
            else:
                kname = class_name.title()
                if kname != class_name:
                    if hasattr(module, kname):
                        klass = getattr(module, kname)
                        candidates.add((module, klass))
                    else:
                        errors.append(f"cannot find {class_name} in {module}")

    # --- return if a single candidate was found else raise error
    if len(candidates) == 1:
        return candidates.pop()[1], node

    elif len(candidates) > 1:
        for mod, klass in candidates:
            if "package" in node:
                if node["package"] == mod.__name__:
                    return klass, node
            elif "module" in node:
                if node["module"] in mod.__name__:
                    return klass, node

        msg = f"Multiple candidates found for class '{class_name}':"
        for mod, _ in candidates:
            msg += f"\n - {mod}"
        msg += "\nResolve by providing the 'module' key in yml config\n"
        raise ModuleNotFoundError(msg)
    else:
        msg = f"Config could not find {class_name}:"
        for err in errors:
            msg += f"\n{err}"
        msg += (
            f"\nCheck that module is located under one of these modules: {base_paths}"
        )
        msg += "\nElse, resolve by providing the 'package' key in yml config\n"
        raise ModuleNotFoundError(msg)