Skip to content

Commit 5cd12a6

Browse files
author
Sebastian Thiel
committed
Merge branch 'multi-value' of https://github.com/ajdavis/GitPython into ajdavis-multi-value
2 parents 6971a93 + 4106f18 commit 5cd12a6

File tree

4 files changed

+243
-11
lines changed

4 files changed

+243
-11
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,6 @@ Contributors are:
2929
-Tim Swast <swast _at_ google.com>
3030
-William Luc Ritchie
3131
-David Host <hostdm _at_ outlook.com>
32+
-A. Jesse Jiryu Davis <jesse _at_ emptysquare.net>
3233

3334
Portions derived from other open source works and are clearly marked.

git/config.py

+127-10
Original file line numberDiff line numberDiff line change
@@ -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+
149194
class 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

git/test/fixtures/git_config_multiple

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[section0]
2+
option0 = value0
3+
4+
[section1]
5+
option1 = value1a
6+
option1 = value1b
7+
other_option1 = other_value1

git/test/test_config.py

+108-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
GitConfigParser
1212
)
1313
from git.compat import string_types
14-
from git.config import cp
14+
from git.config import _OMD, cp
1515
from git.test.lib import (
1616
TestCase,
1717
fixture_path,
@@ -265,3 +265,110 @@ def test_empty_config_value(self):
265265

266266
with self.assertRaises(cp.NoOptionError):
267267
cr.get_value('color', 'ui')
268+
269+
def test_multiple_values(self):
270+
file_obj = self._to_memcache(fixture_path('git_config_multiple'))
271+
with GitConfigParser(file_obj, read_only=False) as cw:
272+
self.assertEqual(cw.get('section0', 'option0'), 'value0')
273+
self.assertEqual(cw.get_values('section0', 'option0'), ['value0'])
274+
self.assertEqual(cw.items('section0'), [('option0', 'value0')])
275+
276+
# Where there are multiple values, "get" returns the last.
277+
self.assertEqual(cw.get('section1', 'option1'), 'value1b')
278+
self.assertEqual(cw.get_values('section1', 'option1'),
279+
['value1a', 'value1b'])
280+
self.assertEqual(cw.items('section1'),
281+
[('option1', 'value1b'),
282+
('other_option1', 'other_value1')])
283+
self.assertEqual(cw.items_all('section1'),
284+
[('option1', ['value1a', 'value1b']),
285+
('other_option1', ['other_value1'])])
286+
with self.assertRaises(KeyError):
287+
cw.get_values('section1', 'missing')
288+
289+
self.assertEqual(cw.get_values('section1', 'missing', 1), [1])
290+
self.assertEqual(cw.get_values('section1', 'missing', 's'), ['s'])
291+
292+
def test_multiple_values_rename(self):
293+
file_obj = self._to_memcache(fixture_path('git_config_multiple'))
294+
with GitConfigParser(file_obj, read_only=False) as cw:
295+
cw.rename_section('section1', 'section2')
296+
cw.write()
297+
file_obj.seek(0)
298+
cr = GitConfigParser(file_obj, read_only=True)
299+
self.assertEqual(cr.get_value('section2', 'option1'), 'value1b')
300+
self.assertEqual(cr.get_values('section2', 'option1'),
301+
['value1a', 'value1b'])
302+
self.assertEqual(cr.items('section2'),
303+
[('option1', 'value1b'),
304+
('other_option1', 'other_value1')])
305+
self.assertEqual(cr.items_all('section2'),
306+
[('option1', ['value1a', 'value1b']),
307+
('other_option1', ['other_value1'])])
308+
309+
def test_multiple_to_single(self):
310+
file_obj = self._to_memcache(fixture_path('git_config_multiple'))
311+
with GitConfigParser(file_obj, read_only=False) as cw:
312+
cw.set_value('section1', 'option1', 'value1c')
313+
314+
cw.write()
315+
file_obj.seek(0)
316+
cr = GitConfigParser(file_obj, read_only=True)
317+
self.assertEqual(cr.get_value('section1', 'option1'), 'value1c')
318+
self.assertEqual(cr.get_values('section1', 'option1'), ['value1c'])
319+
self.assertEqual(cr.items('section1'),
320+
[('option1', 'value1c'),
321+
('other_option1', 'other_value1')])
322+
self.assertEqual(cr.items_all('section1'),
323+
[('option1', ['value1c']),
324+
('other_option1', ['other_value1'])])
325+
326+
def test_single_to_multiple(self):
327+
file_obj = self._to_memcache(fixture_path('git_config_multiple'))
328+
with GitConfigParser(file_obj, read_only=False) as cw:
329+
cw.add_value('section1', 'other_option1', 'other_value1a')
330+
331+
cw.write()
332+
file_obj.seek(0)
333+
cr = GitConfigParser(file_obj, read_only=True)
334+
self.assertEqual(cr.get_value('section1', 'option1'), 'value1b')
335+
self.assertEqual(cr.get_values('section1', 'option1'),
336+
['value1a', 'value1b'])
337+
self.assertEqual(cr.get_value('section1', 'other_option1'),
338+
'other_value1a')
339+
self.assertEqual(cr.get_values('section1', 'other_option1'),
340+
['other_value1', 'other_value1a'])
341+
self.assertEqual(cr.items('section1'),
342+
[('option1', 'value1b'),
343+
('other_option1', 'other_value1a')])
344+
self.assertEqual(
345+
cr.items_all('section1'),
346+
[('option1', ['value1a', 'value1b']),
347+
('other_option1', ['other_value1', 'other_value1a'])])
348+
349+
def test_add_to_multiple(self):
350+
file_obj = self._to_memcache(fixture_path('git_config_multiple'))
351+
with GitConfigParser(file_obj, read_only=False) as cw:
352+
cw.add_value('section1', 'option1', 'value1c')
353+
cw.write()
354+
file_obj.seek(0)
355+
cr = GitConfigParser(file_obj, read_only=True)
356+
self.assertEqual(cr.get_value('section1', 'option1'), 'value1c')
357+
self.assertEqual(cr.get_values('section1', 'option1'),
358+
['value1a', 'value1b', 'value1c'])
359+
self.assertEqual(cr.items('section1'),
360+
[('option1', 'value1c'),
361+
('other_option1', 'other_value1')])
362+
self.assertEqual(cr.items_all('section1'),
363+
[('option1', ['value1a', 'value1b', 'value1c']),
364+
('other_option1', ['other_value1'])])
365+
366+
def test_setlast(self):
367+
# Test directly, not covered by higher-level tests.
368+
omd = _OMD()
369+
omd.setlast('key', 'value1')
370+
self.assertEqual(omd['key'], 'value1')
371+
self.assertEqual(omd.getall('key'), ['value1'])
372+
omd.setlast('key', 'value2')
373+
self.assertEqual(omd['key'], 'value2')
374+
self.assertEqual(omd.getall('key'), ['value2'])

0 commit comments

Comments
 (0)