static.py 35.2 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
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
"""
Bliss static configuration

The next example will require a running bliss configuration server and
assumes the following YAML_ configuration is present:

.. literalinclude:: examples/config/motion.yml
    :language: yaml
    :caption: ./motion_example.yml

Accessing the configured elements from python is easy

.. code-block:: python
    :emphasize-lines: 1,4,7,11,18

    >>> from bliss.config.static import get_config

    >>> # access the bliss configuration object
    >>> config = get_config()

    >>> # see all available object names
    >>> config.names_list
    ['mock1', 'slit1', 's1f', 's1b', 's1u', 's1d', 's1vg', 's1vo', 's1hg', 's1ho']

    >>> # get a hold of motor 's1vo' configuration
    >>> s1u_config = config.get_config('s1u')
    >>> s1u_config
35
    ConfigNode([('name', 's1u')])
36
37
38
39
40
41
42
    >>> s1vo_config['velocity']
    500

    >>> # get a hold of motor 's1vo'
    >>> s1vo = config.get('s1vo')
    >>> s1vo
    <bliss.common.axis.Axis at 0x7f94de365790>
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
43
    >>> s1vo.position
44
45
46
47
    0.0

"""

48
import os
49
import json
50
import types
51
52
53
import pickle
import weakref
import operator
54
import hashlib
55
from collections import defaultdict
56
from collections.abc import MutableMapping, MutableSequence
57

58
59
60
import ruamel
from ruamel.yaml import YAML
from ruamel.yaml.compat import StringIO
Matias Guijarro's avatar
Matias Guijarro committed
61

62
63
from bliss.config.conductor import client
from bliss.config import channels
64
from bliss.common.utils import prudent_update, Singleton
65
from bliss import global_map
66
from bliss.comm import service
67

68

69
def get_config(base_path="", timeout=3., raise_yaml_exc=True):
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
    """
    Return configuration from bliss configuration server

    The first time the function is called, a new
    :class:`~bliss.config.static.Config` object is constructed and returned.
    Subsequent calls will return a cached object. Example::

        >>> from bliss.config.static import get_config

        >>> # access the bliss configuration object
        >>> config = get_config()

        >>> # see all available object names
        >>> config.names_list
        ['mock1', 'slit1', 's1f', 's1b', 's1u', 's1d', 's1vg', 's1vo', 's1hg', 's1ho']

        >>> # get a hold of motor 's1vo' configuration
        >>> s1u_config = config.get_config('s1u')
        >>> s1u_config
89
        ConfigNode([('name', 's1u')])
90
91
92
93
94
95
        >>> s1vo_config['velocity']
        500

    Args:
        base_path (str): base path to config
        timeout (float): response timeout (seconds)
96
97
        raise_yaml_exc (bool): if False will not raise exceptions related
                         to yaml parsing and config nodes creation
98
99
100
101

    Returns:
        Config: the configuration object
    """
102
    return Config(base_path, timeout, raise_yaml_exc=raise_yaml_exc)
103

104

105
106
107
class ConfigReference:
    @staticmethod
    def is_reference(name):
108
109
110
        if isinstance(name, str):
            return name.startswith("$")
        return False
111

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
    def __init__(self, parent, value):
        self._parent = parent
        ref, _, attr = value.lstrip("$").partition(".")
        self._object_name = ref
        self._attr = attr

    def __getstate__(self):
        return {
            "object_name": self.object_name,
            "attr": self.attr,
            "parent": self._parent,
        }

    def __setstate__(self, d):
        self._object_name = d["object_name"]
        self._attr = d["attr"]
        self._parent = d["parent"]

    def __eq__(self, other):
        if isinstance(other, ConfigReference):
            return self._object_name == other._object_name and self._attr == other._attr
        else:
            return False
135

136
137
138
    @property
    def object_name(self):
        return self._object_name
139

