Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 5cd12a6

Browse filesBrowse 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
Copy full SHA for 5cd12a6

File tree

4 files changed

+243
-11
lines changed
Filter options

4 files changed

+243
-11
lines changed

‎AUTHORS

Copy file name to clipboardExpand all lines: AUTHORS
+1Lines changed: 1 addition & 0 deletions
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

Copy file name to clipboardExpand all lines: git/config.py
+127-10Lines changed: 127 additions & 10 deletions
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

Copy file name to clipboard
+7Lines changed: 7 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: git/test/test_config.py
+108-1Lines changed: 108 additions & 1 deletion
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.