diff options
-rw-r--r-- | AUTHORS | 1 | ||||
-rw-r--r-- | git/config.py | 137 | ||||
-rw-r--r-- | git/test/fixtures/git_config_multiple | 7 | ||||
-rw-r--r-- | git/test/test_config.py | 109 |
4 files changed, 243 insertions, 11 deletions
@@ -29,5 +29,6 @@ Contributors are: -Tim Swast <swast _at_ google.com> -William Luc Ritchie -David Host <hostdm _at_ outlook.com> +-A. Jesse Jiryu Davis <jesse _at_ emptysquare.net> Portions derived from other open source works and are clearly marked. diff --git a/git/config.py b/git/config.py index 68d65ae9..b03d9d42 100644 --- a/git/config.py +++ b/git/config.py @@ -146,6 +146,51 @@ class SectionConstraint(object): self._config.__exit__(exception_type, exception_value, traceback) +class _OMD(OrderedDict): + """Ordered multi-dict.""" + + def __setitem__(self, key, value): + super(_OMD, self).__setitem__(key, [value]) + + def add(self, key, value): + if key not in self: + super(_OMD, self).__setitem__(key, [value]) + return + + super(_OMD, self).__getitem__(key).append(value) + + def setall(self, key, values): + super(_OMD, self).__setitem__(key, values) + + def __getitem__(self, key): + return super(_OMD, self).__getitem__(key)[-1] + + def getlast(self, key): + return super(_OMD, self).__getitem__(key)[-1] + + def setlast(self, key, value): + if key not in self: + super(_OMD, self).__setitem__(key, [value]) + return + + prior = super(_OMD, self).__getitem__(key) + prior[-1] = value + + def get(self, key, default=None): + return super(_OMD, self).get(key, [default])[-1] + + def getall(self, key): + return super(_OMD, self).__getitem__(key) + + def items(self): + """List of (key, last value for key).""" + return [(k, self[k]) for k in self] + + def items_all(self): + """List of (key, list of values for key).""" + return [(k, self.getall(k)) for k in self] + + class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, object)): """Implements specifics required to read git style configuration files. @@ -200,7 +245,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje contents into ours. This makes it impossible to write back an individual configuration file. Thus, if you want to modify a single configuration file, turn this off to leave the original dataset unaltered when reading it.""" - cp.RawConfigParser.__init__(self, dict_type=OrderedDict) + cp.RawConfigParser.__init__(self, dict_type=_OMD) # Used in python 3, needs to stay in sync with sections for underlying implementation to work if not hasattr(self, '_proxies'): @@ -348,7 +393,8 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje is_multi_line = True optval = string_decode(optval[1:]) # end handle multi-line - cursect[optname] = optval + # preserves multiple values for duplicate optnames + cursect.add(optname, optval) else: # check if it's an option with no value - it's just ignored by git if not self.OPTVALUEONLY.match(line): @@ -362,7 +408,8 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje is_multi_line = False line = line[:-1] # end handle quotations - cursect[optname] += string_decode(line) + optval = cursect.getlast(optname) + cursect.setlast(optname, optval + string_decode(line)) # END parse section or option # END while reading @@ -442,9 +489,12 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje git compatible format""" def write_section(name, section_dict): fp.write(("[%s]\n" % name).encode(defenc)) - for (key, value) in section_dict.items(): - if key != "__name__": - fp.write(("\t%s = %s\n" % (key, self._value_to_string(value).replace('\n', '\n\t'))).encode(defenc)) + for (key, values) in section_dict.items_all(): + if key == "__name__": + continue + + for v in values: + fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace('\n', '\n\t'))).encode(defenc)) # END if key is not __name__ # END section writing @@ -457,6 +507,22 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje """:return: list((option, value), ...) pairs of all items in the given section""" return [(k, v) for k, v in super(GitConfigParser, self).items(section_name) if k != '__name__'] + def items_all(self, section_name): + """:return: list((option, [values...]), ...) pairs of all items in the given section""" + rv = _OMD(self._defaults) + + for k, vs in self._sections[section_name].items_all(): + if k == '__name__': + continue + + if k in rv and rv.getall(k) == vs: + continue + + for v in vs: + rv.add(k, v) + + return rv.items_all() + @needs_values def write(self): """Write changes to our file, if there are changes at all @@ -508,7 +574,11 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje return self._read_only def get_value(self, section, option, default=None): - """ + """Get an option's value. + + If multiple values are specified for this option in the section, the + last one specified is returned. + :param default: If not None, the given default value will be returned in case the option did not exist @@ -523,6 +593,31 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje return default raise + return self._string_to_value(valuestr) + + def get_values(self, section, option, default=None): + """Get an option's values. + + If multiple values are specified for this option in the section, all are + returned. + + :param default: + If not None, a list containing the given default value will be + returned in case the option did not exist + :return: a list of properly typed values, either int, float or string + + :raise TypeError: in case the value could not be understood + Otherwise the exceptions known to the ConfigParser will be raised.""" + try: + lst = self._sections[section].getall(option) + except Exception: + if default is not None: + return [default] + raise + + return [self._string_to_value(valuestr) for valuestr in lst] + + def _string_to_value(self, valuestr): types = (int, float) for numtype in types: try: @@ -545,7 +640,9 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje return True if not isinstance(valuestr, string_types): - raise TypeError("Invalid value type: only int, long, float and str are allowed", valuestr) + raise TypeError( + "Invalid value type: only int, long, float and str are allowed", + valuestr) return valuestr @@ -572,6 +669,25 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje self.set(section, option, self._value_to_string(value)) return self + @needs_values + @set_dirty_and_flush_changes + def add_value(self, section, option, value): + """Adds a value for the given option in section. + It will create the section if required, and will not throw as opposed to the default + ConfigParser 'set' method. The value becomes the new value of the option as returned + by 'get_value', and appends to the list of values returned by 'get_values`'. + + :param section: Name of the section in which the option resides or should reside + :param option: Name of the option + + :param value: Value to add to option. It must be a string or convertible + to a string + :return: this instance""" + if not self.has_section(section): + self.add_section(section) + self._sections[section].add(option, self._value_to_string(value)) + return self + def rename_section(self, section, new_name): """rename the given section to new_name :raise ValueError: if section doesn't exit @@ -584,8 +700,9 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje raise ValueError("Destination section '%s' already exists" % new_name) super(GitConfigParser, self).add_section(new_name) - for k, v in self.items(section): - self.set(new_name, k, self._value_to_string(v)) + new_section = self._sections[new_name] + for k, vs in self.items_all(section): + new_section.setall(k, vs) # end for each value to copy # This call writes back the changes, which is why we don't have the respective decorator diff --git a/git/test/fixtures/git_config_multiple b/git/test/fixtures/git_config_multiple new file mode 100644 index 00000000..03a97568 --- /dev/null +++ b/git/test/fixtures/git_config_multiple @@ -0,0 +1,7 @@ +[section0] + option0 = value0 + +[section1] + option1 = value1a + option1 = value1b + other_option1 = other_value1 diff --git a/git/test/test_config.py b/git/test/test_config.py index 4d6c8236..93f94748 100644 --- a/git/test/test_config.py +++ b/git/test/test_config.py @@ -11,7 +11,7 @@ from git import ( GitConfigParser ) from git.compat import string_types -from git.config import cp +from git.config import _OMD, cp from git.test.lib import ( TestCase, fixture_path, @@ -265,3 +265,110 @@ class TestBase(TestCase): with self.assertRaises(cp.NoOptionError): cr.get_value('color', 'ui') + + def test_multiple_values(self): + file_obj = self._to_memcache(fixture_path('git_config_multiple')) + with GitConfigParser(file_obj, read_only=False) as cw: + self.assertEqual(cw.get('section0', 'option0'), 'value0') + self.assertEqual(cw.get_values('section0', 'option0'), ['value0']) + self.assertEqual(cw.items('section0'), [('option0', 'value0')]) + + # Where there are multiple values, "get" returns the last. + self.assertEqual(cw.get('section1', 'option1'), 'value1b') + self.assertEqual(cw.get_values('section1', 'option1'), + ['value1a', 'value1b']) + self.assertEqual(cw.items('section1'), + [('option1', 'value1b'), + ('other_option1', 'other_value1')]) + self.assertEqual(cw.items_all('section1'), + [('option1', ['value1a', 'value1b']), + ('other_option1', ['other_value1'])]) + with self.assertRaises(KeyError): + cw.get_values('section1', 'missing') + + self.assertEqual(cw.get_values('section1', 'missing', 1), [1]) + self.assertEqual(cw.get_values('section1', 'missing', 's'), ['s']) + + def test_multiple_values_rename(self): + file_obj = self._to_memcache(fixture_path('git_config_multiple')) + with GitConfigParser(file_obj, read_only=False) as cw: + cw.rename_section('section1', 'section2') + cw.write() + file_obj.seek(0) + cr = GitConfigParser(file_obj, read_only=True) + self.assertEqual(cr.get_value('section2', 'option1'), 'value1b') + self.assertEqual(cr.get_values('section2', 'option1'), + ['value1a', 'value1b']) + self.assertEqual(cr.items('section2'), + [('option1', 'value1b'), + ('other_option1', 'other_value1')]) + self.assertEqual(cr.items_all('section2'), + [('option1', ['value1a', 'value1b']), + ('other_option1', ['other_value1'])]) + + def test_multiple_to_single(self): + file_obj = self._to_memcache(fixture_path('git_config_multiple')) + with GitConfigParser(file_obj, read_only=False) as cw: + cw.set_value('section1', 'option1', 'value1c') + + cw.write() + file_obj.seek(0) + cr = GitConfigParser(file_obj, read_only=True) + self.assertEqual(cr.get_value('section1', 'option1'), 'value1c') + self.assertEqual(cr.get_values('section1', 'option1'), ['value1c']) + self.assertEqual(cr.items('section1'), + [('option1', 'value1c'), + ('other_option1', 'other_value1')]) + self.assertEqual(cr.items_all('section1'), + [('option1', ['value1c']), + ('other_option1', ['other_value1'])]) + + def test_single_to_multiple(self): + file_obj = self._to_memcache(fixture_path('git_config_multiple')) + with GitConfigParser(file_obj, read_only=False) as cw: + cw.add_value('section1', 'other_option1', 'other_value1a') + + cw.write() + file_obj.seek(0) + cr = GitConfigParser(file_obj, read_only=True) + self.assertEqual(cr.get_value('section1', 'option1'), 'value1b') + self.assertEqual(cr.get_values('section1', 'option1'), + ['value1a', 'value1b']) + self.assertEqual(cr.get_value('section1', 'other_option1'), + 'other_value1a') + self.assertEqual(cr.get_values('section1', 'other_option1'), + ['other_value1', 'other_value1a']) + self.assertEqual(cr.items('section1'), + [('option1', 'value1b'), + ('other_option1', 'other_value1a')]) + self.assertEqual( + cr.items_all('section1'), + [('option1', ['value1a', 'value1b']), + ('other_option1', ['other_value1', 'other_value1a'])]) + + def test_add_to_multiple(self): + file_obj = self._to_memcache(fixture_path('git_config_multiple')) + with GitConfigParser(file_obj, read_only=False) as cw: + cw.add_value('section1', 'option1', 'value1c') + cw.write() + file_obj.seek(0) + cr = GitConfigParser(file_obj, read_only=True) + self.assertEqual(cr.get_value('section1', 'option1'), 'value1c') + self.assertEqual(cr.get_values('section1', 'option1'), + ['value1a', 'value1b', 'value1c']) + self.assertEqual(cr.items('section1'), + [('option1', 'value1c'), + ('other_option1', 'other_value1')]) + self.assertEqual(cr.items_all('section1'), + [('option1', ['value1a', 'value1b', 'value1c']), + ('other_option1', ['other_value1'])]) + + def test_setlast(self): + # Test directly, not covered by higher-level tests. + omd = _OMD() + omd.setlast('key', 'value1') + self.assertEqual(omd['key'], 'value1') + self.assertEqual(omd.getall('key'), ['value1']) + omd.setlast('key', 'value2') + self.assertEqual(omd['key'], 'value2') + self.assertEqual(omd.getall('key'), ['value2']) |