140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
    @property
    def attr(self):
        return self._attr

    def dereference(self):
        obj = self._parent.config.get(self.object_name)
        alias = global_map.aliases.get_alias(obj)
        if alias:
            obj = global_map.aliases.get(alias)
        if self.attr:
            return operator.attrgetter(self.attr)(obj)
        return obj

    def encode(self):
        if self.attr:
            return f"${self.object_name}.{self.attr}"
        else:
            return f"${self.object_name}"
158
159


160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
class ConfigList(MutableSequence):
    def __init__(self, parent):
        self._data = []
        self._parent = parent

    def __getstate__(self):
        return {"data": self._data, "parent": self._parent}

    def __setstate__(self, d):
        self._data = d["data"]
        self._parent = d["parent"]

    @property
    def raw_list(self):
        return self._data

    def __eq__(self, other):
        if isinstance(other, ConfigList):
            return self.raw_list == other.raw_list
        else:
            if isinstance(other, MutableSequence):
                return list(other) == list(self)
            return False

    def __getitem__(self, key):
        value = self._data[key]
        if isinstance(value, ConfigReference):
            return value.dereference()
        return value

    def __len__(self):
        return len(self._data)

    def __setitem__(self, key, value):
        self._data[key] = convert_value(value, self._parent)

    def __delitem__(self, key):
        del self._data[key]

    def __repr__(self):
        return repr(self._data)

    def encode(self):
        return self._data

    def insert(self, index, value):
        self._data.insert(index, convert_value(value, self._parent))


def convert_value(value, parent):
    """Convert value to a ConfigReference, a config node or a config list with the given parent

    Scalars, or values with the right type, are just returned as they are
213
    """
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
    if value is None or isinstance(
        value, (ConfigReference, ConfigNode, ConfigList, bool, int, float)
    ):
        pass
    else:
        if isinstance(value, str):
            if ConfigReference.is_reference(value):
                value = ConfigReference(parent, value)
        else:
            if isinstance(value, dict):
                new_node = ConfigNode(parent)
                build_nodes_from_dict(value, new_node)
                value = new_node
            elif isinstance(value, list):
                value = build_nodes_from_list(value, parent)
            else:
                # a custom object from bliss? => make a reference
                try:
                    obj_name = value.name
                except AttributeError:
                    raise ValueError(f"Cannot make a reference to object {value}")
                if obj_name in parent.config.names_list:
                    value = ConfigReference(parent, obj_name)
                else:
                    raise ValueError(f"Cannot make a reference to object {value}")
    return value
240

241
242
243
244
245
246
247
248
249
250
251
252
253

class ConfigNode(MutableMapping):
    """
    Configuration ConfigNode. Do not instantiate this class directly.

    Typical usage goes through :class:`~bliss.config.static.Config`.

    This class has a :class:`dict` like API
    """

    # key which triggers a YAML_ collection to be identified as a bliss named item
    NAME_KEY = "name"
    USER_TAG_KEY = "user_tag"
254
255
    RPC_SERVICE_KEY = "service"

256
257
    indexed_nodes = weakref.WeakValueDictionary()
    tagged_nodes = defaultdict(weakref.WeakSet)
258
    services = weakref.WeakSet()
259
260
261
262
263

    @staticmethod
    def reset_cache():
        ConfigNode.indexed_nodes = weakref.WeakValueDictionary()
        ConfigNode.tagged_nodes = defaultdict(weakref.WeakSet)
264
        ConfigNode.services = weakref.WeakSet()
265

266
267
268
269
270
271
272
273
274
275
276
277
278
    @staticmethod
    def goto_path(d, path_as_list, key_error_exception=True):
        path_in_dict = path_as_list[:]
        while path_in_dict:
            try:
                d = d[path_in_dict.pop(0)]
            except KeyError:
                if key_error_exception:
                    raise
                else:
                    return ConfigNode(d)  # return a new config node, with 'd' as parent
        return d

