@@ -146,6 +146,51 @@ 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 get (self , key , default = None ):
180+ return super (_OMD , self ).get (key , [default ])[- 1 ]
181+
182+ def getall (self , key ):
183+ return super (_OMD , self ).__getitem__ (key )
184+
185+ def items (self ):
186+ """List of (key, last value for key)."""
187+ return [(k , self [k ]) for k in self ]
188+
189+ def items_all (self ):
190+ """List of (key, list of values for key)."""
191+ return [(k , self .getall (k )) for k in self ]
192+
193+
149194class GitConfigParser (with_metaclass (MetaParserBuilder , cp .RawConfigParser , object )):
150195
151196 """Implements specifics required to read git style configuration files.
@@ -200,7 +245,7 @@ def __init__(self, file_or_files, read_only=True, merge_includes=True):
200245 contents into ours. This makes it impossible to write back an individual configuration file.
201246 Thus, if you want to modify a single configuration file, turn this off to leave the original
202247 dataset unaltered when reading it."""
203- cp .RawConfigParser .__init__ (self , dict_type = OrderedDict )
248+ cp .RawConfigParser .__init__ (self , dict_type = _OMD )
204249
205250 # Used in python 3, needs to stay in sync with sections for underlying implementation to work
206251 if not hasattr (self , '_proxies' ):
@@ -348,7 +393,8 @@ def string_decode(v):
348393 is_multi_line = True
349394 optval = string_decode (optval [1 :])
350395 # end handle multi-line
351- cursect [optname ] = optval
396+ # preserves multiple values for duplicate optnames
397+ cursect .add (optname , optval )
352398 else :
353399 # check if it's an option with no value - it's just ignored by git
354400 if not self .OPTVALUEONLY .match (line ):
@@ -362,7 +408,8 @@ def string_decode(v):
362408 is_multi_line = False
363409 line = line [:- 1 ]
364410 # end handle quotations
365- cursect [optname ] += string_decode (line )
411+ optval = cursect .getlast (optname )
412+ cursect .setlast (optname , optval + string_decode (line ))
366413 # END parse section or option
367414 # END while reading
368415
@@ -442,9 +489,12 @@ def _write(self, fp):
442489 git compatible format"""
443490 def write_section (name , section_dict ):
444491 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 ))
492+ for (key , values ) in section_dict .items_all ():
493+ if key == "__name__" :
494+ continue
495+
496+ for v in values :
497+ fp .write (("\t %s = %s\n " % (key , self ._value_to_string (v ).replace ('\n ' , '\n \t ' ))).encode (defenc ))
448498 # END if key is not __name__
449499 # END section writing
450500
@@ -457,6 +507,22 @@ def items(self, section_name):
457507 """:return: list((option, value), ...) pairs of all items in the given section"""
458508 return [(k , v ) for k , v in super (GitConfigParser , self ).items (section_name ) if k != '__name__' ]
459509
510+ def items_all (self , section_name ):
511+ """:return: list((option, [values...]), ...) pairs of all items in the given section"""
512+ rv = _OMD (self ._defaults )
513+
514+ for k , vs in self ._sections [section_name ].items_all ():
515+ if k == '__name__' :
516+ continue
517+
518+ if k in rv and rv .getall (k ) == vs :
519+ continue
520+
521+ for v in vs :
522+ rv .add (k , v )
523+
524+ return rv .items_all ()
525+
460526 @needs_values
461527 def write (self ):
462528 """Write changes to our file, if there are changes at all
@@ -508,7 +574,11 @@ def read_only(self):
508574 return self ._read_only
509575
510576 def get_value (self , section , option , default = None ):
511- """
577+ """Get an option's value.
578+
579+ If multiple values are specified for this option in the section, the
580+ last one specified is returned.
581+
512582 :param default:
513583 If not None, the given default value will be returned in case
514584 the option did not exist
@@ -523,6 +593,31 @@ def get_value(self, section, option, default=None):
523593 return default
524594 raise
525595
596+ return self ._string_to_value (valuestr )
597+
598+ def get_values (self , section , option , default = None ):
599+ """Get an option's values.
600+
601+ If multiple values are specified for this option in the section, all are
602+ returned.
603+
604+ :param default:
605+ If not None, a list containing the given default value will be
606+ returned in case the option did not exist
607+ :return: a list of properly typed values, either int, float or string
608+
609+ :raise TypeError: in case the value could not be understood
610+ Otherwise the exceptions known to the ConfigParser will be raised."""
611+ try :
612+ lst = self ._sections [section ].getall (option )
613+ except Exception :
614+ if default is not None :
615+ return [default ]
616+ raise
617+
618+ return [self ._string_to_value (valuestr ) for valuestr in lst ]
619+
620+ def _string_to_value (self , valuestr ):
526621 types = (int , float )
527622 for numtype in types :
528623 try :
@@ -545,7 +640,9 @@ def get_value(self, section, option, default=None):
545640 return True
546641
547642 if not isinstance (valuestr , string_types ):
548- raise TypeError ("Invalid value type: only int, long, float and str are allowed" , valuestr )
643+ raise TypeError (
644+ "Invalid value type: only int, long, float and str are allowed" ,
645+ valuestr )
549646
550647 return valuestr
551648
@@ -572,6 +669,25 @@ def set_value(self, section, option, value):
572669 self .set (section , option , self ._value_to_string (value ))
573670 return self
574671
672+ @needs_values
673+ @set_dirty_and_flush_changes
674+ def add_value (self , section , option , value ):
675+ """Adds a value for the given option in section.
676+ It will create the section if required, and will not throw as opposed to the default
677+ ConfigParser 'set' method. The value becomes the new value of the option as returned
678+ by 'get_value', and appends to the list of values returned by 'get_values`'.
679+
680+ :param section: Name of the section in which the option resides or should reside
681+ :param option: Name of the option
682+
683+ :param value: Value to add to option. It must be a string or convertible
684+ to a string
685+ :return: this instance"""
686+ if not self .has_section (section ):
687+ self .add_section (section )
688+ self ._sections [section ].add (option , self ._value_to_string (value ))
689+ return self
690+
575691 def rename_section (self , section , new_name ):
576692 """rename the given section to new_name
577693 :raise ValueError: if section doesn't exit
@@ -584,8 +700,9 @@ def rename_section(self, section, new_name):
584700 raise ValueError ("Destination section '%s' already exists" % new_name )
585701
586702 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 ))
703+ new_section = self ._sections [new_name ]
704+ for k , vs in self .items_all (section ):
705+ new_section .setall (k , vs )
589706 # end for each value to copy
590707
591708 # This call writes back the changes, which is why we don't have the respective decorator
0 commit comments