Merge branch 'multi-value' of https://github.com/ajdavis/GitPython in… · gitpython-developers/GitPython@5cd12a6 · GitHub
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

4 files changed

Lines changed: 243 additions & 11 deletions

File tree

AUTHORS

Lines changed: 1 addition & 0 deletions

git/config.py

Lines 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
Lines 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

Lines changed: 108 additions & 1 deletion

0 commit comments

Comments
 (0)