279
280
    def __init__(self, parent=None, filename=None, path=None):
        self._data = {}
281
        self._parent = parent
282
283
284
285
286
287
288
289
290
        self._filename = filename
        self._path = path

    def raw_get(self, key):
        return self._data.get(key)

    def raw_items(self):
        return self._data.items()

291
292
293
294
295
296
    def get(self, key, default=None):
        if key in self._data:
            return self[key]
        else:
            return default

297
298
299
    def encode(self):
        return self._data

300
301
302
303
304
305
306
307
    def md5hash(self):
        """Return md5 hex digest of the config node

        Uses internal config dict to build the hash, so
        two nodes with same digest represent the exact same config
        """
        return hashlib.md5(str(self._data).encode()).hexdigest()

308
309
310
    def reparent(self, new_parent_node):
        self._parent = new_parent_node

311
312
313
314
    def reload(self):
        with client.remote_open(self.filename) as f:
            yaml = YAML(pure=True)
            yaml.allow_duplicate_keys = True
315
            d = ConfigNode.goto_path(yaml.load(f.read()), self.path)
316
317
318
319
            self._data = {}
            for k, v in d.items():
                self[k] = v

320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
    @property
    def config(self):
        return self.root.config

    @property
    def root(self):
        root = self
        while root.parent:
            root = root.parent
        return root

    @property
    def path(self):
        """Return a list to access the node in the list+dictionaries from the YAML file parsing
        """
        parent_path = []
        if self.parent:
            if self.parent.filename == self.filename:
                parent_path = self.parent.path
        # return a copy of the path
        return list(parent_path + self._path if self._path is not None else [])

    def __getstate__(self):
        return {
            "data": self._data,
            "parent": self._parent,
            "filename": self._filename,
            "path": self._path,
        }

    def __setstate__(self, d):
        self._data = d["data"]
        self._parent = d["parent"]
        self._filename = d["filename"]
        self._path = d["path"]

    def __eq__(self, other):
        if isinstance(other, ConfigNode):
            return dict(self.raw_items()) == dict(other.raw_items())
        elif isinstance(other, MutableMapping):
            return dict(other) == dict(self)
        else:
            return False

    def __getitem__(self, key):
        """Return value if it is not a reference, otherwise evaluate and return the reference value
        """
        value = self._data[key]
        if isinstance(value, ConfigReference):
            return value.dereference()
        return value

    def __setitem__(self, key, value):
        if key == ConfigNode.NAME_KEY:
            # need to index this node
            node = self
            name = value
            if name is None or not isinstance(name, str) or name[:1].isdigit():
                raise ValueError(
                    f"Invalid name {name} in file ({node.filename}). Must start with [a-zA-Z_]"
                )
            if ConfigReference.is_reference(name):
                # a name must be a string, or a direct reference to an object in config
Wout De Nolf's avatar
Wout De Nolf committed
383
                assert "." not in name
384
385
386
387
388
389
390
391
392
393
394
395
            else:
                if name in ConfigNode.indexed_nodes:
                    existing_node = ConfigNode.indexed_nodes[name]
                    if existing_node.filename == self.filename:
                        pass
                    else:
                        raise ValueError(
                            f"Duplicated name {name}, already in {existing_node.filename}"
                        )
                else:
                    ConfigNode.indexed_nodes[name] = node
        elif key == ConfigNode.USER_TAG_KEY:
396
            node = self
397
398
399
            user_tags = value if isinstance(value, MutableSequence) else [value]
            for tag in user_tags:
                ConfigNode.tagged_nodes[tag].add(node)
400
401
        elif key == ConfigNode.RPC_SERVICE_KEY:
            ConfigNode.services.add(self)
402
403
        self._data[key] = convert_value(value, self)

404
405
406
407
408
409
410
411
412
413
    def setdefault(self, key, value):
        """Re-implement 'setdefault' to not return value but element of the
        dict (once it is inserted).
        """
        try:
            return self[key]
        except KeyError:
            self[key] = value
            return self[key]

