@@ -146,6 +146,43 @@ def __exit__(self, exception_type, exception_value, traceback):
146146 self ._config .__exit__ (exception_type , exception_value , traceback )
147147
148148
149+ class _OMD (OrderedDict ):
150+ """Ordered multi-dict."""
151+
152+ def __setitem__ (self , key , value ):
153+ super (_OMD , self ).__setitem__ (key , [value ])
154+
155+ def add (self , key , value ):
156+ if key not in self :
157+ super (_OMD , self ).__setitem__ (key , [value ])
158+ return
159+
160+ super (_OMD , self ).__getitem__ (key ).append (value )
161+
162+ def setall (self , key , values ):
163+ super (_OMD , self ).__setitem__ (key , values )
164+
165+ def __getitem__ (self , key ):
166+ return super (_OMD , self ).__getitem__ (key )[- 1 ]
167+
168+ def getlast (self , key ):
169+ return super (_OMD , self ).__getitem__ (key )[- 1 ]
170+
171+ def setlast (self , key , value ):
172+ if key not in self :
173+ super (_OMD , self ).__setitem__ (key , [value ])
174+ return
175+
176+ prior = super (_OMD , self ).__getitem__ (key )
177+ prior [- 1 ] = value
178+
179+ def getall (self , key ):
180+ return super (_OMD , self ).__getitem__ (key )
181+
182+ def items_all (self ):
183+ return [(k , self .get (k )) for k in self ]
184+
185+
149186class GitConfigParser (with_metaclass (MetaParserBuilder , cp .RawConfigParser , object )):
150187
151188 """Implements specifics required to read git style configuration files.
@@ -200,7 +237,7 @@ def __init__(self, file_or_files, read_only=True, merge_includes=True):
200237 contents into ours. This makes it impossible to write back an individual configuration file.
201238 Thus, if you want to modify a single configuration file, turn this off to leave the original
202239 dataset unaltered when reading it."""
203- cp .RawConfigParser .__init__ (self , dict_type = OrderedDict )
240+ cp .RawConfigParser .__init__ (self , dict_type = _OMD )
204241
205242 # Used in python 3, needs to stay in sync with sections for underlying implementation to work
206243 if not hasattr (self , '_proxies' ):
@@ -348,7 +385,8 @@ def string_decode(v):
348385 is_multi_line = True
349386 optval = string_decode (optval [1 :])
350387 # end handle multi-line
351- cursect [optname ] = optval
388+ # preserves multiple values for duplicate optnames
389+ cursect .add (optname , optval )
352390 else :
353391 # check if it's an option with no value - it's just ignored by git
354392 if not self .OPTVALUEONLY .match (line ):
@@ -362,7 +400,8 @@ def string_decode(v):
362400 is_multi_line = False
363401 line = line [:- 1 ]
364402 # end handle quotations
365- cursect [optname ] += string_decode (line )
403+ optval = cursect .getlast (optname )
404+ cursect .setlast (optname , optval + string_decode (line ))
366405 # END parse section or option
367406 # END while reading
368407
@@ -442,9 +481,17 @@ def _write(self, fp):
442481 git compatible format"""
443482 def write_section (name , section_dict ):
444483 fp .write (("[%s]\n " % name ).encode (defenc ))
445- for (key , value ) in section_dict .items ():
446- if key != "__name__" :
447- fp .write (("\t %s = %s\n " % (key , self ._value_to_string (value ).replace ('\n ' , '\n \t ' ))).encode (defenc ))
484+ for (key , value ) in section_dict .items_all ():
485+ if key == "__name__" :
486+ continue
487+ elif isinstance (value , list ):
488+ values = value
489+ else :
490+ # self._defaults isn't a multidict
491+ values = [value ]
492+
493+ for v in values :
494+ fp .write (("\t %s = %s\n " % (key , self ._value_to_string (v ).replace ('\n ' , '\n \t ' ))).encode (defenc ))
448495 # END if key is not __name__
449496 # END section writing
450497
@@ -457,6 +504,27 @@ def items(self, section_name):
457504 """:return: list((option, value), ...) pairs of all items in the given section"""
458505 return [(k , v ) for k , v in super (GitConfigParser , self ).items (section_name ) if k != '__name__' ]
459506
507+ def items_all (self , section_name ):
508+ """:return: list((option, [values...]), ...) pairs of all items in the given section"""
509+ rv = OrderedDict ()
510+ for k , v in self ._defaults :
511+ rv [k ] = [v ]
512+
513+ for k , v in self ._sections [section_name ].items_all ():
514+ if k == '__name__' :
515+ continue
516+
517+ if k not in rv :
518+ rv [k ] = v
519+ continue
520+
521+ if rv [k ] == v :
522+ continue
523+
524+ rv [k ].extend (v )
525+
526+ return rv .items ()
527+
460528 @needs_values
461529 def write (self ):
462530 """Write changes to our file, if there are changes at all
@@ -508,7 +576,11 @@ def read_only(self):
508576 return self ._read_only
509577
510578 def get_value (self , section , option , default = None ):
511- """
579+ """Get an option's value.
580+
581+ If multiple values are specified for this option in the section, the
582+ last one specified is returned.
583+
512584 :param default:
513585 If not None, the given default value will be returned in case
514586 the option did not exist
@@ -523,6 +595,31 @@ def get_value(self, section, option, default=None):
523595 return default
524596 raise
525597
598+ return self ._string_to_value (valuestr )
599+
600+ def get_values (self , section , option , default = None ):
601+ """Get an option's values.
602+
603+ If multiple values are specified for this option in the section, all are
604+ returned.
605+
606+ :param default:
607+ If not None, a list containing the given default value will be
608+ returned in case the option did not exist
609+ :return: a list of properly typed values, either int, float or string
610+
611+ :raise TypeError: in case the value could not be understood
612+ Otherwise the exceptions known to the ConfigParser will be raised."""
613+ try :
614+ lst = self ._sections [section ].getall (option )
615+ except Exception :
616+ if default is not None :
617+ return [default ]
618+ raise
619+
620+ return [self ._string_to_value (valuestr ) for valuestr in lst ]
621+
622+ def _string_to_value (self , valuestr ):
526623 types = (int , float )
527624 for numtype in types :
528625 try :
@@ -545,7 +642,9 @@ def get_value(self, section, option, default=None):
545642 return True
546643
547644 if not isinstance (valuestr , string_types ):
548- raise TypeError ("Invalid value type: only int, long, float and str are allowed" , valuestr )
645+ raise TypeError (
646+ "Invalid value type: only int, long, float and str are allowed" ,
647+ valuestr )
549648
550649 return valuestr
551650
@@ -572,6 +671,25 @@ def set_value(self, section, option, value):
572671 self .set (section , option , self ._value_to_string (value ))
573672 return self
574673
674+ @needs_values
675+ @set_dirty_and_flush_changes
676+ def add_value (self , section , option , value ):
677+ """Adds a value for the given option in section.
678+ It will create the section if required, and will not throw as opposed to the default
679+ ConfigParser 'set' method. The value becomes the new value of the option as returned
680+ by 'get_value', and appends to the list of values returned by 'get_values`'.
681+
682+ :param section: Name of the section in which the option resides or should reside
683+ :param option: Name of the option
684+
685+ :param value: Value to add to option. It must be a string or convertible
686+ to a string
687+ :return: this instance"""
688+ if not self .has_section (section ):
689+ self .add_section (section )
690+ self ._sections [section ].add (option , self ._value_to_string (value ))
691+ return self
692+
575693 def rename_section (self , section , new_name ):
576694 """rename the given section to new_name
577695 :raise ValueError: if section doesn't exit
@@ -584,8 +702,9 @@ def rename_section(self, section, new_name):
584702 raise ValueError ("Destination section '%s' already exists" % new_name )
585703
586704 super (GitConfigParser , self ).add_section (new_name )
587- for k , v in self .items (section ):
588- self .set (new_name , k , self ._value_to_string (v ))
705+ new_section = self ._sections [new_name ]
706+ for k , vs in self .items_all (section ):
707+ new_section .setall (k , vs )
589708 # end for each value to copy
590709
591710 # This call writes back the changes, which is why we don't have the respective decorator
0 commit comments