Salome HOME
Merge branch 'master' into ngr/python3_dev
[tools/configuration.git] / config / sconfig / salome_config.py
1 #  -*- coding: iso-8859-1 -*-
2
3 """
4 Manage SALOME configuration.
5
6 Typical usage:
7
8 tool = CfgTool()
9 # Create configuration and set its parameters
10 tool.set("cfg", "name", "V7_6_0", "comment", "SALOME version 7.6.0")
11 # Add product
12 tool.set("boost", "version", "1.52.2", "url", "https://sourceforge.net/projects/boost/files/boost/1.52.0/boost_1_52_0.tar.gz", "comment", "Set of libraries for the C++ programming language")
13 # Add patches to the product (note: patch file should  be manually put to the patches directory)
14 tool.set("boost.patches.boost_patch_1.patch", "comment", "Fixes compilation problems on some platforms")
15 tool.set("boost.patches.boost_patch_2.patch", "comment", "gcc 5 compatibility")
16 # Inspect configuration: give all products
17 tool.get("cfg", "products")
18 # Inspect configuration: give parameters of the configuration
19 tool.get("cfg", "name")
20 tool.get("cfg", "comment")
21 # Inspect configuration: give parameters of the product
22 tool.get("boost", "version")
23 tool.get("boost", "url")
24 # Inspect configuration: give patches for the product
25 tool.get("boost", "patches")
26 # Verify configuration
27 conf_ok = tool.verify()
28 # Verify product
29 boost_ok = tool.verify("boost")
30 # Dump configuration
31 tool.dump()
32 # Dump product
33 tool.dump("boost")
34 # Remove parameters of configuration
35 tool.remove("cfg", "comment")
36 # Remove parameters of product
37 tool.remove("boost", "url")
38 # Remove patch from product
39 tool.remove("boost.patches.boost_patch_2.patch")
40 # Remove product
41 tool.remove("boost")
42 # Clean configuration
43 tool.clean()
44 """
45
46 import os
47 import xml.etree.ElementTree as ET
48 try:
49     exceptionClass = ET.ParseError
50 except:
51     import xml.parsers.expat
52     exceptionClass = xml.parsers.expat.ExpatError
53
54 __all__ = [
55     "defaultConfFile",
56     "configTag",
57     "softwareTag",
58     "patchesTag",
59     "patchTag",
60     "nameAttr",
61     "commentAttr",
62     "versionAttr",
63     "urlAttr",
64     "supportedTags",
65     "supportedAttributes",
66     "tagAttributes",
67     "tagChildren",
68     "softwareAlias",
69     "patchesAlias",
70     "childAlias",
71     "pathSeparator",
72     "CfgTool",
73  ]
74
75 def defaultConfFile():
76     """
77     Return path to the default SALOME configuration file (string).
78     """
79     return os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "salome.xml"))
80
81 def configTag():
82     """
83     Return XML tag for configuration (string).
84     """
85     return "config"
86
87 def softwareTag():
88     """
89     Return XML tag for software (string).
90     """
91     return "product"
92
93 def patchesTag():
94     """
95     Return XML tag for patches set (string).
96     """
97     return "patches"
98     
99 def patchTag():
100     """
101     Return XML tag for patch (string).
102     """
103     return "patch"
104
105 def nameAttr():
106     """
107     Return XML attribute for name parameter (string).
108     """
109     return "name"
110
111 def commentAttr():
112     """
113     Return XML attribute for comment parameter (string).
114     """
115     return "comment"
116
117 def versionAttr():
118     """
119     Return XML attribute for version parameter (string).
120     """
121     return "version"
122
123 def urlAttr():
124     """
125     Return XML attribute for url parameter (string).
126     """
127     return "url"
128
129 def supportedTags():
130     """
131     Return list of all supported XML tags (list of strings).
132     """
133     return [configTag(), softwareTag(), patchesTag(), patchTag()]
134     
135 def supportedAttributes():
136     """
137     Return list of all supported XML attributes (list of strings).
138     """
139     return [nameAttr(), commentAttr(), versionAttr(), urlAttr()]
140     
141 def tagAttributes(tag, force = False):
142     """
143     Return list of attributes supported for the specified XML tag.
144     
145     Parameters:
146       tag: XML tag.
147       force: if True, all supported attributes are returned, including "special" ones.
148
149     Return value is list of strings.
150     """
151     attrs = {}
152     if tag == configTag():
153         # config
154         attrs[nameAttr()]     = False  # optional
155         attrs[commentAttr()]  = False  # optional
156         pass
157     elif tag == softwareTag():
158         # software
159         # note: name for software is specified implicitly via the target path
160         if force:
161             attrs[nameAttr()] = True   # mandatory
162             pass
163         attrs[versionAttr()]  = True   # mandatory
164         attrs[urlAttr()]      = False  # optional
165         attrs[commentAttr()]  = False  # optional
166         pass
167     elif tag == patchTag():
168         # patch
169         # note: name for patch is specified implicitly via the target path
170         if force:
171             attrs[nameAttr()] = True   # mandatory
172             pass
173         pass
174         attrs[urlAttr()]      = False
175         attrs[commentAttr()]  = False
176     return attrs
177
178 def tagChildren(tag):
179     """
180     Return supported child nodes' tags for given XML element.
181     Note: None means root 'config' XML element.
182     
183     Parameters:
184       tag: XML tag.
185
186     Return value is list of strings.
187     """
188     ctags = []
189     if tag == configTag():     ctags += [softwareTag()]
190     elif tag == softwareTag(): ctags += [patchesTag()]
191     elif tag == patchesTag():  ctags += [patchTag()]
192     elif tag is None:          ctags += [configTag()]
193     return ctags
194
195 def softwareAlias():
196     """
197     Return parameter's alias for list of software are to be used with 'get' command (string).
198     """
199     return softwareTag()+"s"
200
201 def patchesAlias():
202     """
203     Return parameter's alias for list patches to be used with 'get' command (string).
204     """
205     return patchesTag()
206
207 def childAlias(tag, param):
208     """
209     Return children node tag for children list alias.
210     
211     Parameters:
212       tag: XML tag.
213       param: children list alias.
214
215     Return child node tag name or None if alias is unsupported.
216     """
217     ctag = None
218     if tag == configTag():
219         if param == softwareAlias(): ctag = softwareTag()
220         pass
221     elif tag == softwareTag():
222         if param == patchesAlias(): ctag = patchTag()
223         pass
224     return ctag
225
226 def pathSeparator():
227     """
228     Return string used as a separator of path's component (string).
229     """
230     return "."
231
232 class CfgTool(object):
233     """
234     A tool to manage SALOME configuration files.
235     """
236     def __init__(self, cfgFile=None):
237         """
238         Constructor.
239         
240         Parameters:
241           cfgFile: a path to the configuration file (string);
242                    if not specified, default one is used.
243         """
244         self.enc = "utf-8"
245         self.cfgFile = cfgFile if cfgFile else defaultConfFile()
246         try:
247             self.tree = ET.parse(self.cfgFile).getroot()
248             self._checkConfig()
249             pass
250         except IOError as e:
251             self.tree = self._new()
252             pass
253         except exceptionClass as e:
254             if e.code == 3: # no element found, it's OK
255                 self.tree = self._new()
256             else:
257                 raise Exception("bad XML file %s: %s" % (self.cfgFile, str(e)))
258             pass
259         except Exception as e:
260             raise Exception("unkwnown error: %s" % str(e))
261         pass
262     
263     def encoding(self):
264         """
265         Return current encoding of the configuration file (string).
266         Default is "utf-8".
267         """
268         return self.enc
269     
270     def setEncoding(self, value):
271         """
272         Set encoding for configuration file..
273         Parameters:
274           value: new encoding to be used when writing configuration file (string).
275         """
276         self.enc = value
277         self._write()
278         pass
279
280     def get(self, target, param):
281         """
282         Get value of specified object's parameter.
283         Parameter can be a specific keyword that refers to the list of
284         child nodes. In this case the function returns list that
285         contains names of all child nodes.
286
287         Parameters:
288           target: object being checked (string).
289           param: parameter which value is being inspected.
290          
291         Return value is string or list of strings.
292         """
293         path = self._processTarget(target)
294         tag = path[-1][0]
295         elem = self._findPath(path)
296         if elem is None:
297             raise Exception("no such target %s" % target)
298         result = None
299         if childAlias(tag, param):
300             result = self._children(elem, childAlias(tag, param))
301             pass
302         elif param in tagAttributes(tag):
303             result = elem.get(param) if elem is not None and elem.get(param) else ""
304             pass
305         else:
306             raise Exception("unsupported parameter %s for target %s" % (param, target))
307         return result
308
309     def set(self, target = None, *args, **kwargs):
310         """
311         Create or modify an object in the SALOME configuration.
312
313         Parameters:
314           target: object being created or modified (string); if not specified,
315                   parameters of config itself will be modified.
316           args:   positional arguments that describe parameters to be set (couple);
317                   each couple of arguments specifies a parameter and its value
318                   (strings).
319           kwargs: keyword arguments - same as 'args' but specified in form of
320                   dictionary.
321         """
322         path = self._processTarget(target)
323         tag = path[-1][0]
324         params = {}
325         # process keyword arguments
326         for param, value in list(kwargs.items()):
327             if param not in tagAttributes(tag):
328                 raise Exception("unsupported parameter %s for target %s" % (param, target))
329             params[param] = value
330             pass
331         # process positional arguments
332         i = 0
333         while i < len(args):
334             param = args[i]
335             if param not in tagAttributes(tag):
336                 raise Exception("unsupported parameter %s for target %s" % (param, target))
337             value = ""
338             if i+1 < len(args) and args[i+1] not in tagAttributes(tag):
339                 value = args[i+1]
340                 i += 1
341                 pass
342             params[param] = value
343             i += 1
344             pass
345         # create / modify target
346         elem = self._findPath(path, True)
347         for param, value in list(params.items()):
348             elem.set(param, value)
349             pass
350         self._write()
351         pass
352
353     def remove(self, target, *args):
354         """
355         Remove object or its parameter(s).
356
357         Parameters:
358           target: object (string).
359           args: list of parameters which have to be removed (strings).
360          
361         Return value is string.
362         """
363         path = self._processTarget(target)
364         tag = path[-1][0]
365         elem = self._findPath(path)
366         if elem is None:
367             raise Exception("no such target %s" % target)
368         if args:
369             # remove attributes of the target
370             # first check that all attributes are valid
371             for param in args:
372                 if param not in tagAttributes(tag):
373                     raise Exception("unsupported parameter %s for target %s" % (param, target))
374                 elif param not in elem.attrib:
375                     raise Exception("parameter %s is not set for target %s" % (param, target))
376                 pass
377             # now remove all attributes
378             for param in args:
379                 elem.attrib.pop(param)
380                 pass
381             pass
382         else:
383             # remove target
384             if elem == self.tree:
385                 self.tree = self._new()
386                 pass
387             else:
388                 path = path[:-1]
389                 parent = self._findPath(path)
390                 if parent is not None: parent.remove(elem)
391                 pass
392             pass
393         self._write()
394         pass
395     
396     def dump(self, target = None):
397         """
398         Dump the configuration.
399         
400         Parameters:
401           target: object (string); if not specified, all configuration is dumped.
402         """
403         if target is not None:
404             path = self._processTarget(target)
405             elem = self._findPath(path)
406             if elem is None:
407                 raise Exception("no such target %s" % target)
408             pass
409         else:
410             elem = self.tree
411             pass
412         self._dump(elem)
413         pass
414
415     def verify(self, target = None, errors = None):
416         """
417         Verify configuration
418         
419         Parameters:
420           target: object (string); if not specified, all configuration is verified.
421
422         Returns True if object is valid or False otherwise.
423         """
424         if errors is None: errors = []
425         if target is not None:
426             path = self._processTarget(target)
427             elem = self._findPath(path)
428             if elem is None:
429                 raise Exception("no such target %s" % target)
430             pass
431         else:
432             elem = self.tree
433             pass
434         return self._verifyTag(elem, errors)
435
436     def clean(self):
437         """
438         Clean the configuration.
439         """
440         self.tree = self._new()
441         self._write()
442         pass
443     
444     def patchesDir(self):
445         """
446         Return path to the patches directory (string).
447         """
448         return os.path.join(os.path.dirname(self.cfgFile), "patches")
449
450     def _new(self):
451         """
452         (internal)
453         Create and return new empty root element.
454         
455         Return values is an XML element (xml.etree.ElementTree.Element).
456         """
457         return ET.Element(configTag())
458
459     def _makeChild(self, elem, tag):
460         """
461         (internal)
462         Create child element for given parent element.
463
464         Parameters:
465           elem: XML element (xml.etree.ElementTree.Element).
466           tag: tag of the child element
467
468         Return value is new XML element (xml.etree.ElementTree.Element).
469         """
470         child = ET.SubElement(elem, tag)
471         child._parent = elem # set parent!!!
472         return child
473
474     def _processTarget(self, target):
475         """
476         (internal)
477         Check target and return XML path for it.
478
479         Parameters:
480           target: target path.
481           
482         Return value is a list of tuples; each tuple is a couple
483         of path component and optional component's name.
484         """
485         if target is None: target = ""
486         comps = [i.strip() for i in target.split(pathSeparator())]
487         path = []
488         # add root to the path
489         path.append((configTag(), None))
490         if comps[0] in ["", "cfg", configTag()]: comps = comps[1:]
491         if comps:
492             # second component of path can be only "software"
493             if not comps[0] or comps[0] in supportedTags() + supportedAttributes() + ["cfg"]:
494                 raise Exception("badly specified target '%s'" % target)
495             path.append((softwareTag(), comps[0]))
496             comps = comps[1:]
497             pass
498         if comps:
499             # third component of path can be only "patches" or patch
500             if comps[0] not in [patchesTag(), patchTag()]:
501                 raise Exception("badly specified target '%s'" % target)
502             path.append((patchesTag(), None))
503             comps = comps[1:]
504             pass
505         if comps:
506             # fourth component of path can be only a patch name
507             path.append((patchTag(), pathSeparator().join(comps)))
508             pass
509         return path
510     
511     def _findPath(self, path, create=False):
512         """
513         (internal)
514         Find and return XML element specified by its path.
515         If path does not exist and 'create' is True, XML element will be created.
516
517         Parameters:
518           path: XML element's path data (see _processTarget()).
519           create: flag that forces creating XML element if it does not exist
520                   (default is False).
521
522         Return value is an XML element (xml.etree.ElementTree.Element).
523         """
524         if len(path) == 1:
525             if path[0][0] != configTag():
526                 raise Exception("error parsing target path")
527             return self.tree
528         elem = self.tree
529         for tag, name in path[1:]:
530             if name:
531                 children = [i for i in elem.getchildren() if i.tag == tag and i.get(nameAttr()) == name]
532                 if len(children) > 1:
533                     raise Exception("error parsing target path: more than one child element found")
534                 elif len(children) == 1:
535                     elem = children[0]
536                     pass
537                 elif create:
538                     elem = self._makeChild(elem, tag)
539                     elem.set(nameAttr(), name)
540                     pass
541                 else:
542                     return None
543                 pass
544             else:
545                 children = [i for i in elem.getchildren() if i.tag == tag]
546                 if len(children) > 1:
547                     raise Exception("error parsing target path: more than one child element found")
548                 elif len(children) == 1:
549                     elem = children[0]
550                     pass
551                 elif create:
552                     elem = self._makeChild(elem, tag)
553                     pass
554                 else:
555                     return None
556                 pass
557             pass
558         return elem
559
560     def _path(self, elem):
561         """
562         (internal)
563         Construct path to the XML element.
564
565         Parameters:
566           elem: XML element (xml.etree.ElementTree.Element).
567
568         Return value is string.
569         """
570         def _mkname(_obj):
571             _name = _obj.tag
572             attrs = tagAttributes(_obj.tag, True)
573             if nameAttr() in attrs and attrs[nameAttr()]:
574                 if nameAttr() not in list(_obj.keys()): _name += " [unnamed]"
575                 else: _name += " [%s]" % _obj.get(nameAttr())
576                 pass
577             return _name
578         path = []
579         while elem is not None:
580             path.append(_mkname(elem))
581             elem = elem._parent if hasattr(elem, "_parent") else None
582             pass
583         path.reverse()
584         return pathSeparator().join(path)
585
586     def _children(self, elem, param):
587         """
588         (internal)
589         Get names of children nodes for element.
590
591         Parameters:
592           elem: XML element (xml.etree.ElementTree.Element).
593           param: name of the children' tag.
594
595         Return value is a list of names of child elements (strings).
596         """
597         result = []
598         result += [i.get(nameAttr()) for i in \
599                        [i for i in elem.getchildren() if i.tag == param and i.get(nameAttr())]]
600         for c in elem.getchildren():
601             result += self._children(c, param)
602             pass
603         return result
604         
605     def _write(self):
606         """
607         (internal)
608         Write data tree content to the associated XML file.
609         """
610         try:
611             with open(self.cfgFile, 'w') as f:
612                 # write header
613                 f.write('<?xml version="1.0" encoding="%s" ?>\n' % self.encoding() )
614                 f.write('<!DOCTYPE config>\n')
615                 # prettify content
616                 self._prettify(self.tree)
617                 # write content
618                 et = ET.ElementTree(self.tree)
619                 et.write(f, self.encoding())
620                 pass
621             pass
622         except IOError as e:
623             raise Exception("can't write to %s: %s" % (self.cfgFile, e.strerror))
624         pass
625
626     def _prettify(self, elem, level=0, hasSiblings=False):
627         """
628         (internal)
629         Prettify XML file content.
630
631         Parameters:
632           elem: XML element (xml.etree.ElementTree.Element).
633           level: indentation level.
634           hasSiblings: True when item has siblings (i.e. this is not the last item
635              in the parent's children list).
636         """
637         indent = "  "
638         children = elem.getchildren()
639         tail = "\n"
640         if hasSiblings: tail += indent * level
641         elif level > 0: tail += indent * (level-1)
642         text = None
643         if children: text = "\n" + indent * (level+1)
644         elem.tail = tail
645         elem.text = text
646         for i in range(len(children)):
647             self._prettify(children[i], level+1, len(children)>1 and i+1<len(children))
648             pass
649         pass
650
651     def _dump(self, elem, level=0):
652         """
653         (internal)
654         Dump XML element.
655
656         Parameters:
657           elem: XML element (xml.etree.ElementTree.Element).
658           level: indentation level.
659         """
660         if elem is None:
661             return
662         indent = "  "
663         # dump element
664         print("%s%s" % (indent * level, elem.tag))
665         attrs = tagAttributes(elem.tag, True)
666         format = "%" + "-%ds" % max([len(i) for i in supportedAttributes()]) + " : %s"
667         for a in attrs:
668             if a in list(elem.attrib.keys()):
669                 print(indent*(level+1) + format % (a, elem.get(a)))
670                 pass
671             pass
672         print()
673         # dump all childrens recursively
674         for c in elem.getchildren():
675             self._dump(c, level+1)
676             pass
677         pass
678
679     def _checkConfig(self):
680         """
681         (internal)
682         Verify configuration (used to check validity of associated XML file).
683         """
684         errors = []
685         self._checkTag(self.tree, None, errors)
686         if errors:
687             errors = ["Bad XML format:"] + ["- %s" % i for i in errors]
688             raise Exception("\n".join(errors))
689         pass
690
691     def _checkTag(self, elem, tag, errors):
692         """
693         (internal)
694         Check if format of given XML element is valid.
695         
696         Parameters:
697           elem: XML element (xml.etree.ElementTree.Element).
698           tag: expected XML element's tag (string).
699           errors: output list to collect error messages (strings).
700         """
701         if elem.tag not in tagChildren(tag):
702             errors.append("bad XML element: %s" % elem.tag)
703         else:
704             # check attributes
705             attrs = list(elem.keys())
706             for attr in attrs:
707                 if attr not in tagAttributes(elem.tag, True):
708                     errors.append("unsupported attribute '%s' for XML element '%s'" % (attr, elem.tag))
709                     pass
710                 pass
711             # check all childrens recursively
712             children = elem.getchildren()
713             for child in children:
714                 child._parent = elem # set parent!!!
715                 self._checkTag(child, elem.tag, errors)
716                 pass
717             pass
718         pass
719
720     def _verifyTag(self, elem, errors):
721         """
722         (internal)
723         Verify given XML element is valid in terms of SALOME configuration.
724         
725         Parameters:
726           elem: XML element (xml.etree.ElementTree.Element).
727           errors: output list to collect error messages (strings).
728         """
729         attrs = tagAttributes(elem.tag, True)
730         # check mandatory attributes
731         for attr in attrs:
732             if attrs[attr] and (attr not in list(elem.keys()) or not elem.get(attr).strip()):
733                 errors.append("mandatory parameter '%s' of object '%s' is not set" % (attr, self._path(elem)))
734                 pass
735             pass
736         # specific check for particular XML element
737         try:
738             self._checkObject(elem)
739         except Exception as e:
740             errors.append("%s : %s" % (self._path(elem), str(e)))
741         # check all childrens recursively
742         for c in elem.getchildren():
743             self._verifyTag(c, errors)
744             pass
745         return len(errors) == 0
746
747     def _checkObject(self, elem):
748         """
749         (internal)
750         Perform specific check for given XML element.
751         
752         Raises an exception that if object is invalid.
753         
754         Parameters:
755           elem: XML element (xml.etree.ElementTree.Element).
756         """
757         if elem.tag == patchTag():
758             filename = elem.get(nameAttr())
759             url = elem.get(urlAttr())
760             if filename and not url:
761                 # if url is not given, we should check that file is present locally
762                 filepath = os.path.join(self.patchesDir(), filename)
763                 if not os.path.exists(filepath):
764                     raise Exception("patch file %s is not found" % filepath)
765                 pass
766             else:
767                 # TODO: we might check validity of URL here (see urlparse)!
768                 pass
769             pass
770         pass
771
772     pass # class CfgTool