414
415
416
417
418
419
420
421
    def __delitem__(self, key):
        del self._data[key]

    def __iter__(self):
        return iter(self._data)

    def __len__(self):
        return len(self._data)
bliss administrator's avatar
bliss administrator committed
422

423
424
425
426
    def __hash__(self):
        return id(self)

    @property
427
    def filename(self):
428
        """Filename where the configuration of this node is located"""
429
430
431
432
433
        filename = self._filename
        if filename is None:
            if self._parent:
                return self._parent.filename
        return filename
434

435
436
    @property
    def parent(self):
437
        """Parent Node"""
438
439
440
441
        return self._parent

    @property
    def children(self):
442
        """List of children Nodes"""
443
        return self.get("__children__")
444
445
446

    @property
    def plugin(self):
447
        """Active plugin name for this Node or None if no plugin active"""
448
        plugin = self.get("plugin")
449
        if plugin:
450
            return plugin
451
452
453
454
455
        else:
            try:
                return self._parent.plugin
            except AttributeError:
                return  # no parent == root node, no plugin
456

457
458
459
460
461
462
463
464
    @property
    def is_service(self):
        """Is this node is serve with a rpc server"""
        through_server = self in ConfigNode.services
        if through_server is False and self._parent:
            return self._parent.is_service
        return through_server

465
466
467
468
469
470
471
472
473
474
475
    def get_top_key_node(self, key):
        topnode = None
        node = self
        while True:
            if node.get(key):
                topnode = node
            node = node._parent
            if node is None or "__children__" in node.keys():
                break
        return topnode

476
477
478
479
480
481
482
483
484
    def get_inherited_value_and_node(self, key):
        """
        @see get_inherited
        """
        value = self.get(key)
        if value is None and self._parent:
            return self._parent.get_inherited_value_and_node(key)
        return value, self

485
    def get_inherited(self, key, default=None):
486
487
488
489
490
491
492
493
494
        """
        Returns the value for the given config key. If the key does not exist
        in this Node it is searched recusively up in the Node tree until it
        finds a parent which defines it

        Args:
            key (str): key to search

        Returns:
495
496
            object: value corresponding to the key or a default if key is not found
            in the Node tree and default is provied (None if no default)
497
        """
498
499
        value = self.get_inherited_value_and_node(key)[0]
        return value if value is not None else default
500

501
    def pprint(self, indent=1, depth=None):
502
503
504
505
506
507
508
        """
        Pretty prints this Node

        Keyword Args:
            indent (int): indentation level (default: 1)
            depth (int): max depth (default: None, meaning no max)
        """
509
        self._pprint(self, 0, indent, 0, depth)
510

511
    def save(self):
512
513
514
        """
        Saves the Node configuration persistently in the server
        """
515
516
        # Get the original node, synchronize it with
        # the copied one
517
        filename = self.filename
518

519
520
        if filename is None:
            return  # Memory
521

522
523
524
525
526
        yaml = YAML(pure=True)
        yaml.allow_duplicate_keys = True
        yaml.default_flow_style = False
        try:
            yaml_contents = yaml.load(
527
                client.get_text_file(filename, self.config._connection)
528
529
530
            )
        except RuntimeError:
            # file does not exist
531
            yaml_contents = self.to_dict(resolve_references=False)
532
        else:
533
            prudent_update(
534
535
                ConfigNode.goto_path(yaml_contents, self.path),
                self.to_dict(resolve_references=False),
536
            )
537
538
539
540

        string_stream = StringIO()
        yaml.dump(yaml_contents, stream=string_stream)
        file_content = string_stream.getvalue()
541
        self.config.set_config_db_file(filename, file_content)
542

543
    def clone(self):
544
        """
545
        return a full copy of this node
546
        """
547
        node = pickle.loads(pickle.dumps(self, protocol=-1))
