Salome HOME
gestion de path multiples dans la configuration projet
[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     makePath('', 'suffix') -> 'suffix'
351     makePath('prefix', 'suffix') -> 'prefix.suffix'
352     makePath('prefix', '[1]') -> 'prefix[1]'
353
354     @param prefix: The prefix to use. If it evaluates as false, the suffix is returned.
355     @type prefix: str
356     @param suffix: The suffix to use. It is either an identifier or an index in brackets.
357     @type suffix: str
358     @return: The path concatenation of prefix and suffix, with adot if the suffix is not a bracketed index.
359     @rtype: str
360     """
361     if not prefix:
362         rv = suffix
363     elif suffix[0] == '[':
364         rv = prefix + suffix
365     else:
366         rv = prefix + '.' + suffix
367     return rv
368
369
370 class Container(object):
371     """
372     This internal class is the base class for mappings and sequences.
373
374     @ivar path: A string which describes how to get
375     to this instance from the root of the hierarchy.
376
377     Example::
378
379         a.list.of[1].or['more'].elements
380     """
381     def __init__(self, parent):
382         """
383         Initialize an instance.
384
385         @param parent: The parent of this instance in the hierarchy.
386         @type parent: A L{Container} instance.
387         """
388         object.__setattr__(self, 'parent', parent)
389
390     def setPath(self, path):
391         """
392         Set the path for this instance.
393         @param path: The path - a string which describes how to get
394         to this instance from the root of the hierarchy.
395         @type path: str
396         """
397         object.__setattr__(self, 'path', path)
398
399     def evaluate(self, item):
400         """
401         Evaluate items which are instances of L{Reference} or L{Expression}.
402
403         L{Reference} instances are evaluated using L{Reference.resolve},
404         and L{Expression} instances are evaluated using
405         L{Expression.evaluate}.
406
407         @param item: The item to be evaluated.
408         @type item: any
409         @return: If the item is an instance of L{Reference} or L{Expression},
410         the evaluated value is returned, otherwise the item is returned
411         unchanged.
412         """
413         if isinstance(item, Reference):
414             item = item.resolve(self)
415         elif isinstance(item, Expression):
416             item = item.evaluate(self)
417         return item
418
419     def writeToStream(self, stream, indent, container, evaluated=False):
420         """
421         Write this instance to a stream at the specified indentation level.
422
423         Should be redefined in subclasses.
424
425         @param stream: The stream to write to
426         @type stream: A writable stream (file-like object)
427         @param indent: The indentation level
428         @type indent: int
429         @param container: The container of this instance
430         @type container: L{Container}
431         @raise NotImplementedError: If a subclass does not override this
432         """
433         raise NotImplementedError
434
435     def writeValue(self, value, stream, indent, evaluated=False):
436         if isinstance(self, Mapping):
437             indstr = ' '
438         else:
439             indstr = indent * '  '
440         if isinstance(value, Reference) or isinstance(value, Expression):
441             if not evaluated:
442                 stream.write('%s%r%s' % (indstr, value, NEWLINE))
443             else:
444                 stream.write('%s%r%s' % (indstr, self.evaluate(value), NEWLINE))
445         else:
446             if isinstance(value, str): # and not isWord(value):
447                 value = repr(value)
448             stream.write('%s%s%s' % (indstr, value, NEWLINE))
449
450 class Mapping(Container):
451     """
452     This internal class implements key-value mappings in configurations.
453     """
454
455     def __init__(self, parent=None):
456         """
457         Initialize an instance.
458
459         @param parent: The parent of this instance in the hierarchy.
460         @type parent: A L{Container} instance.
461         """
462         Container.__init__(self, parent)
463         object.__setattr__(self, 'path', '')
464         object.__setattr__(self, 'data', {})
465         object.__setattr__(self, 'order', [])   # to preserve ordering
466         object.__setattr__(self, 'comments', {})
467
468     def __delitem__(self, key):
469         """
470         Remove an item
471         """
472         data = object.__getattribute__(self, 'data')
473         if key not in data:
474             raise AttributeError(key)
475         order = object.__getattribute__(self, 'order')
476         comments = object.__getattribute__(self, 'comments')
477         del data[key]
478         order.remove(key)
479         del comments[key]
480
481     def __getitem__(self, key):
482         data = object.__getattribute__(self, 'data')
483         if key not in data:
484             raise AttributeError("Unknown pyconf key: '%s'" % key)
485         rv = data[key]
486         return self.evaluate(rv)
487
488     __getattr__ = __getitem__
489     
490     '''
491     def __getattribute__(self, name):
492         if name == "__dict__":
493             return {}
494         if name in ["__methods__", "__members__"]:
495             return []
496         #if name == "__class__":
497         #    return ''
498         data = object.__getattribute__(self, "data")
499         useData = name in data
500         if useData:
501             rv = getattr(data, name)
502         else:
503             rv = object.__getattribute__(self, name)
504             if rv is None:
505                 raise AttributeError(name)
506         return rv
507     '''
508
509     def iteritems(self):
510         for key in self.keys():
511             yield(key, self[key])
512         raise StopIteration
513
514     def __contains__(self, item):
515         order = object.__getattribute__(self, 'order')
516         return item in order
517
518     def addMapping(self, key, value, comment, setting=False):
519         """
520         Add a key-value mapping with a comment.
521
522         @param key: The key for the mapping.
523         @type key: str
524         @param value: The value for the mapping.
525         @type value: any
526         @param comment: The comment for the key (can be None).
527         @type comment: str
528         @param setting: If True, ignore clashes. This is set
529         to true when called from L{__setattr__}.
530         @raise ConfigFormatError: If an existing key is seen
531         again and setting is False.
532         """
533         data = object.__getattribute__(self, 'data')
534         order = object.__getattribute__(self, 'order')
535         comments = object.__getattribute__(self, 'comments')
536
537         data[key] = value
538         if key not in order:
539             order.append(key)
540         elif not setting:
541             raise ConfigFormatError("repeated key: %s" % key)
542         comments[key] = comment
543
544     def __setattr__(self, name, value):
545         self.addMapping(name, value, None, True)
546
547     __setitem__ = __setattr__
548
549     def keys(self):
550         """
551         Return the keys in a similar way to a dictionary.
552         """
553         return object.__getattribute__(self, 'order')
554
555     def get(self, key, default=None):
556         """
557         Allows a dictionary-style get operation.
558         """
559         if key in self:
560             return self[key]
561         return default
562
563     def __str__(self):
564         return str(object.__getattribute__(self, 'data'))
565
566     def __repr__(self):
567         return repr(object.__getattribute__(self, 'data'))
568
569     def __len__(self):
570         return len(object.__getattribute__(self, 'order'))
571
572     def __iter__(self):
573         return self.iterkeys()
574
575     def iterkeys(self):
576         order = object.__getattribute__(self, 'order')
577         return order.__iter__()
578
579     def writeToStream(self, stream, indent, container, evaluated=False):
580         """
581         Write this instance to a stream at the specified indentation level.
582
583         Should be redefined in subclasses.
584
585         @param stream: The stream to write to
586         @type stream: A writable stream (file-like object)
587         @param indent: The indentation level
588         @type indent: int
589         @param container: The container of this instance
590         @type container: L{Container}
591         """
592         indstr = indent * '  '
593         if len(self) == 0:
594             stream.write(' { }%s' % NEWLINE)
595         else:
596             if isinstance(container, Mapping):
597                 stream.write(NEWLINE)
598             stream.write('%s{%s' % (indstr, NEWLINE))
599             self.__save__(stream, indent + 1, evaluated=evaluated)
600             stream.write('%s}%s' % (indstr, NEWLINE))
601
602     def __save__(self, stream, indent=0, evaluated=False):
603         """
604         Save this configuration to the specified stream.
605         @param stream: A stream to which the configuration is written.
606         @type stream: A write-only stream (file-like object).
607         @param indent: The indentation level for the output.
608         @type indent: int
609         """
610         indstr = indent * '  '
611         order = object.__getattribute__(self, 'order')
612         data = object.__getattribute__(self, 'data')
613         maxlen = 0 # max(map(lambda x: len(x), order))
614         for key in order:
615             comment = self.comments[key]
616             if isWord(key):
617                 skey = key
618             else:
619                 skey = repr(key)
620             if comment:
621                 stream.write('%s#%s' % (indstr, comment))
622             if skey.startswith("u'"):
623                 skey = skey[1:]
624             stream.write('%s%-*s :' % (indstr, maxlen, skey))
625             value = data[key]
626             if isinstance(value, Container):
627                 value.writeToStream(stream, indent, self, evaluated=evaluated)
628             else:
629                 self.writeValue(value, stream, indent, evaluated=evaluated)
630
631 class Config(Mapping):
632     """
633     This class represents a configuration, and is the only one which clients
634     need to interface to, under normal circumstances.
635     """
636
637     class Namespace(object):
638         """
639         This internal class is used for implementing default namespaces.
640
641         An instance acts as a namespace.
642         """
643         def __init__(self):
644             self.sys = sys
645             self.os = os
646
647     def __init__(self, streamOrFile=None, parent=None, PWD = None):
648         """
649         Initializes an instance.
650
651         @param streamOrFile: If specified, causes this instance to be loaded
652         from the stream (by calling L{load}). If a string is provided, it is
653         passed to L{streamOpener} to open a stream. Otherwise, the passed
654         value is assumed to be a stream and used as is.
655         @type streamOrFile: A readable stream (file-like object) or a name.
656         @param parent: If specified, this becomes the parent of this instance
657         in the configuration hierarchy.
658         @type parent: a L{Container} instance.
659         """
660         try: # Python 3 compatibility
661             if isinstance(streamOrFile, unicode):
662                 streamOrFile = streamOrFile.encode()
663         except NameError:
664             pass
665         Mapping.__init__(self, parent)
666         object.__setattr__(self, 'reader', ConfigReader(self))
667         object.__setattr__(self, 'namespaces', [Config.Namespace()])
668         if streamOrFile is not None:
669             if isinstance(streamOrFile, str) or isinstance(streamOrFile, bytes):
670                 global streamOpener
671                 if streamOpener is None:
672                     streamOpener = defaultStreamOpener
673                 streamOrFile = streamOpener(streamOrFile)
674             load = object.__getattribute__(self, "load")
675             load(streamOrFile)
676             # Specific add for salomeTools : PWD
677             if PWD:
678                 key, pwd = PWD
679                 if key == "":
680                     self.PWD = pwd
681                 else:
682                     self[key].PWD = pwd
683
684     def load(self, stream):
685         """
686         Load the configuration from the specified stream. Multiple streams can
687         be used to populate the same instance, as long as there are no
688         clashing keys. The stream is closed.
689         @param stream: A stream from which the configuration is read.
690         @type stream: A read-only stream (file-like object).
691         @raise ConfigError: if keys in the loaded configuration clash with
692         existing keys.
693         @raise ConfigFormatError: if there is a syntax error in the stream.
694         """
695         reader = object.__getattribute__(self, 'reader')
696         reader.load(stream)
697         stream.close()
698
699     def addNamespace(self, ns, name=None):
700         """
701         Add a namespace to this configuration which can be used to evaluate
702         (resolve) dotted-identifier expressions.
703         @param ns: The namespace to be added.
704         @type ns: A module or other namespace suitable for passing as an
705         argument to vars().
706         @param name: A name for the namespace, which, if specified, provides
707         an additional level of indirection.
708         @type name: str
709         """
710         namespaces = object.__getattribute__(self, 'namespaces')
711         if name is None:
712             namespaces.append(ns)
713         else:
714             setattr(namespaces[0], name, ns)
715
716     def removeNamespace(self, ns, name=None):
717         """
718         Remove a namespace added with L{addNamespace}.
719         @param ns: The namespace to be removed.
720         @param name: The name which was specified when L{addNamespace} was
721         called.
722         @type name: str
723         """
724         namespaces = object.__getattribute__(self, 'namespaces')
725         if name is None:
726             namespaces.remove(ns)
727         else:
728             delattr(namespaces[0], name)
729
730     def __save__(self, stream, indent=0, no_close=False, evaluated=False):
731         """
732         Save this configuration to the specified stream. The stream is
733         closed if this is the top-level configuration in the hierarchy.
734         L{Mapping.__save__} is called to do all the work.
735         @param stream: A stream to which the configuration is written.
736         @type stream: A write-only stream (file-like object).
737         @param indent: The indentation level for the output.
738         @type indent: int
739         """
740         Mapping.__save__(self, stream, indent, evaluated=evaluated)
741         if indent == 0 and not no_close:
742             stream.close()
743
744     def getByPath(self, path):
745         """
746         Obtain a value in the configuration via its path.
747         @param path: The path of the required value
748         @type path: str
749         @return the value at the specified path.
750         @rtype: any
751         @raise ConfigError: If the path is invalid
752         """
753         s = 'self.' + path
754         try:
755             return eval(s)
756         except Exception as e:
757             raise ConfigError(str(e))
758
759 class Sequence(Container):
760     """
761     This internal class implements a value which is a sequence of other values.
762     """
763     class SeqIter(object):
764         """
765         This internal class implements an iterator for a L{Sequence} instance.
766         """
767         def __init__(self, seq):
768             self.seq = seq
769             self.limit = len(object.__getattribute__(seq, 'data'))
770             self.index = 0
771
772         def __iter__(self):
773             return self
774
775         def next(self):
776             if self.index >= self.limit:
777                 raise StopIteration
778             rv = self.seq[self.index]
779             self.index += 1
780             return rv
781         
782         # This method is for python3 compatibility
783         def __next__(self): 
784             if self.index >= self.limit:
785                 raise StopIteration
786             rv = self.seq[self.index]
787             self.index += 1
788             return rv
789
790     def __init__(self, parent=None):
791         """
792         Initialize an instance.
793
794         @param parent: The parent of this instance in the hierarchy.
795         @type parent: A L{Container} instance.
796         """
797         Container.__init__(self, parent)
798         object.__setattr__(self, 'data', [])
799         object.__setattr__(self, 'comments', [])
800
801     def append(self, item, comment):
802         """
803         Add an item to the sequence.
804
805         @param item: The item to add.
806         @type item: any
807         @param comment: A comment for the item.
808         @type comment: str
809         """
810         data = object.__getattribute__(self, 'data')
811         comments = object.__getattribute__(self, 'comments')
812         data.append(item)
813         comments.append(comment)
814
815     def __getitem__(self, index):
816         data = object.__getattribute__(self, 'data')
817         try:
818             rv = data[index]
819         except (IndexError, KeyError, TypeError):
820             raise ConfigResolutionError('Invalid pyconf index %r for %r' % (index, object.__getattribute__(self, 'path')))
821         if not isinstance(rv, list):
822             rv = self.evaluate(rv)
823         else:
824             # deal with a slice
825             result = []
826             for a in rv:
827                 result.append(self.evaluate(a))
828             rv = result
829         return rv
830
831     def __iter__(self):
832         return Sequence.SeqIter(self)
833
834     def __repr__(self):
835         return repr(object.__getattribute__(self, 'data'))
836
837     def __str__(self):
838         return str(self[:]) # using the slice evaluates the contents
839
840     def __len__(self):
841         return len(object.__getattribute__(self, 'data'))
842
843     def writeToStream(self, stream, indent, container, evaluated=False):
844         """
845         Write this instance to a stream at the specified indentation level.
846
847         Should be redefined in subclasses.
848
849         @param stream: The stream to write to
850         @type stream: A writable stream (file-like object)
851         @param indent: The indentation level
852         @type indent: int
853         @param container: The container of this instance
854         @type container: L{Container}
855         """
856         indstr = indent * '  '
857         if len(self) == 0:
858             stream.write(' [ ]%s' % NEWLINE)
859         else:
860             if isinstance(container, Mapping):
861                 stream.write(NEWLINE)
862             stream.write('%s[%s' % (indstr, NEWLINE))
863             self.__save__(stream, indent + 1, evaluated=evaluated)
864             stream.write('%s]%s' % (indstr, NEWLINE))
865
866     def __save__(self, stream, indent, evaluated=False):
867         """
868         Save this instance to the specified stream.
869         @param stream: A stream to which the configuration is written.
870         @type stream: A write-only stream (file-like object).
871         @param indent: The indentation level for the output, > 0
872         @type indent: int
873         """
874         if indent == 0:
875             raise ConfigError("sequence cannot be saved as a top-level item")
876         data = object.__getattribute__(self, 'data')
877         comments = object.__getattribute__(self, 'comments')
878         indstr = indent * '  '
879         for i in range(0, len(data)):
880             value = data[i]
881             comment = comments[i]
882             if comment:
883                 stream.write('%s#%s' % (indstr, comment))
884             if isinstance(value, Container):
885                 value.writeToStream(stream, indent, self, evaluated=evaluated)
886             else:
887                 self.writeValue(value, stream, indent, evaluated=evaluated)
888
889 class Reference(object):
890     """
891     This internal class implements a value which is a reference to another value.
892     """
893     def __init__(self, config, type, ident):
894         """
895         Initialize an instance.
896
897         @param config: The configuration which contains this reference.
898         @type config: A L{Config} instance.
899         @param type: The type of reference.
900         @type type: BACKTICK or DOLLAR
901         @param ident: The identifier which starts the reference.
902         @type ident: str
903         """
904         self.config = config
905         self.type = type
906         self.elements = [ident]
907
908     def addElement(self, type, ident):
909         """
910         Add an element to the reference.
911
912         @param type: The type of reference.
913         @type type: BACKTICK or DOLLAR
914         @param ident: The identifier which continues the reference.
915         @type ident: str
916         """
917         self.elements.append((type, ident))
918
919     def findConfig(self, container):
920         """
921         Find the closest enclosing configuration to the specified container.
922
923         @param container: The container to start from.
924         @type container: L{Container}
925         @return: The closest enclosing configuration, or None.
926         @rtype: L{Config}
927         """
928         while (container is not None) and not isinstance(container, Config):
929             container = object.__getattribute__(container, 'parent')
930         return container
931
932     def resolve(self, container):
933         """
934         Resolve this instance in the context of a container.
935
936         @param container: The container to resolve from.
937         @type container: L{Container}
938         @return: The resolved value.
939         @rtype: any
940         @raise ConfigResolutionError: If resolution fails.
941         """
942         rv = None
943         path = object.__getattribute__(container, 'path')
944         current = container
945         while current is not None:
946             if self.type == BACKTICK:
947                 namespaces = object.__getattribute__(current, 'namespaces')
948                 found = False
949                 for ns in namespaces:
950                     try:
951                         rv = eval(str(self)[1:-1], vars(ns))
952                         found = True
953                         break
954                     except:
955                         pass
956                 if found:
957                     break
958             else:
959                 key = self.elements[0]
960                 try:
961                     rv = current[key]
962                     for item in self.elements[1:]:
963                         key = item[1]
964                         rv = rv[key]
965                     break
966                 except:
967                     rv = None
968                     pass
969             current = object.__getattribute__(current, 'parent')
970         if current is None:
971             raise ConfigResolutionError("unable to evaluate %r in the configuration %s" % (self, path))
972         return rv
973
974     def __str__(self):
975         s = self.elements[0]
976         for tt, tv in self.elements[1:]:
977             if tt == DOT:
978                 s += '.%s' % tv
979             else:
980                 s += '[%r]' % tv
981         if self.type == BACKTICK:
982             return BACKTICK + s + BACKTICK
983         else:
984             return DOLLAR + s
985
986     def __repr__(self):
987         return self.__str__()
988
989 class Expression(object):
990     """
991     This internal class implements a value which is obtained by evaluating an expression.
992     """
993     def __init__(self, op, lhs, rhs):
994         """
995         Initialize an instance.
996
997         @param op: the operation expressed in the expression.
998         @type op: PLUS, MINUS, STAR, SLASH, MOD
999         @param lhs: the left-hand-side operand of the expression.
1000         @type lhs: any Expression or primary value.
1001         @param rhs: the right-hand-side operand of the expression.
1002         @type rhs: any Expression or primary value.
1003         """
1004         self.op = op
1005         self.lhs = lhs
1006         self.rhs = rhs
1007
1008     def __str__(self):
1009         return '%r %s %r' % (self.lhs, self.op, self.rhs)
1010
1011     def __repr__(self):
1012         return self.__str__()
1013
1014     def evaluate(self, container):
1015         """
1016         Evaluate this instance in the context of a container.
1017
1018         @param container: The container to evaluate in from.
1019         @type container: L{Container}
1020         @return: The evaluated value.
1021         @rtype: any
1022         @raise ConfigResolutionError: If evaluation fails.
1023         @raise ZeroDivideError: If division by zero occurs.
1024         @raise TypeError: If the operation is invalid, e.g.
1025         subtracting one string from another.
1026         """
1027         lhs = self.lhs
1028         if isinstance(lhs, Reference):
1029             lhs = lhs.resolve(container)
1030         elif isinstance(lhs, Expression):
1031             lhs = lhs.evaluate(container)
1032         rhs = self.rhs
1033         if isinstance(rhs, Reference):
1034             rhs = rhs.resolve(container)
1035         elif isinstance(rhs, Expression):
1036             rhs = rhs.evaluate(container)
1037         op = self.op
1038         if op == PLUS:
1039             rv = lhs + rhs
1040         elif op == MINUS:
1041             rv = lhs - rhs
1042         elif op == STAR:
1043             rv = lhs * rhs
1044         elif op == SLASH:
1045             rv = lhs / rhs
1046         else:
1047             rv = lhs % rhs
1048         return rv
1049
1050 class ConfigReader(object):
1051     """
1052     This internal class implements a parser for configurations.
1053     """
1054
1055     def __init__(self, config):
1056         self.filename = None
1057         self.config = config
1058         self.lineno = 0
1059         self.colno = 0
1060         self.lastc = None
1061         self.last_token = None
1062         self.commentchars = '#'
1063         self.whitespace = ' \t\r\n'
1064         self.quotes = '\'"'
1065         self.punct = ':-+*/%,.{}[]()@`$'
1066         self.digits = '0123456789'
1067         self.wordchars = '%s' % WORDCHARS # make a copy
1068         self.identchars = self.wordchars + self.digits
1069         self.pbchars = []
1070         self.pbtokens = []
1071         self.comment = None
1072
1073     def location(self):
1074         """
1075         Return the current location (filename, line, column) in the stream
1076         as a string.
1077
1078         Used when printing error messages,
1079
1080         @return: A string representing a location in the stream being read.
1081         @rtype: str
1082         """
1083         return "%s(%d,%d)" % (self.filename, self.lineno, self.colno)
1084
1085     def getChar(self):
1086         """
1087         Get the next char from the stream. Update line and column numbers
1088         appropriately.
1089
1090         @return: The next character from the stream.
1091         @rtype: str
1092         """
1093         if self.pbchars:
1094             c = self.pbchars.pop()
1095             if isinstance(c,bytes):
1096                 c = c.decode()
1097         else:
1098             c = self.stream.read(1)
1099             if isinstance(c,bytes):
1100                 c = c.decode()
1101             self.colno += 1
1102             if c == '\n':
1103                 self.lineno += 1
1104                 self.colno = 1
1105         return c
1106
1107     def __repr__(self):
1108         return "<ConfigReader at 0x%08x>" % id(self)
1109
1110     __str__ = __repr__
1111
1112     def getToken(self):
1113         """
1114         Get a token from the stream. String values are returned in a form
1115         where you need to eval() the returned value to get the actual
1116         string. The return value is (token_type, token_value).
1117
1118         Multiline string tokenizing is thanks to David Janes (BlogMatrix)
1119
1120         @return: The next token.
1121         @rtype: A token tuple.
1122         """
1123         if self.pbtokens:
1124             return self.pbtokens.pop()
1125         stream = self.stream
1126         self.comment = None
1127         token = ''
1128         tt = EOF
1129         while True:
1130             c = self.getChar()
1131             if not c:
1132                 break
1133             elif c == '#':
1134                 if self.comment :
1135                     self.comment += '#' + stream.readline()
1136                 else :
1137                     self.comment = stream.readline()
1138                 self.lineno += 1
1139                 continue
1140             if c in self.quotes:
1141                 token = c
1142                 quote = c
1143                 tt = STRING
1144                 escaped = False
1145                 multiline = False
1146                 c1 = self.getChar()
1147                 if c1 == quote:
1148                     c2 = self.getChar()
1149                     if c2 == quote:
1150                         multiline = True
1151                         token += quote
1152                         token += quote
1153                     else:
1154                         self.pbchars.append(c2)
1155                         self.pbchars.append(c1)
1156                 else:
1157                     self.pbchars.append(c1)
1158                 while True:
1159                     c = self.getChar()
1160                     if not c:
1161                         break
1162                     token += c
1163                     if (c == quote) and not escaped:
1164                         if not multiline or (len(token) >= 6 and token.endswith(token[:3]) and token[-4] != '\\'):
1165                             break
1166                     if c == '\\':
1167                         escaped = not escaped
1168                     else:
1169                         escaped = False
1170                 if not c:
1171                     raise ConfigFormatError('%s: Unterminated quoted string: %r, %r' % (self.location(), token, c))
1172                 break
1173             if c in self.whitespace:
1174                 self.lastc = c
1175                 continue
1176             elif c in self.punct:
1177                 token = c
1178                 tt = c
1179                 if (self.lastc == ']') or (self.lastc in self.identchars):
1180                     if c == '[':
1181                         tt = LBRACK2
1182                     elif c == '(':
1183                         tt = LPAREN2
1184                 break
1185             elif c in self.digits:
1186                 token = c
1187                 tt = NUMBER
1188                 while True:
1189                     c = self.getChar()
1190                     if not c:
1191                         break
1192                     if c in self.digits:
1193                         token += c
1194                     elif (c == '.') and token.find('.') < 0:
1195                         token += c
1196                     else:
1197                         if c and (c not in self.whitespace):
1198                             self.pbchars.append(c)
1199                         break
1200                 break
1201             elif c in self.wordchars:
1202                 token = c
1203                 tt = WORD
1204                 c = self.getChar()
1205                 while c and (c in self.identchars):
1206                     token += c
1207                     c = self.getChar()
1208                 if c: # and c not in self.whitespace:
1209                     self.pbchars.append(c)
1210                 if token == "True":
1211                     tt = TRUE
1212                 elif token == "False":
1213                     tt = FALSE
1214                 elif token == "None":
1215                     tt = NONE
1216                 break
1217             else:
1218                 raise ConfigFormatError('%s: Unexpected character: %r' % (self.location(), c))
1219         if token:
1220             self.lastc = token[-1]
1221         else:
1222             self.lastc = None
1223         self.last_token = tt
1224         
1225         # Python 2.x specific unicode conversion
1226         if sys.version_info[0] == 2 and tt == WORD and isinstance(token, unicode):
1227             token = token.encode('ascii')
1228         return (tt, token)
1229
1230     def load(self, stream, parent=None, suffix=None):
1231         """
1232         Load the configuration from the specified stream.
1233
1234         @param stream: A stream from which to load the configuration.
1235         @type stream: A stream (file-like object).
1236         @param parent: The parent of the configuration (to which this reader
1237         belongs) in the hierarchy. Specified when the configuration is
1238         included in another one.
1239         @type parent: A L{Container} instance.
1240         @param suffix: The suffix of this configuration in the parent
1241         configuration. Should be specified whenever the parent is not None.
1242         @raise ConfigError: If parent is specified but suffix is not.
1243         @raise ConfigFormatError: If there are syntax errors in the stream.
1244         """
1245         if parent is not None:
1246             if suffix is None:
1247                 raise ConfigError("internal error: load called with parent but no suffix")
1248             self.config.setPath(makePath(object.__getattribute__(parent, 'path'), suffix))
1249         self.setStream(stream)
1250         self.token = self.getToken()
1251         self.parseMappingBody(self.config)
1252         if self.token[0] != EOF:
1253             raise ConfigFormatError('%s: expecting EOF, found %r' % (self.location(), self.token[1]))
1254
1255     def setStream(self, stream):
1256         """
1257         Set the stream to the specified value, and prepare to read from it.
1258
1259         @param stream: A stream from which to load the configuration.
1260         @type stream: A stream (file-like object).
1261         """
1262         self.stream = stream
1263         if hasattr(stream, 'name'):
1264             filename = stream.name
1265         else:
1266             filename = '?'
1267         self.filename = filename
1268         self.lineno = 1
1269         self.colno = 1
1270
1271     def match(self, t):
1272         """
1273         Ensure that the current token type matches the specified value, and
1274         advance to the next token.
1275
1276         @param t: The token type to match.
1277         @type t: A valid token type.
1278         @return: The token which was last read from the stream before this
1279         function is called.
1280         @rtype: a token tuple - see L{getToken}.
1281         @raise ConfigFormatError: If the token does not match what's expected.
1282         """
1283         if self.token[0] != t:
1284             raise ConfigFormatError("%s: expecting %s, found %r" % (self.location(), t, self.token[1]))
1285         rv = self.token
1286         self.token = self.getToken()
1287         return rv
1288
1289     def parseMappingBody(self, parent):
1290         """
1291         Parse the internals of a mapping, and add entries to the provided
1292         L{Mapping}.
1293
1294         @param parent: The mapping to add entries to.
1295         @type parent: A L{Mapping} instance.
1296         """
1297         while self.token[0] in [WORD, STRING]:
1298             self.parseKeyValuePair(parent)
1299
1300     def parseKeyValuePair(self, parent):
1301         """
1302         Parse a key-value pair, and add it to the provided L{Mapping}.
1303
1304         @param parent: The mapping to add entries to.
1305         @type parent: A L{Mapping} instance.
1306         @raise ConfigFormatError: if a syntax error is found.
1307         """
1308         comment = self.comment
1309         tt, tv = self.token
1310         if tt == WORD:
1311             key = tv
1312             suffix = tv
1313         elif tt == STRING:
1314             key = eval(tv)
1315             suffix = '[%s]' % tv
1316         else:
1317             msg = "%s: expecting word or string, found %r"
1318             raise ConfigFormatError(msg % (self.location(), tv))
1319         self.token = self.getToken()
1320         # for now, we allow key on its own as a short form of key : True
1321         if self.token[0] == COLON:
1322             self.token = self.getToken()
1323             value = self.parseValue(parent, suffix)
1324         else:
1325             value = True
1326         try:
1327             parent.addMapping(key, value, comment)
1328         except Exception as e:
1329             raise ConfigFormatError("%s: %s, %r" % (self.location(), e,
1330                                     self.token[1]))
1331         tt = self.token[0]
1332         if tt not in [EOF, WORD, STRING, RCURLY, COMMA]:
1333             msg = "%s: expecting one of EOF, WORD, STRING, \
1334 RCURLY, COMMA, found %r"
1335             raise ConfigFormatError(msg  % (self.location(), self.token[1]))
1336         if tt == COMMA:
1337             self.token = self.getToken()
1338
1339     def parseValue(self, parent, suffix):
1340         """
1341         Parse a value.
1342
1343         @param parent: The container to which the value will be added.
1344         @type parent: A L{Container} instance.
1345         @param suffix: The suffix for the value.
1346         @type suffix: str
1347         @return: The value
1348         @rtype: any
1349         @raise ConfigFormatError: if a syntax error is found.
1350         """
1351         tt = self.token[0]
1352         if tt in [STRING, WORD, NUMBER, LPAREN, DOLLAR,
1353                   TRUE, FALSE, NONE, BACKTICK, MINUS]:
1354             rv = self.parseScalar()
1355         elif tt == LBRACK:
1356             rv = self.parseSequence(parent, suffix)
1357         elif tt in [LCURLY, AT]:
1358             rv = self.parseMapping(parent, suffix)
1359         else:
1360             raise ConfigFormatError("%s: unexpected input: %r" %
1361                (self.location(), self.token[1]))
1362         return rv
1363
1364     def parseSequence(self, parent, suffix):
1365         """
1366         Parse a sequence.
1367
1368         @param parent: The container to which the sequence will be added.
1369         @type parent: A L{Container} instance.
1370         @param suffix: The suffix for the value.
1371         @type suffix: str
1372         @return: a L{Sequence} instance representing the sequence.
1373         @rtype: L{Sequence}
1374         @raise ConfigFormatError: if a syntax error is found.
1375         """
1376         rv = Sequence(parent)
1377         rv.setPath(makePath(object.__getattribute__(parent, 'path'), suffix))
1378         self.match(LBRACK)
1379         comment = self.comment
1380         tt = self.token[0]
1381         while tt in [STRING, WORD, NUMBER, LCURLY, LBRACK, LPAREN, DOLLAR,
1382                      TRUE, FALSE, NONE, BACKTICK]:
1383             suffix = '[%d]' % len(rv)
1384             value = self.parseValue(parent, suffix)
1385             rv.append(value, comment)
1386             tt = self.token[0]
1387             comment = self.comment
1388             if tt == COMMA:
1389                 self.match(COMMA)
1390                 tt = self.token[0]
1391                 comment = self.comment
1392                 continue
1393         self.match(RBRACK)
1394         return rv
1395
1396     def parseMapping(self, parent, suffix):
1397         """
1398         Parse a mapping.
1399
1400         @param parent: The container to which the mapping will be added.
1401         @type parent: A L{Container} instance.
1402         @param suffix: The suffix for the value.
1403         @type suffix: str
1404         @return: a L{Mapping} instance representing the mapping.
1405         @rtype: L{Mapping}
1406         @raise ConfigFormatError: if a syntax error is found.
1407         """
1408         if self.token[0] == LCURLY:
1409             self.match(LCURLY)
1410             rv = Mapping(parent)
1411             rv.setPath(
1412                makePath(object.__getattribute__(parent, 'path'), suffix))
1413             self.parseMappingBody(rv)
1414             self.match(RCURLY)
1415         else:
1416             self.match(AT)
1417             _, fn = self.match(STRING)
1418             rv = Config(eval(fn), parent)
1419         return rv
1420
1421     def parseScalar(self):
1422         """
1423         Parse a scalar - a terminal value such as a string or number, or
1424         an L{Expression} or L{Reference}.
1425
1426         @return: the parsed scalar
1427         @rtype: any scalar
1428         @raise ConfigFormatError: if a syntax error is found.
1429         """
1430         lhs = self.parseTerm()
1431         tt = self.token[0]
1432         while tt in [PLUS, MINUS]:
1433             self.match(tt)
1434             rhs = self.parseTerm()
1435             lhs = Expression(tt, lhs, rhs)
1436             tt = self.token[0]
1437         return lhs
1438
1439     def parseTerm(self):
1440         """
1441         Parse a term in an additive expression (a + b, a - b)
1442
1443         @return: the parsed term
1444         @rtype: any scalar
1445         @raise ConfigFormatError: if a syntax error is found.
1446         """
1447         lhs = self.parseFactor()
1448         tt = self.token[0]
1449         while tt in [STAR, SLASH, MOD]:
1450             self.match(tt)
1451             rhs = self.parseFactor()
1452             lhs = Expression(tt, lhs, rhs)
1453             tt = self.token[0]
1454         return lhs
1455
1456     def parseFactor(self):
1457         """
1458         Parse a factor in an multiplicative expression (a * b, a / b, a % b)
1459
1460         @return: the parsed factor
1461         @rtype: any scalar
1462         @raise ConfigFormatError: if a syntax error is found.
1463         """
1464         tt = self.token[0]
1465         if tt in [NUMBER, WORD, STRING, TRUE, FALSE, NONE]:
1466             rv = self.token[1]
1467             if tt != WORD:
1468                 rv = eval(rv)
1469             self.match(tt)
1470         elif tt == LPAREN:
1471             self.match(LPAREN)
1472             rv = self.parseScalar()
1473             self.match(RPAREN)
1474         elif tt == DOLLAR:
1475             self.match(DOLLAR)
1476             rv = self.parseReference(DOLLAR)
1477         elif tt == BACKTICK:
1478             self.match(BACKTICK)
1479             rv = self.parseReference(BACKTICK)
1480             self.match(BACKTICK)
1481         elif tt == MINUS:
1482             self.match(MINUS)
1483             rv = -self.parseScalar()
1484         else:
1485             raise ConfigFormatError("%s: unexpected input: %r" %
1486                (self.location(), self.token[1]))
1487         return rv
1488
1489     def parseReference(self, type):
1490         """
1491         Parse a reference.
1492
1493         @return: the parsed reference
1494         @rtype: L{Reference}
1495         @raise ConfigFormatError: if a syntax error is found.
1496         """
1497         word = self.match(WORD)
1498         rv = Reference(self.config, type, word[1])
1499         while self.token[0] in [DOT, LBRACK2]:
1500             self.parseSuffix(rv)
1501         return rv
1502
1503     def parseSuffix(self, ref):
1504         """
1505         Parse a reference suffix.
1506
1507         @param ref: The reference of which this suffix is a part.
1508         @type ref: L{Reference}.
1509         @raise ConfigFormatError: if a syntax error is found.
1510         """
1511         tt = self.token[0]
1512         if tt == DOT:
1513             self.match(DOT)
1514             word = self.match(WORD)
1515             ref.addElement(DOT, word[1])
1516         else:
1517             self.match(LBRACK2)
1518             tt, tv = self.token
1519             if tt not in [NUMBER, STRING]:
1520                 raise ConfigFormatError("%s: expected number or string, found %r" % (self.location(), tv))
1521             self.token = self.getToken()
1522             tv = eval(tv)
1523             self.match(RBRACK)
1524             ref.addElement(LBRACK, tv)
1525
1526 def defaultMergeResolve(map1, map2, key):
1527     """\
1528     A default resolver for merge conflicts. 
1529     Returns a string indicating what action to take to resolve the conflict.
1530
1531     @param map1: The map being merged into.
1532     @type map1: L{Mapping}.
1533     @param map2: The map being used as the merge operand.
1534     @type map2: L{Mapping}.
1535     @param key: The key in map2 (which also exists in map1).
1536     @type key: str
1537
1538     @return: One of "merge", "append", "mismatch" or "overwrite"
1539              indicating what action should be taken. This should
1540              be appropriate to the objects being merged - e.g.
1541              there is no point returning "merge" if the two objects
1542              are instances of L{Sequence}.
1543
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