]> SALOME platform Git repositories - tools/sat.git/blob - src/pyconf.py
Salome HOME
Factorisation des jobs. Voir https://codev-tuleap.cea.fr/plugins/tracker/?aid=8543...
[tools/sat.git] / src / pyconf.py
1 #!/usr/bin/env python
2 #-*- coding:utf-8 -*-
3
4 # Copyright 2004-2007 by Vinay Sajip. All Rights Reserved.
5 #
6 # Permission to use, copy, modify, and distribute this software and its
7 # documentation for any purpose and without fee is hereby granted,
8 # provided that the above copyright notice appear in all copies and that
9 # both that copyright notice and this permission notice appear in
10 # supporting documentation, and that the name of Vinay Sajip
11 # not be used in advertising or publicity pertaining to distribution
12 # of the software without specific, written prior permission.
13 # VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
14 # ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
15 # VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
16 # ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
17 # IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
18 # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19
20 #  Copyright (C) 2010-2013  CEA/DEN
21 #
22 #  This library is free software; you can redistribute it and/or
23 #  modify it under the terms of the GNU Lesser General Public
24 #  License as published by the Free Software Foundation; either
25 #  version 2.1 of the License.
26 #
27 #  This library is distributed in the hope that it will be useful,
28 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
29 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
30 #  Lesser General Public License for more details.
31 #
32 #  You should have received a copy of the GNU Lesser General Public
33 #  License along with this library; if not, write to the Free Software
34 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
35
36 # CEA adds : 
37 # Possibility to overwrites value in a pyconf file
38 # Python 3 porting
39
40
41 """
42 This is a configuration module for Python.
43
44 This module should work under Python versions >= 2.2, and cannot be used with
45 earlier versions since it uses new-style classes.
46
47 Development and testing has only been carried out (so far) on Python 2.3.4 and
48 Python 2.4.2. See the test module (test_config.py) included in the
49 U{distribution<http://www.red-dove.com/python_config.html|_blank>} (follow the
50 download link).
51
52 A simple example - with the example configuration file::
53
54     messages:
55     [
56       {
57         stream : `sys.stderr`
58         message: 'Welcome'
59         name: 'Harry'
60       }
61       {
62         stream : `sys.stdout`
63         message: 'Welkom'
64         name: 'Ruud'
65       }
66       {
67         stream : $messages[0].stream
68         message: 'Bienvenue'
69         name: Yves
70       }
71     ]
72
73 a program to read the configuration would be::
74
75     from config import Config
76
77     f = file('simple.cfg')
78     cfg = Config(f)
79     for m in cfg.messages:
80         s = '%s, %s' % (m.message, m.name)
81         try:
82             print >> m.stream, s
83         except IOError, e:
84             print e
85
86 which, when run, would yield the console output::
87
88     Welcome, Harry
89     Welkom, Ruud
90     Bienvenue, Yves
91
92 See U{this tutorial<http://www.red-dove.com/python_config.html|_blank>} for more
93 information.
94
95 #modified for salomeTools
96 @version: 0.3.7.1
97
98 @author: Vinay Sajip
99
100 @copyright: Copyright (C) 2004-2007 Vinay Sajip. All Rights Reserved.
101
102
103 @var streamOpener: The default stream opener. This is a factory function which
104 takes a string (e.g. filename) and returns a stream suitable for reading. If
105 unable to open the stream, an IOError exception should be thrown.
106
107 The default value of this variable is L{defaultStreamOpener}. For an example
108 of how it's used, see test_config.py (search for streamOpener).
109 """
110
111 __author__  = "Vinay Sajip <vinay_sajip@red-dove.com>"
112 __status__  = "alpha"
113 __version__ = "0.3.7.1" #modified for salomeTools
114 __date__    = "05 October 2007"
115
116 import codecs
117 import os
118 import sys
119
120 WORD = 'a'
121 NUMBER = '9'
122 STRING = '"'
123 EOF = ''
124 LCURLY = '{'
125 RCURLY = '}'
126 LBRACK = '['
127 LBRACK2 = 'a['
128 RBRACK = ']'
129 LPAREN = '('
130 LPAREN2 = '(('
131 RPAREN = ')'
132 DOT = '.'
133 COMMA = ','
134 COLON = ':'
135 AT = '@'
136 PLUS = '+'
137 MINUS = '-'
138 STAR = '*'
139 SLASH = '/'
140 MOD = '%'
141 BACKTICK = '`'
142 DOLLAR = '$'
143 TRUE = 'True'
144 FALSE = 'False'
145 NONE = 'None'
146
147 WORDCHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"
148
149 if sys.platform == 'win32':
150     NEWLINE = '\r\n'
151 elif os.name == 'mac':
152     NEWLINE = '\r'
153 else:
154     NEWLINE = '\n'
155
156 try:
157     has_utf32 = True
158 except:
159     has_utf32 = False
160
161 class ConfigInputStream(object):
162     """
163     An input stream which can read either ANSI files with default encoding
164     or Unicode files with BOMs.
165
166     Handles UTF-8, UTF-16LE, UTF-16BE. Could handle UTF-32 if Python had
167     built-in support.
168     """
169     def __init__(self, stream):
170         """
171         Initialize an instance.
172
173         @param stream: The underlying stream to be read. Should be seekable.
174         @type stream: A stream (file-like object).
175         """
176         encoding = None
177         signature = stream.read(4)
178         used = -1
179         if has_utf32:
180             if signature == codecs.BOM_UTF32_LE:
181                 encoding = 'utf-32le'
182             elif signature == codecs.BOM_UTF32_BE:
183                 encoding = 'utf-32be'
184         if encoding is None:
185             if signature[:3] == codecs.BOM_UTF8:
186                 used = 3
187                 encoding = 'utf-8'
188             elif signature[:2] == codecs.BOM_UTF16_LE:
189                 used = 2
190                 encoding = 'utf-16le'
191             elif signature[:2] == codecs.BOM_UTF16_BE:
192                 used = 2
193                 encoding = 'utf-16be'
194             else:
195                 used = 0
196         if used >= 0:
197             stream.seek(used)
198         if encoding:
199             reader = codecs.getreader(encoding)
200             stream = reader(stream)
201         self.stream = stream
202         self.encoding = encoding
203
204     def read(self, size):
205         if (size == 0) or (self.encoding is None):
206             rv = self.stream.read(size)
207         else:
208             rv = u''
209             while size > 0:
210                 rv += self.stream.read(1)
211                 size -= 1
212         return rv
213
214     def close(self):
215         self.stream.close()
216
217     def readline(self):
218         if self.encoding is None:
219             line = ''
220         else:
221             line = u''
222         while True:
223             c = self.stream.read(1)
224             if isinstance(c, bytes):
225                 c = c.decode()
226             if c:
227                 line += c
228             if c == '\n':
229                 break
230         return line
231
232 class ConfigOutputStream(object):
233     """
234     An output stream which can write either ANSI files with default encoding
235     or Unicode files with BOMs.
236
237     Handles UTF-8, UTF-16LE, UTF-16BE. Could handle UTF-32 if Python had
238     built-in support.
239     """
240
241     def __init__(self, stream, encoding=None):
242         """
243         Initialize an instance.
244
245         @param stream: The underlying stream to be written.
246         @type stream: A stream (file-like object).
247         @param encoding: The desired encoding.
248         @type encoding: str
249         """
250         if encoding is not None:
251             encoding = str(encoding).lower()
252         self.encoding = encoding
253         if encoding == "utf-8":
254             stream.write(codecs.BOM_UTF8)
255         elif encoding == "utf-16be":
256             stream.write(codecs.BOM_UTF16_BE)
257         elif encoding == "utf-16le":
258             stream.write(codecs.BOM_UTF16_LE)
259         elif encoding == "utf-32be":
260             stream.write(codecs.BOM_UTF32_BE)
261         elif encoding == "utf-32le":
262             stream.write(codecs.BOM_UTF32_LE)
263
264         if encoding is not None:
265             writer = codecs.getwriter(encoding)
266             stream = writer(stream)
267         self.stream = stream
268
269     def write(self, data):
270         self.stream.write(data)
271
272     def flush(self):
273         self.stream.flush()
274
275     def close(self):
276         self.stream.close()
277
278 def defaultStreamOpener(name):
279     """
280     This function returns a read-only stream, given its name. The name passed
281     in should correspond to an existing stream, otherwise an exception will be
282     raised.
283
284     This is the default value of L{streamOpener}; assign your own callable to
285     streamOpener to return streams based on names. For example, you could use
286     urllib2.urlopen().
287
288     @param name: The name of a stream, most commonly a file name.
289     @type name: str
290     @return: A stream with the specified name.
291     @rtype: A read-only stream (file-like object)
292     """
293     return ConfigInputStream(open(name, 'rb'))
294
295 streamOpener = None
296
297 __resolveOverwrite__ = True
298
299 class ConfigError(Exception):
300     """
301     This is the base class of exceptions raised by this module.
302     """
303     pass
304
305 class ConfigFormatError(ConfigError):
306     """
307     This is the base class of exceptions raised due to syntax errors in
308     configurations.
309     """
310     pass
311
312 class ConfigResolutionError(ConfigError):
313     """
314     This is the base class of exceptions raised due to semantic errors in
315     configurations.
316     """
317     pass
318
319 def isWord(s):
320     """
321     See if a passed-in value is an identifier. If the value passed in is not a
322     string, False is returned. An identifier consists of alphanumerics or
323     underscore characters.
324
325     Examples::
326
327         isWord('a word') ->False
328         isWord('award') -> True
329         isWord(9) -> False
330         isWord('a_b_c_') ->True
331
332     @note: isWord('9abc') will return True - not exactly correct, but adequate
333     for the way it's used here.
334
335     @param s: The name to be tested
336     @type s: any
337     @return: True if a word, else False
338     @rtype: bool
339     """
340     if type(s) != type(''):
341         return False
342     s = s.replace('_', '')
343     return s.isalnum()
344
345 def makePath(prefix, suffix):
346     """
347     Make a path from a prefix and suffix.
348
349     Examples::
350
351         makePath('', 'suffix') -> 'suffix'
352         makePath('prefix', 'suffix') -> 'prefix.suffix'
353         makePath('prefix', '[1]') -> 'prefix[1]'
354
355     @param prefix:  The prefix to use. If it evaluates as false, the suffix
356                     is returned.
357     @type prefix:   str
358     @param suffix:  The suffix to use. It is either an identifier or an
359                     index in brackets.
360     @type suffix:   str
361     @return:        The path concatenation of prefix and suffix, with a
362                     dot if the suffix is not a bracketed index.
363     @rtype:         str
364
365     """
366     if not prefix:
367         rv = suffix
368     elif suffix[0] == '[':
369         rv = prefix + suffix
370     else:
371         rv = prefix + '.' + suffix
372     return rv
373
374
375 class Container(object):
376     """
377     This internal class is the base class for mappings and sequences.
378
379     @ivar path: A string which describes how to get
380     to this instance from the root of the hierarchy.
381
382     Example::
383
384         a.list.of[1].or['more'].elements
385     """
386     def __init__(self, parent):
387         """
388         Initialize an instance.
389
390         @param parent: The parent of this instance in the hierarchy.
391         @type parent: A L{Container} instance.
392         """
393         object.__setattr__(self, 'parent', parent)
394
395     def setPath(self, path):
396         """
397         Set the path for this instance.
398         @param path: The path - a string which describes how to get
399         to this instance from the root of the hierarchy.
400         @type path: str
401         """
402         object.__setattr__(self, 'path', path)
403
404     def evaluate(self, item):
405         """
406         Evaluate items which are instances of L{Reference} or L{Expression}.
407
408         L{Reference} instances are evaluated using L{Reference.resolve},
409         and L{Expression} instances are evaluated using
410         L{Expression.evaluate}.
411
412         @param item: The item to be evaluated.
413         @type item: any
414         @return: If the item is an instance of L{Reference} or L{Expression},
415         the evaluated value is returned, otherwise the item is returned
416         unchanged.
417         """
418         if isinstance(item, Reference):
419             item = item.resolve(self)
420         elif isinstance(item, Expression):
421             item = item.evaluate(self)
422         return item
423
424     def writeToStream(self, stream, indent, container):
425         """
426         Write this instance to a stream at the specified indentation level.
427
428         Should be redefined in subclasses.
429
430         @param stream: The stream to write to
431         @type stream: A writable stream (file-like object)
432         @param indent: The indentation level
433         @type indent: int
434         @param container: The container of this instance
435         @type container: L{Container}
436         @raise NotImplementedError: If a subclass does not override this
437         """
438         raise NotImplementedError
439
440     def writeValue(self, value, stream, indent):
441         if isinstance(self, Mapping):
442             indstr = ' '
443         else:
444             indstr = indent * '  '
445         if isinstance(value, Reference) or isinstance(value, Expression):
446             stream.write('%s%r%s' % (indstr, value, NEWLINE))
447         else:
448             if isinstance(value, str): # and not isWord(value):
449                 value = repr(value)
450             stream.write('%s%s%s' % (indstr, value, NEWLINE))
451
452 class Mapping(Container):
453     """
454     This internal class implements key-value mappings in configurations.
455     """
456
457     def __init__(self, parent=None):
458         """
459         Initialize an instance.
460
461         @param parent: The parent of this instance in the hierarchy.
462         @type parent: A L{Container} instance.
463         """
464         Container.__init__(self, parent)
465         object.__setattr__(self, 'path', '')
466         object.__setattr__(self, 'data', {})
467         object.__setattr__(self, 'order', [])   # to preserve ordering
468         object.__setattr__(self, 'comments', {})
469
470     def __delitem__(self, key):
471         """
472         Remove an item
473         """
474         data = object.__getattribute__(self, 'data')
475         if key not in data:
476             raise AttributeError(key)
477         order = object.__getattribute__(self, 'order')
478         comments = object.__getattribute__(self, 'comments')
479         del data[key]
480         order.remove(key)
481         del comments[key]
482
483     def __getitem__(self, key):
484         data = object.__getattribute__(self, 'data')
485         if key not in data:
486             raise AttributeError(key)
487         rv = data[key]
488         return self.evaluate(rv)
489
490     __getattr__ = __getitem__
491     
492     '''
493     def __getattribute__(self, name):
494         if name == "__dict__":
495             return {}
496         if name in ["__methods__", "__members__"]:
497             return []
498         #if name == "__class__":
499         #    return ''
500         data = object.__getattribute__(self, "data")
501         useData = data.has_key(name)
502         if useData:
503             rv = getattr(data, name)
504         else:
505             rv = object.__getattribute__(self, name)
506             if rv is None:
507                 raise AttributeError(name)
508         return rv
509     '''
510
511     def iteritems(self):
512         for key in self.keys():
513             yield(key, self[key])
514         raise StopIteration
515
516     def __contains__(self, item):
517         order = object.__getattribute__(self, 'order')
518         return item in order
519
520     def addMapping(self, key, value, comment, setting=False):
521         """
522         Add a key-value mapping with a comment.
523
524         @param key: The key for the mapping.
525         @type key: str
526         @param value: The value for the mapping.
527         @type value: any
528         @param comment: The comment for the key (can be None).
529         @type comment: str
530         @param setting: If True, ignore clashes. This is set
531         to true when called from L{__setattr__}.
532         @raise ConfigFormatError: If an existing key is seen
533         again and setting is False.
534         """
535         data = object.__getattribute__(self, 'data')
536         order = object.__getattribute__(self, 'order')
537         comments = object.__getattribute__(self, 'comments')
538
539         data[key] = value
540         if key not in order:
541             order.append(key)
542         elif not setting:
543             raise ConfigFormatError("repeated key: %s" % key)
544         comments[key] = comment
545
546     def __setattr__(self, name, value):
547         self.addMapping(name, value, None, True)
548
549     __setitem__ = __setattr__
550
551     def keys(self):
552         """
553         Return the keys in a similar way to a dictionary.
554         """
555         return object.__getattribute__(self, 'order')
556
557     def get(self, key, default=None):
558         """
559         Allows a dictionary-style get operation.
560         """
561         if key in self:
562             return self[key]
563         return default
564
565     def __str__(self):
566         return str(object.__getattribute__(self, 'data'))
567
568     def __repr__(self):
569         return repr(object.__getattribute__(self, 'data'))
570
571     def __len__(self):
572         return len(object.__getattribute__(self, 'order'))
573
574     def __iter__(self):
575         return self.iterkeys()
576
577     def iterkeys(self):
578         order = object.__getattribute__(self, 'order')
579         return order.__iter__()
580
581     def writeToStream(self, stream, indent, container):
582         """
583         Write this instance to a stream at the specified indentation level.
584
585         Should be redefined in subclasses.
586
587         @param stream: The stream to write to
588         @type stream: A writable stream (file-like object)
589         @param indent: The indentation level
590         @type indent: int
591         @param container: The container of this instance
592         @type container: L{Container}
593         """
594         indstr = indent * '  '
595         if len(self) == 0:
596             stream.write(' { }%s' % NEWLINE)
597         else:
598             if isinstance(container, Mapping):
599                 stream.write(NEWLINE)
600             stream.write('%s{%s' % (indstr, NEWLINE))
601             self.__save__(stream, indent + 1)
602             stream.write('%s}%s' % (indstr, NEWLINE))
603
604     def __save__(self, stream, indent=0):
605         """
606         Save this configuration to the specified stream.
607         @param stream: A stream to which the configuration is written.
608         @type stream: A write-only stream (file-like object).
609         @param indent: The indentation level for the output.
610         @type indent: int
611         """
612         indstr = indent * '  '
613         order = object.__getattribute__(self, 'order')
614         data = object.__getattribute__(self, 'data')
615         maxlen = 0 # max(map(lambda x: len(x), order))
616         for key in order:
617             comment = self.comments[key]
618             if isWord(key):
619                 skey = key
620             else:
621                 skey = repr(key)
622             if comment:
623                 stream.write('%s#%s' % (indstr, comment))
624             if skey.startswith("u'"):
625                 skey = skey[1:]
626             stream.write('%s%-*s :' % (indstr, maxlen, skey))
627             value = data[key]
628             if isinstance(value, Container):
629                 value.writeToStream(stream, indent, self)
630             else:
631                 self.writeValue(value, stream, indent)
632
633 class Config(Mapping):
634     """
635     This class represents a configuration, and is the only one which clients
636     need to interface to, under normal circumstances.
637     """
638
639     class Namespace(object):
640         """
641         This internal class is used for implementing default namespaces.
642
643         An instance acts as a namespace.
644         """
645         def __init__(self):
646             self.sys = sys
647             self.os = os
648
649     def __init__(self, streamOrFile=None, parent=None, PWD = None):
650         """
651         Initializes an instance.
652
653         @param streamOrFile: If specified, causes this instance to be loaded
654         from the stream (by calling L{load}). If a string is provided, it is
655         passed to L{streamOpener} to open a stream. Otherwise, the passed
656         value is assumed to be a stream and used as is.
657         @type streamOrFile: A readable stream (file-like object) or a name.
658         @param parent: If specified, this becomes the parent of this instance
659         in the configuration hierarchy.
660         @type parent: a L{Container} instance.
661         """
662         try: # Python 3 compatibility
663             if isinstance(streamOrFile, unicode):
664                 streamOrFile = streamOrFile.encode()
665         except NameError:
666             pass
667         Mapping.__init__(self, parent)
668         object.__setattr__(self, 'reader', ConfigReader(self))
669         object.__setattr__(self, 'namespaces', [Config.Namespace()])
670         if streamOrFile is not None:
671             if isinstance(streamOrFile, str) or isinstance(streamOrFile, bytes):
672                 global streamOpener
673                 if streamOpener is None:
674                     streamOpener = defaultStreamOpener
675                 streamOrFile = streamOpener(streamOrFile)
676             load = object.__getattribute__(self, "load")
677             load(streamOrFile)
678             # Specific add for salomeTools : PWD
679             if PWD:
680                 key, pwd = PWD
681                 if key == "":
682                     self.PWD = pwd
683                 else:
684                     self[key].PWD = pwd
685
686     def load(self, stream):
687         """
688         Load the configuration from the specified stream. Multiple streams can
689         be used to populate the same instance, as long as there are no
690         clashing keys. The stream is closed.
691         @param stream: A stream from which the configuration is read.
692         @type stream: A read-only stream (file-like object).
693         @raise ConfigError: if keys in the loaded configuration clash with
694         existing keys.
695         @raise ConfigFormatError: if there is a syntax error in the stream.
696         """
697         reader = object.__getattribute__(self, 'reader')
698         reader.load(stream)
699         stream.close()
700
701     def addNamespace(self, ns, name=None):
702         """
703         Add a namespace to this configuration which can be used to evaluate
704         (resolve) dotted-identifier expressions.
705         @param ns: The namespace to be added.
706         @type ns: A module or other namespace suitable for passing as an
707         argument to vars().
708         @param name: A name for the namespace, which, if specified, provides
709         an additional level of indirection.
710         @type name: str
711         """
712         namespaces = object.__getattribute__(self, 'namespaces')
713         if name is None:
714             namespaces.append(ns)
715         else:
716             setattr(namespaces[0], name, ns)
717
718     def removeNamespace(self, ns, name=None):
719         """
720         Remove a namespace added with L{addNamespace}.
721         @param ns: The namespace to be removed.
722         @param name: The name which was specified when L{addNamespace} was
723         called.
724         @type name: str
725         """
726         namespaces = object.__getattribute__(self, 'namespaces')
727         if name is None:
728             namespaces.remove(ns)
729         else:
730             delattr(namespaces[0], name)
731
732     def __save__(self, stream, indent=0, no_close=False):
733         """
734         Save this configuration to the specified stream. The stream is
735         closed if this is the top-level configuration in the hierarchy.
736         L{Mapping.__save__} is called to do all the work.
737         @param stream: A stream to which the configuration is written.
738         @type stream: A write-only stream (file-like object).
739         @param indent: The indentation level for the output.
740         @type indent: int
741         """
742         Mapping.__save__(self, stream, indent)
743         if indent == 0 and not no_close:
744             stream.close()
745
746     def getByPath(self, path):
747         """
748         Obtain a value in the configuration via its path.
749         @param path: The path of the required value
750         @type path: str
751         @return the value at the specified path.
752         @rtype: any
753         @raise ConfigError: If the path is invalid
754         """
755         s = 'self.' + path
756         try:
757             return eval(s)
758         except Exception as e:
759             raise ConfigError(str(e))
760
761 class Sequence(Container):
762     """
763     This internal class implements a value which is a sequence of other values.
764     """
765     class SeqIter(object):
766         """
767         This internal class implements an iterator for a L{Sequence} instance.
768         """
769         def __init__(self, seq):
770             self.seq = seq
771             self.limit = len(object.__getattribute__(seq, 'data'))
772             self.index = 0
773
774         def __iter__(self):
775             return self
776
777         def next(self):
778             if self.index >= self.limit:
779                 raise StopIteration
780             rv = self.seq[self.index]
781             self.index += 1
782             return rv
783         
784         # This method is for python3 compatibility
785         def __next__(self): 
786             if self.index >= self.limit:
787                 raise StopIteration
788             rv = self.seq[self.index]
789             self.index += 1
790             return rv
791
792     def __init__(self, parent=None):
793         """
794         Initialize an instance.
795
796         @param parent: The parent of this instance in the hierarchy.
797         @type parent: A L{Container} instance.
798         """
799         Container.__init__(self, parent)
800         object.__setattr__(self, 'data', [])
801         object.__setattr__(self, 'comments', [])
802
803     def append(self, item, comment):
804         """
805         Add an item to the sequence.
806
807         @param item: The item to add.
808         @type item: any
809         @param comment: A comment for the item.
810         @type comment: str
811         """
812         data = object.__getattribute__(self, 'data')
813         comments = object.__getattribute__(self, 'comments')
814         data.append(item)
815         comments.append(comment)
816
817     def __getitem__(self, index):
818         data = object.__getattribute__(self, 'data')
819         try:
820             rv = data[index]
821         except (IndexError, KeyError, TypeError):
822             raise ConfigResolutionError('%r is not a valid index for %r' % (index, object.__getattribute__(self, 'path')))
823         if not isinstance(rv, list):
824             rv = self.evaluate(rv)
825         else:
826             # deal with a slice
827             result = []
828             for a in rv:
829                 result.append(self.evaluate(a))
830             rv = result
831         return rv
832
833     def __iter__(self):
834         return Sequence.SeqIter(self)
835
836     def __repr__(self):
837         return repr(object.__getattribute__(self, 'data'))
838
839     def __str__(self):
840         return str(self[:]) # using the slice evaluates the contents
841
842     def __len__(self):
843         return len(object.__getattribute__(self, 'data'))
844
845     def writeToStream(self, stream, indent, container):
846         """
847         Write this instance to a stream at the specified indentation level.
848
849         Should be redefined in subclasses.
850
851         @param stream: The stream to write to
852         @type stream: A writable stream (file-like object)
853         @param indent: The indentation level
854         @type indent: int
855         @param container: The container of this instance
856         @type container: L{Container}
857         """
858         indstr = indent * '  '
859         if len(self) == 0:
860             stream.write(' [ ]%s' % NEWLINE)
861         else:
862             if isinstance(container, Mapping):
863                 stream.write(NEWLINE)
864             stream.write('%s[%s' % (indstr, NEWLINE))
865             self.__save__(stream, indent + 1)
866             stream.write('%s]%s' % (indstr, NEWLINE))
867
868     def __save__(self, stream, indent):
869         """
870         Save this instance to the specified stream.
871         @param stream: A stream to which the configuration is written.
872         @type stream: A write-only stream (file-like object).
873         @param indent: The indentation level for the output, > 0
874         @type indent: int
875         """
876         if indent == 0:
877             raise ConfigError("sequence cannot be saved as a top-level item")
878         data = object.__getattribute__(self, 'data')
879         comments = object.__getattribute__(self, 'comments')
880         indstr = indent * '  '
881         for i in range(0, len(data)):
882             value = data[i]
883             comment = comments[i]
884             if comment:
885                 stream.write('%s#%s' % (indstr, comment))
886             if isinstance(value, Container):
887                 value.writeToStream(stream, indent, self)
888             else:
889                 self.writeValue(value, stream, indent)
890
891 class Reference(object):
892     """
893     This internal class implements a value which is a reference to another value.
894     """
895     def __init__(self, config, type, ident):
896         """
897         Initialize an instance.
898
899         @param config: The configuration which contains this reference.
900         @type config: A L{Config} instance.
901         @param type: The type of reference.
902         @type type: BACKTICK or DOLLAR
903         @param ident: The identifier which starts the reference.
904         @type ident: str
905         """
906         self.config = config
907         self.type = type
908         self.elements = [ident]
909
910     def addElement(self, type, ident):
911         """
912         Add an element to the reference.
913
914         @param type: The type of reference.
915         @type type: BACKTICK or DOLLAR
916         @param ident: The identifier which continues the reference.
917         @type ident: str
918         """
919         self.elements.append((type, ident))
920
921     def findConfig(self, container):
922         """
923         Find the closest enclosing configuration to the specified container.
924
925         @param container: The container to start from.
926         @type container: L{Container}
927         @return: The closest enclosing configuration, or None.
928         @rtype: L{Config}
929         """
930         while (container is not None) and not isinstance(container, Config):
931             container = object.__getattribute__(container, 'parent')
932         return container
933
934     def resolve(self, container):
935         """
936         Resolve this instance in the context of a container.
937
938         @param container: The container to resolve from.
939         @type container: L{Container}
940         @return: The resolved value.
941         @rtype: any
942         @raise ConfigResolutionError: If resolution fails.
943         """
944         rv = None
945         path = object.__getattribute__(container, 'path')
946         current = container
947         while current is not None:
948             if self.type == BACKTICK:
949                 namespaces = object.__getattribute__(current, 'namespaces')
950                 found = False
951                 for ns in namespaces:
952                     try:
953                         rv = eval(str(self)[1:-1], vars(ns))
954                         found = True
955                         break
956                     except:
957                         pass
958                 if found:
959                     break
960             else:
961                 key = self.elements[0]
962                 try:
963                     rv = current[key]
964                     for item in self.elements[1:]:
965                         key = item[1]
966                         rv = rv[key]
967                     break
968                 except:
969                     rv = None
970                     pass
971             current = object.__getattribute__(current, 'parent')
972         if current is None:
973             raise ConfigResolutionError("unable to evaluate %r in the configuration %s" % (self, path))
974         return rv
975
976     def __str__(self):
977         s = self.elements[0]
978         for tt, tv in self.elements[1:]:
979             if tt == DOT:
980                 s += '.%s' % tv
981             else:
982                 s += '[%r]' % tv
983         if self.type == BACKTICK:
984             return BACKTICK + s + BACKTICK
985         else:
986             return DOLLAR + s
987
988     def __repr__(self):
989         return self.__str__()
990
991 class Expression(object):
992     """
993     This internal class implements a value which is obtained by evaluating an expression.
994     """
995     def __init__(self, op, lhs, rhs):
996         """
997         Initialize an instance.
998
999         @param op: the operation expressed in the expression.
1000         @type op: PLUS, MINUS, STAR, SLASH, MOD
1001         @param lhs: the left-hand-side operand of the expression.
1002         @type lhs: any Expression or primary value.
1003         @param rhs: the right-hand-side operand of the expression.
1004         @type rhs: any Expression or primary value.
1005         """
1006         self.op = op
1007         self.lhs = lhs
1008         self.rhs = rhs
1009
1010     def __str__(self):
1011         return '%r %s %r' % (self.lhs, self.op, self.rhs)
1012
1013     def __repr__(self):
1014         return self.__str__()
1015
1016     def evaluate(self, container):
1017         """
1018         Evaluate this instance in the context of a container.
1019
1020         @param container: The container to evaluate in from.
1021         @type container: L{Container}
1022         @return: The evaluated value.
1023         @rtype: any
1024         @raise ConfigResolutionError: If evaluation fails.
1025         @raise ZeroDivideError: If division by zero occurs.
1026         @raise TypeError: If the operation is invalid, e.g.
1027         subtracting one string from another.
1028         """
1029         lhs = self.lhs
1030         if isinstance(lhs, Reference):
1031             lhs = lhs.resolve(container)
1032         elif isinstance(lhs, Expression):
1033             lhs = lhs.evaluate(container)
1034         rhs = self.rhs
1035         if isinstance(rhs, Reference):
1036             rhs = rhs.resolve(container)
1037         elif isinstance(rhs, Expression):
1038             rhs = rhs.evaluate(container)
1039         op = self.op
1040         if op == PLUS:
1041             rv = lhs + rhs
1042         elif op == MINUS:
1043             rv = lhs - rhs
1044         elif op == STAR:
1045             rv = lhs * rhs
1046         elif op == SLASH:
1047             rv = lhs / rhs
1048         else:
1049             rv = lhs % rhs
1050         return rv
1051
1052 class ConfigReader(object):
1053     """
1054     This internal class implements a parser for configurations.
1055     """
1056
1057     def __init__(self, config):
1058         self.filename = None
1059         self.config = config
1060         self.lineno = 0
1061         self.colno = 0
1062         self.lastc = None
1063         self.last_token = None
1064         self.commentchars = '#'
1065         self.whitespace = ' \t\r\n'
1066         self.quotes = '\'"'
1067         self.punct = ':-+*/%,.{}[]()@`$'
1068         self.digits = '0123456789'
1069         self.wordchars = '%s' % WORDCHARS # make a copy
1070         self.identchars = self.wordchars + self.digits
1071         self.pbchars = []
1072         self.pbtokens = []
1073         self.comment = None
1074
1075     def location(self):
1076         """
1077         Return the current location (filename, line, column) in the stream
1078         as a string.
1079
1080         Used when printing error messages,
1081
1082         @return: A string representing a location in the stream being read.
1083         @rtype: str
1084         """
1085         return "%s(%d,%d)" % (self.filename, self.lineno, self.colno)
1086
1087     def getChar(self):
1088         """
1089         Get the next char from the stream. Update line and column numbers
1090         appropriately.
1091
1092         @return: The next character from the stream.
1093         @rtype: str
1094         """
1095         if self.pbchars:
1096             c = self.pbchars.pop()
1097             if isinstance(c,bytes):
1098                 c = c.decode()
1099         else:
1100             c = self.stream.read(1)
1101             if isinstance(c,bytes):
1102                 c = c.decode()
1103             self.colno += 1
1104             if c == '\n':
1105                 self.lineno += 1
1106                 self.colno = 1
1107         return c
1108
1109     def __repr__(self):
1110         return "<ConfigReader at 0x%08x>" % id(self)
1111
1112     __str__ = __repr__
1113
1114     def getToken(self):
1115         """
1116         Get a token from the stream. String values are returned in a form
1117         where you need to eval() the returned value to get the actual
1118         string. The return value is (token_type, token_value).
1119
1120         Multiline string tokenizing is thanks to David Janes (BlogMatrix)
1121
1122         @return: The next token.
1123         @rtype: A token tuple.
1124         """
1125         if self.pbtokens:
1126             return self.pbtokens.pop()
1127         stream = self.stream
1128         self.comment = None
1129         token = ''
1130         tt = EOF
1131         while True:
1132             c = self.getChar()
1133             if not c:
1134                 break
1135             elif c == '#':
1136                 if self.comment :
1137                     self.comment += '#' + stream.readline()
1138                 else :
1139                     self.comment = stream.readline()
1140                 self.lineno += 1
1141                 continue
1142             if c in self.quotes:
1143                 token = c
1144                 quote = c
1145                 tt = STRING
1146                 escaped = False
1147                 multiline = False
1148                 c1 = self.getChar()
1149                 if c1 == quote:
1150                     c2 = self.getChar()
1151                     if c2 == quote:
1152                         multiline = True
1153                         token += quote
1154                         token += quote
1155                     else:
1156                         self.pbchars.append(c2)
1157                         self.pbchars.append(c1)
1158                 else:
1159                     self.pbchars.append(c1)
1160                 while True:
1161                     c = self.getChar()
1162                     if not c:
1163                         break
1164                     token += c
1165                     if (c == quote) and not escaped:
1166                         if not multiline or (len(token) >= 6 and token.endswith(token[:3]) and token[-4] != '\\'):
1167                             break
1168                     if c == '\\':
1169                         escaped = not escaped
1170                     else:
1171                         escaped = False
1172                 if not c:
1173                     raise ConfigFormatError('%s: Unterminated quoted string: %r, %r' % (self.location(), token, c))
1174                 break
1175             if c in self.whitespace:
1176                 self.lastc = c
1177                 continue
1178             elif c in self.punct:
1179                 token = c
1180                 tt = c
1181                 if (self.lastc == ']') or (self.lastc in self.identchars):
1182                     if c == '[':
1183                         tt = LBRACK2
1184                     elif c == '(':
1185                         tt = LPAREN2
1186                 break
1187             elif c in self.digits:
1188                 token = c
1189                 tt = NUMBER
1190                 while True:
1191                     c = self.getChar()
1192                     if not c:
1193                         break
1194                     if c in self.digits:
1195                         token += c
1196                     elif (c == '.') and token.find('.') < 0:
1197                         token += c
1198                     else:
1199                         if c and (c not in self.whitespace):
1200                             self.pbchars.append(c)
1201                         break
1202                 break
1203             elif c in self.wordchars:
1204                 token = c
1205                 tt = WORD
1206                 c = self.getChar()
1207                 while c and (c in self.identchars):
1208                     token += c
1209                     c = self.getChar()
1210                 if c: # and c not in self.whitespace:
1211                     self.pbchars.append(c)
1212                 if token == "True":
1213                     tt = TRUE
1214                 elif token == "False":
1215                     tt = FALSE
1216                 elif token == "None":
1217                     tt = NONE
1218                 break
1219             else:
1220                 raise ConfigFormatError('%s: Unexpected character: %r' % (self.location(), c))
1221         if token:
1222             self.lastc = token[-1]
1223         else:
1224             self.lastc = None
1225         self.last_token = tt
1226         
1227         # Python 2.x specific unicode conversion
1228         if sys.version_info[0] == 2 and tt == WORD and isinstance(token, unicode):
1229             token = token.encode('ascii')
1230         return (tt, token)
1231
1232     def load(self, stream, parent=None, suffix=None):
1233         """
1234         Load the configuration from the specified stream.
1235
1236         @param stream: A stream from which to load the configuration.
1237         @type stream: A stream (file-like object).
1238         @param parent: The parent of the configuration (to which this reader
1239         belongs) in the hierarchy. Specified when the configuration is
1240         included in another one.
1241         @type parent: A L{Container} instance.
1242         @param suffix: The suffix of this configuration in the parent
1243         configuration. Should be specified whenever the parent is not None.
1244         @raise ConfigError: If parent is specified but suffix is not.
1245         @raise ConfigFormatError: If there are syntax errors in the stream.
1246         """
1247         if parent is not None:
1248             if suffix is None:
1249                 raise ConfigError("internal error: load called with parent but no suffix")
1250             self.config.setPath(makePath(object.__getattribute__(parent, 'path'), suffix))
1251         self.setStream(stream)
1252         self.token = self.getToken()
1253         self.parseMappingBody(self.config)
1254         if self.token[0] != EOF:
1255             raise ConfigFormatError('%s: expecting EOF, found %r' % (self.location(), self.token[1]))
1256
1257     def setStream(self, stream):
1258         """
1259         Set the stream to the specified value, and prepare to read from it.
1260
1261         @param stream: A stream from which to load the configuration.
1262         @type stream: A stream (file-like object).
1263         """
1264         self.stream = stream
1265         if hasattr(stream, 'name'):
1266             filename = stream.name
1267         else:
1268             filename = '?'
1269         self.filename = filename
1270         self.lineno = 1
1271         self.colno = 1
1272
1273     def match(self, t):
1274         """
1275         Ensure that the current token type matches the specified value, and
1276         advance to the next token.
1277
1278         @param t: The token type to match.
1279         @type t: A valid token type.
1280         @return: The token which was last read from the stream before this
1281         function is called.
1282         @rtype: a token tuple - see L{getToken}.
1283         @raise ConfigFormatError: If the token does not match what's expected.
1284         """
1285         if self.token[0] != t:
1286             raise ConfigFormatError("%s: expecting %s, found %r" % (self.location(), t, self.token[1]))
1287         rv = self.token
1288         self.token = self.getToken()
1289         return rv
1290
1291     def parseMappingBody(self, parent):
1292         """
1293         Parse the internals of a mapping, and add entries to the provided
1294         L{Mapping}.
1295
1296         @param parent: The mapping to add entries to.
1297         @type parent: A L{Mapping} instance.
1298         """
1299         while self.token[0] in [WORD, STRING]:
1300             self.parseKeyValuePair(parent)
1301
1302     def parseKeyValuePair(self, parent):
1303         """
1304         Parse a key-value pair, and add it to the provided L{Mapping}.
1305
1306         @param parent: The mapping to add entries to.
1307         @type parent: A L{Mapping} instance.
1308         @raise ConfigFormatError: if a syntax error is found.
1309         """
1310         comment = self.comment
1311         tt, tv = self.token
1312         if tt == WORD:
1313             key = tv
1314             suffix = tv
1315         elif tt == STRING:
1316             key = eval(tv)
1317             suffix = '[%s]' % tv
1318         else:
1319             msg = "%s: expecting word or string, found %r"
1320             raise ConfigFormatError(msg % (self.location(), tv))
1321         self.token = self.getToken()
1322         # for now, we allow key on its own as a short form of key : True
1323         if self.token[0] == COLON:
1324             self.token = self.getToken()
1325             value = self.parseValue(parent, suffix)
1326         else:
1327             value = True
1328         try:
1329             parent.addMapping(key, value, comment)
1330         except Exception as e:
1331             raise ConfigFormatError("%s: %s, %r" % (self.location(), e,
1332                                     self.token[1]))
1333         tt = self.token[0]
1334         if tt not in [EOF, WORD, STRING, RCURLY, COMMA]:
1335             msg = "%s: expecting one of EOF, WORD, STRING, \
1336 RCURLY, COMMA, found %r"
1337             raise ConfigFormatError(msg  % (self.location(), self.token[1]))
1338         if tt == COMMA:
1339             self.token = self.getToken()
1340
1341     def parseValue(self, parent, suffix):
1342         """
1343         Parse a value.
1344
1345         @param parent: The container to which the value will be added.
1346         @type parent: A L{Container} instance.
1347         @param suffix: The suffix for the value.
1348         @type suffix: str
1349         @return: The value
1350         @rtype: any
1351         @raise ConfigFormatError: if a syntax error is found.
1352         """
1353         tt = self.token[0]
1354         if tt in [STRING, WORD, NUMBER, LPAREN, DOLLAR,
1355                   TRUE, FALSE, NONE, BACKTICK, MINUS]:
1356             rv = self.parseScalar()
1357         elif tt == LBRACK:
1358             rv = self.parseSequence(parent, suffix)
1359         elif tt in [LCURLY, AT]:
1360             rv = self.parseMapping(parent, suffix)
1361         else:
1362             raise ConfigFormatError("%s: unexpected input: %r" %
1363                (self.location(), self.token[1]))
1364         return rv
1365
1366     def parseSequence(self, parent, suffix):
1367         """
1368         Parse a sequence.
1369
1370         @param parent: The container to which the sequence will be added.
1371         @type parent: A L{Container} instance.
1372         @param suffix: The suffix for the value.
1373         @type suffix: str
1374         @return: a L{Sequence} instance representing the sequence.
1375         @rtype: L{Sequence}
1376         @raise ConfigFormatError: if a syntax error is found.
1377         """
1378         rv = Sequence(parent)
1379         rv.setPath(makePath(object.__getattribute__(parent, 'path'), suffix))
1380         self.match(LBRACK)
1381         comment = self.comment
1382         tt = self.token[0]
1383         while tt in [STRING, WORD, NUMBER, LCURLY, LBRACK, LPAREN, DOLLAR,
1384                      TRUE, FALSE, NONE, BACKTICK]:
1385             suffix = '[%d]' % len(rv)
1386             value = self.parseValue(parent, suffix)
1387             rv.append(value, comment)
1388             tt = self.token[0]
1389             comment = self.comment
1390             if tt == COMMA:
1391                 self.match(COMMA)
1392                 tt = self.token[0]
1393                 comment = self.comment
1394                 continue
1395         self.match(RBRACK)
1396         return rv
1397
1398     def parseMapping(self, parent, suffix):
1399         """
1400         Parse a mapping.
1401
1402         @param parent: The container to which the mapping will be added.
1403         @type parent: A L{Container} instance.
1404         @param suffix: The suffix for the value.
1405         @type suffix: str
1406         @return: a L{Mapping} instance representing the mapping.
1407         @rtype: L{Mapping}
1408         @raise ConfigFormatError: if a syntax error is found.
1409         """
1410         if self.token[0] == LCURLY:
1411             self.match(LCURLY)
1412             rv = Mapping(parent)
1413             rv.setPath(
1414                makePath(object.__getattribute__(parent, 'path'), suffix))
1415             self.parseMappingBody(rv)
1416             self.match(RCURLY)
1417         else:
1418             self.match(AT)
1419             _, fn = self.match(STRING)
1420             rv = Config(eval(fn), parent)
1421         return rv
1422
1423     def parseScalar(self):
1424         """
1425         Parse a scalar - a terminal value such as a string or number, or
1426         an L{Expression} or L{Reference}.
1427
1428         @return: the parsed scalar
1429         @rtype: any scalar
1430         @raise ConfigFormatError: if a syntax error is found.
1431         """
1432         lhs = self.parseTerm()
1433         tt = self.token[0]
1434         while tt in [PLUS, MINUS]:
1435             self.match(tt)
1436             rhs = self.parseTerm()
1437             lhs = Expression(tt, lhs, rhs)
1438             tt = self.token[0]
1439         return lhs
1440
1441     def parseTerm(self):
1442         """
1443         Parse a term in an additive expression (a + b, a - b)
1444
1445         @return: the parsed term
1446         @rtype: any scalar
1447         @raise ConfigFormatError: if a syntax error is found.
1448         """
1449         lhs = self.parseFactor()
1450         tt = self.token[0]
1451         while tt in [STAR, SLASH, MOD]:
1452             self.match(tt)
1453             rhs = self.parseFactor()
1454             lhs = Expression(tt, lhs, rhs)
1455             tt = self.token[0]
1456         return lhs
1457
1458     def parseFactor(self):
1459         """
1460         Parse a factor in an multiplicative expression (a * b, a / b, a % b)
1461
1462         @return: the parsed factor
1463         @rtype: any scalar
1464         @raise ConfigFormatError: if a syntax error is found.
1465         """
1466         tt = self.token[0]
1467         if tt in [NUMBER, WORD, STRING, TRUE, FALSE, NONE]:
1468             rv = self.token[1]
1469             if tt != WORD:
1470                 rv = eval(rv)
1471             self.match(tt)
1472         elif tt == LPAREN:
1473             self.match(LPAREN)
1474             rv = self.parseScalar()
1475             self.match(RPAREN)
1476         elif tt == DOLLAR:
1477             self.match(DOLLAR)
1478             rv = self.parseReference(DOLLAR)
1479         elif tt == BACKTICK:
1480             self.match(BACKTICK)
1481             rv = self.parseReference(BACKTICK)
1482             self.match(BACKTICK)
1483         elif tt == MINUS:
1484             self.match(MINUS)
1485             rv = -self.parseScalar()
1486         else:
1487             raise ConfigFormatError("%s: unexpected input: %r" %
1488                (self.location(), self.token[1]))
1489         return rv
1490
1491     def parseReference(self, type):
1492         """
1493         Parse a reference.
1494
1495         @return: the parsed reference
1496         @rtype: L{Reference}
1497         @raise ConfigFormatError: if a syntax error is found.
1498         """
1499         word = self.match(WORD)
1500         rv = Reference(self.config, type, word[1])
1501         while self.token[0] in [DOT, LBRACK2]:
1502             self.parseSuffix(rv)
1503         return rv
1504
1505     def parseSuffix(self, ref):
1506         """
1507         Parse a reference suffix.
1508
1509         @param ref: The reference of which this suffix is a part.
1510         @type ref: L{Reference}.
1511         @raise ConfigFormatError: if a syntax error is found.
1512         """
1513         tt = self.token[0]
1514         if tt == DOT:
1515             self.match(DOT)
1516             word = self.match(WORD)
1517             ref.addElement(DOT, word[1])
1518         else:
1519             self.match(LBRACK2)
1520             tt, tv = self.token
1521             if tt not in [NUMBER, STRING]:
1522                 raise ConfigFormatError("%s: expected number or string, found %r" % (self.location(), tv))
1523             self.token = self.getToken()
1524             tv = eval(tv)
1525             self.match(RBRACK)
1526             ref.addElement(LBRACK, tv)
1527
1528 def defaultMergeResolve(map1, map2, key):
1529     """
1530     A default resolver for merge conflicts. Returns a string
1531     indicating what action to take to resolve the conflict.
1532
1533     @param map1: The map being merged into.
1534     @type map1: L{Mapping}.
1535     @param map2: The map being used as the merge operand.
1536     @type map2: L{Mapping}.
1537     @param key: The key in map2 (which also exists in map1).
1538     @type key: str
1539     @return: One of "merge", "append", "mismatch" or "overwrite"
1540              indicating what action should be taken. This should
1541              be appropriate to the objects being merged - e.g.
1542              there is no point returning "merge" if the two objects
1543              are instances of L{Sequence}.
1544     @rtype: str
1545     """
1546     obj1 = map1[key]
1547     obj2 = map2[key]
1548     if isinstance(obj1, Mapping) and isinstance(obj2, Mapping):
1549         rv = "merge"
1550     elif isinstance(obj1, Sequence) and isinstance(obj2, Sequence):
1551         rv = "append"
1552     else:
1553         rv = "mismatch"
1554     return rv
1555
1556 def overwriteMergeResolve(map1, map2, key):
1557     """
1558     An overwriting resolver for merge conflicts. Calls L{defaultMergeResolve},
1559     but where a "mismatch" is detected, returns "overwrite" instead.
1560
1561     @param map1: The map being merged into.
1562     @type map1: L{Mapping}.
1563     @param map2: The map being used as the merge operand.
1564     @type map2: L{Mapping}.
1565     @param key: The key in map2 (which also exists in map1).
1566     @type key: str
1567     """
1568     rv = defaultMergeResolve(map1, map2, key)
1569     if rv == "mismatch":
1570         rv = "overwrite"
1571     return rv
1572
1573 def deepCopyMapping(inMapping):
1574     res = Mapping()
1575     for element in inMapping:
1576         res[element] = inMapping[element]
1577     return res
1578
1579 class ConfigMerger(object):
1580     """
1581     This class is used for merging two configurations. If a key exists in the
1582     merge operand but not the merge target, then the entry is copied from the
1583     merge operand to the merge target. If a key exists in both configurations,
1584     then a resolver (a callable) is called to decide how to handle the
1585     conflict.
1586     """
1587
1588     def __init__(self, resolver=defaultMergeResolve):
1589         """
1590         Initialise an instance.
1591
1592         @param resolver:
1593         @type resolver: A callable which takes the argument list
1594         (map1, map2, key) where map1 is the mapping being merged into,
1595         map2 is the merge operand and key is the clashing key. The callable
1596         should return a string indicating how the conflict should be resolved.
1597         For possible return values, see L{defaultMergeResolve}. The default
1598         value preserves the old behaviour
1599         """
1600         self.resolver = resolver
1601
1602     def merge(self, merged, mergee):
1603         """
1604         Merge two configurations. The second configuration is unchanged,
1605         and the first is changed to reflect the results of the merge.
1606
1607         @param merged: The configuration to merge into.
1608         @type merged: L{Config}.
1609         @param mergee: The configuration to merge.
1610         @type mergee: L{Config}.
1611         """
1612         self.mergeMapping(merged, mergee)
1613
1614     def overwriteKeys(self, map1, seq2):
1615         """
1616         Renint variables. The second mapping is unchanged,
1617         and the first is changed depending the keys of the second mapping.
1618         @param map1: The mapping to reinit keys into.
1619         @type map1: L{Mapping}.
1620         @param map2: The mapping container reinit information.
1621         @type map2: L{Mapping}.
1622         """
1623
1624         overwrite_list = object.__getattribute__(seq2, 'data')
1625         for overwrite_instruction in overwrite_list:
1626             object.__setattr__(overwrite_instruction, 'parent', map1)
1627             if "__condition__" in overwrite_instruction.keys():
1628                 overwrite_condition = overwrite_instruction["__condition__"]
1629                 if eval(overwrite_condition, globals(), map1):
1630                     for key in overwrite_instruction.keys():
1631                         if key == "__condition__":
1632                             continue
1633                         try:
1634                             exec( 'map1.' + key + " = " + repr(overwrite_instruction[key]))
1635                         except:
1636                             exec('map1.' + key + " = " + str(overwrite_instruction[key]))
1637             else:
1638                 for key in overwrite_instruction.keys():
1639                     try:
1640                         exec('map1.' + key + " = " + repr(overwrite_instruction[key]))
1641                     except:
1642                         exec('map1.' + key + " = " + str(overwrite_instruction[key]))
1643
1644     def mergeMapping(self, map1, map2):
1645         """
1646         Merge two mappings recursively. The second mapping is unchanged,
1647         and the first is changed to reflect the results of the merge.
1648
1649         @param map1: The mapping to merge into.
1650         @type map1: L{Mapping}.
1651         @param map2: The mapping to merge.
1652         @type map2: L{Mapping}.
1653         """
1654         keys = map1.keys()
1655         global __resolveOverwrite__
1656         for key in map2.keys():
1657             if __resolveOverwrite__ and key == "__overwrite__":
1658                 self.overwriteKeys(map1,map2[key])
1659
1660             elif key not in keys:
1661                 map1[key] = map2[key]
1662                 if isinstance(map1[key], Container) :
1663                     object.__setattr__(map1[key], 'parent', map1)
1664             else:
1665                 obj1 = map1[key]
1666                 obj2 = map2[key]
1667                 decision = self.resolver(map1, map2, key)
1668                 if decision == "merge":
1669                     self.mergeMapping(obj1, obj2)
1670                 elif decision == "append":
1671                     self.mergeSequence(obj1, obj2)
1672                 elif decision == "overwrite":
1673                     map1[key] = obj2
1674                     if isinstance(map1[key], Container):
1675                         object.__setattr__(map1[key], 'parent', map1)
1676                 elif decision == "mismatch":
1677                     self.handleMismatch(obj1, obj2)
1678                 else:
1679                     msg = "unable to merge: don't know how to implement %r"
1680                     raise ValueError(msg % decision)
1681
1682     def mergeSequence(self, seq1, seq2):
1683         """
1684         Merge two sequences. The second sequence is unchanged,
1685         and the first is changed to have the elements of the second
1686         appended to it.
1687
1688         @param seq1: The sequence to merge into.
1689         @type seq1: L{Sequence}.
1690         @param seq2: The sequence to merge.
1691         @type seq2: L{Sequence}.
1692         """
1693         data1 = object.__getattribute__(seq1, 'data')
1694         data2 = object.__getattribute__(seq2, 'data')
1695         for obj in data2:
1696             data1.append(obj)
1697         comment1 = object.__getattribute__(seq1, 'comments')
1698         comment2 = object.__getattribute__(seq2, 'comments')
1699         for obj in comment2:
1700             comment1.append(obj)
1701
1702     def handleMismatch(self, obj1, obj2):
1703         """
1704         Handle a mismatch between two objects.
1705
1706         @param obj1: The object to merge into.
1707         @type obj1: any
1708         @param obj2: The object to merge.
1709         @type obj2: any
1710         """
1711         raise ConfigError("unable to merge %r with %r" % (obj1, obj2))
1712
1713 class ConfigList(list):
1714     """
1715     This class implements an ordered list of configurations and allows you
1716     to try getting the configuration from each entry in turn, returning
1717     the first successfully obtained value.
1718     """
1719
1720     def getByPath(self, path):
1721         """
1722         Obtain a value from the first configuration in the list which defines
1723         it.
1724
1725         @param path: The path of the value to retrieve.
1726         @type path: str
1727         @return: The value from the earliest configuration in the list which
1728         defines it.
1729         @rtype: any
1730         @raise ConfigError: If no configuration in the list has an entry with
1731         the specified path.
1732         """
1733         found = False
1734         rv = None
1735         for entry in self:
1736             try:
1737                 rv = entry.getByPath(path)
1738                 found = True
1739                 break
1740             except ConfigError:
1741                 pass
1742         if not found:
1743             raise ConfigError("unable to resolve %r" % path)
1744         return rv