548
        # keep source node in case of saving
549
        return node
550

551
    def to_dict(self, resolve_references=True):
552
        """
553
        full copy and transform to dict object.
554

555
556
        the return object is a simple dictionary
        """
557
558
559
560
561
562
        if resolve_references:

            def decoder_hook(d):
                for k, v in d.items():
                    if isinstance(v, str) and ConfigReference.is_reference(v):
                        d[k] = ConfigReference(self.parent, v).dereference()
563
564
565
566
567
568
569
                    elif isinstance(v, list):
                        d[k] = [
                            ConfigReference(self.parent, item).dereference()
                            if ConfigReference.is_reference(item)
                            else item
                            for item in v
                        ]
570
571
572
573
574
575
576
                return d

            return json.JSONDecoder(object_hook=decoder_hook).decode(
                json.dumps(self._data, cls=ConfigNodeDictEncoder)
            )
        else:
            return json.loads(json.dumps(self._data, cls=ConfigNodeDictEncoder))
577
578

    @staticmethod
579
580
    def _pprint(node, cur_indet, indent, cur_depth, depth):
        space = " " * cur_indet
581
        print(f"{space}{{ filename: {repr(node.filename)}")
582
        dict_space = " " * (cur_indet + 2)
Vincent Michel's avatar
Vincent Michel committed
583
584
        for k, v in node.items():
            print("%s%s:" % (dict_space, k), end=" ")
585
            if isinstance(v, ConfigNode):
Vincent Michel's avatar
Vincent Michel committed
586
                print()
587
                ConfigNode._pprint(v, cur_indet + indent, indent, cur_depth + 1, depth)
588
            elif isinstance(v, MutableSequence):
589
                list_ident = cur_indet + indent
590
                list_space = " " * list_ident
Vincent Michel's avatar
Vincent Michel committed
591
                print("\n%s[" % list_space)
592
                for item in v:
593
                    if isinstance(item, ConfigNode):
Vincent Michel's avatar
Vincent Michel committed
594
                        print()
595
                        ConfigNode._pprint(
596
597
                            item, list_ident + indent, indent, cur_depth + 1, depth
                        )
598
                    else:
Vincent Michel's avatar
Vincent Michel committed
599
600
                        print(item)
                print("%s]" % list_space)
601
            else:
Vincent Michel's avatar
Vincent Michel committed
602
603
                print(v)
        print("%s}" % space)
604

605
606
607
608
    def __info__(self):
        value = repr(self._data)
        return "filename:<%s>,plugin:%r,%s" % (self.filename, self.plugin, value)

609
    def __repr__(self):
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
        return repr(self._data)


class ConfigNodeDictEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (ConfigNode, ConfigList, ConfigReference)):
            return obj.encode()
        return super().default(obj)


class RootConfigNode(ConfigNode):
    def __init__(self, config):
        super().__init__()
        self._config = config

    def __getstate__(self):
        d = super().__getstate__()
627
628
629
630
631
        d["config_object"] = (
            self._config._base_path,
            self._config._timeout,
            self._config.raise_yaml_exc,
        )
632
633
634
635
        return d

    def __setstate__(self, d):
        super().__setstate__(d)
636
        self._config = get_config(*d["config_object"])
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673

    @property
    def config(self):
        return self._config


def build_nodes_from_list(l, parent, path=None):
    result = ConfigList(parent)
    for i, value in enumerate(l):
        if isinstance(value, dict):
            node = ConfigNode(parent, path=[i] if path is None else [path, i])
            build_nodes_from_dict(value, node)
            result.append(node)
        elif isinstance(value, list):
            result.append(
                build_nodes_from_list(
                    value, parent, path=[i] if path is None else [path, i]
                )
            )
        else:
            result.append(value)
    return result


def build_nodes_from_dict(d, parent):
    if d is None:
        raise TypeError("Error parsing %r" % parent)
    else:
        for key, value in d.items():
            if isinstance(value, dict):
                node = ConfigNode(parent, path=[key])
                build_nodes_from_dict(value, node)
                parent[key] = node
            elif isinstance(value, list):
                parent[key] = build_nodes_from_list(value, parent, path=key)
            else:
                parent[key] = value
674

675

676
class InvalidConfig(RuntimeError):
677
678
679
    pass


680
class Config(metaclass=Singleton):
681
682
683
684
685
686
687
    """
    Bliss static configuration object.

    Typical usage is to call :func:`get_config` which will return an instance
    of this class.
    """

688
689
    def __init__(self, base_path, timeout=3, connection=None, raise_yaml_exc=True):
        self.raise_yaml_exc = raise_yaml_exc
690
        self._base_path = base_path
691
        self._timeout = timeout
692
        self._connection = connection or client.get_default_connection()
693
        self.invalid_yaml_files = dict()
694
695
        self._name2instance = weakref.WeakValueDictionary()
        self._name2cache = dict()
696
        self.reload(timeout=timeout)
697

698
699
700
701
702
    def close(self):
        self._clear_instances()
        channels.Bus.clear_cache()
        self._connection.close()

703
    def reload(self, base_path=None, timeout=3):
704
705
706
707
708
709
710
711
712
713
714
715
716
717
        """
        Reloads the configuration from the bliss server.

        Effectively cleans any cache (bliss objects and configuration tree)

        Keyword args:

            base_path (str): base path to config [default: empty string,
                             meaning full configuration]
            timeout (float): response timeout (seconds) [default: 3 seconds]

        Raises:
            RuntimeError: in case of connection timeout
        """
718
719
720
        if base_path is None:
            base_path = self._base_path

721
722
723
        ConfigNode.reset_cache()
        self._root_node = RootConfigNode(self)
        self._root_node["__children__"] = ConfigList(self._root_node)
Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
724

725
        self._clear_instances()
726
        self.invalid_yaml_files = dict()
727

728
729
730
        path2file = client.get_config_db_files(
            base_path=base_path, timeout=timeout, connection=self._connection
        )
731

732
        for path, file_content in path2file:
Matias Guijarro's avatar
Matias Guijarro committed
733
734
            if not file_content:
                continue
735
736
            base_path, file_name = os.path.split(path)
            fs_node, fs_key = self._get_or_create_path_node(base_path)
737
            if isinstance(fs_node, MutableSequence):
738
                continue
739

740
            try:
741
742
743
744
745
746
747
748
                try:
                    # typ='safe' -> Gives dict instead of OrderedDict subclass
                    # (removing comments)
                    # pure=True -> if False 052 is interpreted as octal (using C engine)

                    yaml = YAML(pure=True)
                    yaml.allow_duplicate_keys = True
                    d = yaml.load(file_content)
749
750
751
752
                except (
                    ruamel.yaml.scanner.ScannerError,
                    ruamel.yaml.parser.ParserError,
                ) as exp:
753
754
755
756
757
                    exp.note = "Error in YAML parsing:\n"
                    exp.note += "----------------\n"
                    exp.note += f"{file_content}\n"
                    exp.note += "----------------\n"
                    exp.note += "Hint: You can check your configuration with an on-line YAML validator like http://www.yamllint.com/ \n\n"
758
                    exp.problem_mark.name = path
759
                    if self.raise_yaml_exc:
760
761
762
763
764
765
                        raise exp
                    else:
                        raise InvalidConfig("Error in YAML parsing", path)
                except ruamel.yaml.error.MarkedYAMLError as exp:
                    if exp.problem_mark is not None:
                        exp.problem_mark.name = path
766
767
768
769
                    if self.raise_yaml_exc:
                        raise exp
                    else:
                        raise InvalidConfig("Error in YAML parsing", path)
770
771
772
773
774
775
776
777
778
779

                is_init_file = False
                if file_name.startswith("__init__"):
                    _, last_path = os.path.split(base_path)
                    is_init_file = not last_path.startswith("@")

                if is_init_file:
                    if d is None:
                        continue

780
781
782
783
784
785
                    if fs_key:
                        parents = fs_node[fs_key] = ConfigNode(fs_node, filename=path)
                    else:
                        parents = self._root_node = RootConfigNode(self)

                    parents["__children__"] = ConfigList(parents)
786
787
                    # do not accept a list in case of __init__ file
                    if isinstance(d, MutableSequence):
788
789
790
791
792
                        _msg = "List are not allowed in *%s* file" % path
                        if self.raise_yaml_exc:
                            raise TypeError(_msg)
                        else:
                            raise InvalidConfig(_msg, path)
793
                    try:
794
                        build_nodes_from_dict(d, parents)
795
                    except (TypeError, AttributeError):
796
                        _msg = (f"Error while parsing '{path}'",)
797
798
799
800
                        if self.raise_yaml_exc:
                            raise RuntimeError(_msg)
                        else:
                            raise InvalidConfig(_msg, path)
801

coutinho's avatar
coutinho committed
802
                    continue
803
                else:
804
                    if isinstance(d, MutableSequence):
805
806
807
                        parents = ConfigList(fs_node)
                        for i, item in enumerate(d):
                            local_parent = ConfigNode(fs_node, path, path=[i])
808
                            try:
809
810
                                build_nodes_from_dict(item, local_parent)
                            except (ValueError, TypeError, AttributeError):
811
812
                                _msg = f"Error while parsing a list on '{path}'"
                                if self.raise_yaml_exc:
813
814
815
816
817
818
                                    raise RuntimeError(_msg)
                                else:
                                    raise InvalidConfig(_msg, path)
                            else:
                                parents.append(local_parent)
                    else:
819
                        parents = ConfigNode(fs_node, path)
820
                        try:
821
822
                            build_nodes_from_dict(d, parents)
                        except (ValueError, TypeError, AttributeError):
823
824
                            _msg = f"Error while parsing '{path}'"
                            if self.raise_yaml_exc:
825
826
827
                                raise RuntimeError(_msg)
                            else:
                                raise InvalidConfig(_msg, path)
828

829
830
831
832
                if isinstance(fs_node, MutableSequence):
                    continue
                elif fs_key == "":
                    children = fs_node
833
                else:
834
835
836
                    children = fs_node.get(fs_key)

                if isinstance(children, MutableSequence):
837
                    if isinstance(parents, MutableSequence):
838
                        children.extend(parents)
839
                    else:
840
841
842
843
844
845
846
847
848
849
                        children.append(parents)
                elif children is not None:
                    # check if this node is __init__
                    children_node = children.get("__children__")
                    if isinstance(children_node, MutableSequence):  # it's an init node
                        if isinstance(parents, MutableSequence):
                            for p in parents:
                                p._parent = children
                                children_node.append(p)
                        else:
850
                            parents.reparent(children)
851
                            children_node.append(parents)
852
                    else:
853
854
855
856
857
858
859
860
861
862
863
864
865
                        if isinstance(parents, MutableSequence):
                            parents.append(children)
                            fs_node[fs_key] = parents
                        else:
                            fs_node[fs_key] = [children, parents]
                else:
                    fs_node[fs_key] = parents

            except InvalidConfig as exp:
                msg, path = exp.args[:2]
                self.invalid_yaml_files[path] = msg
                continue

866
867
    @property
    def names_list(self):
868
869
870
871
872
873
        """
        List of existing configuration names

        Returns:
            list<str>: sequence of configuration names
        """
874
        return sorted(list(ConfigNode.indexed_nodes.keys()))
875

876
877
878
879
880
881
882
883
    @property
    def user_tags_list(self):
        """
        List of existing user tags

        Returns:
            list<str>: sequence of user tag names
        """
884
        return sorted(list(ConfigNode.tagged_nodes.keys()))
885

886
887
888
889
890
891
    @property
    def service_names_list(self):
        return sorted(
            name for name, node in ConfigNode.indexed_nodes.items() if node.is_service
        )

892
893
    @property
    def root(self):
894
        """
895
        ConfigReference to the root :class:`~bliss.config.static.ConfigNode`
896
        """
897
898
        return self._root_node

899
    def set_config_db_file(self, filename, content):
900
901
902
903
904
905
906
907
908
909
910
911
        """
        Update the server filename with the given content

        Args:
            filename (str): YAML_ file name (path relative to configuration
                            base directory. Example: motion/icepap.yml)
            content (str): configuration content

        Raises:
            RuntimeError: in case of connection timeout
        """

912
913
        full_filename = os.path.join(self._base_path, filename)
        client.set_config_db_file(full_filename, content, connection=self._connection)
914
915

    def _get_or_create_path_node(self, base_path):
Matias Guijarro's avatar
Matias Guijarro committed
916
        node = self._root_node
917
918
919
920
921
        if "/" in base_path:
            sp_path = base_path.split("/")  # beacon server runs on linux
        else:
            sp_path = base_path.split("\\")  # beacon server runs on windows

922
923
924
925
926
927
928
929
        if sp_path[-1].startswith("@"):
            sp_path.pop()

        for i, p in enumerate(sp_path[:-1]):
            if p.startswith("@"):
                rel_init_path = os.path.join(*sp_path[: i + 1])
                init_path = os.path.join(rel_init_path, "__init__.yml")
                for c in self._file2node.get(init_path, []):
930
931
932
933
934
935
                    child = c
                    break
            else:
                try:
                    child = node.get(p)
                except AttributeError:
936
                    # because it's a list and we need a dict (reparent)
937
                    gp = node[0].parent
938
                    parent = ConfigNode(gp)
939
                    for c in node:
940
                        c.reparent(parent)
941
942
943
944
                    gp[sp_path[i - 1]] = gp
                    node = gp
                    child = None

Matias Guijarro's avatar
Matias Guijarro committed
945
            if child is None:
946
                child = ConfigNode(node)
Matias Guijarro's avatar
Matias Guijarro committed
947
948
                node[p] = child
            node = child
949

950
        sp_path = [x for x in sp_path if not x.startswith("@")]
951
952

        return node, sp_path and sp_path[-1]
Matias Guijarro's avatar
Matias Guijarro committed
953

954
    def get_config(self, name):
955
        """
956
        Returns the config :class:`~bliss.config.static.ConfigNode` with the
957
958
959
960
961
962
        given name

        Args:
            name (str): config node name

        Returns:
963
            ~bliss.config.static.ConfigNode: config node or None if object is
964
965
            not found
        """
966
        return ConfigNode.indexed_nodes.get(name)
967

968
969
    def get_user_tag_configs(self, tag_name):
        """
970
        Returns the set of config nodes (:class:`~bliss.config.static.ConfigNode`)
971
972
973
974
975
976
977
978
        which have the given user *tag_name*.

        Args:
            tag_name (str): user tag name

        Returns:
            set<Node>: the set of nodes wich have the given user tag
        """
979
        return set(ConfigNode.tagged_nodes.get(tag_name, ()))
980

Sebastien Petitdemange's avatar
Sebastien Petitdemange committed
981
    def get(self, name):
982
983
984
        """
        Returns an object instance from its configuration name

985
        If names starts with *$* it means it is a reference to an existing object in the config
986
987
        If the reference contains '.', the specified attribute can be evaluated
        by calling '.dereference()'
988
989
990
991
992

        Args:
            name (str): config node name

        Returns:
993
            ~bliss.config.static.ConfigNode: config node
994
995
996
997

        Raises:
            RuntimeError: if name is not found in configuration
        """
998
999
1000
        return self._get(name)

    def _get(self, name, direct_access=False):