| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -29,5 +29,6 @@ Contributors are: | |||
| 29 | 29 | -Tim Swast <swast _at_ google.com> | |
| 30 | 30 | -William Luc Ritchie | |
| 31 | 31 | -David Host <hostdm _at_ outlook.com> | |
| 32 | + -A. Jesse Jiryu Davis <jesse _at_ emptysquare.net> | ||
| 32 | 33 | ||
| 33 | 34 | Portions derived from other open source works and are clearly marked. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -146,6 +146,51 @@ def __exit__(self, exception_type, exception_value, traceback): | |||
| 146 | 146 | self._config.__exit__(exception_type, exception_value, traceback) | |
| 147 | 147 | ||
| 148 | 148 | ||
| 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 | + | ||
| 149 | 194 | class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, object)): | |
| 150 | 195 | ||
| 151 | 196 | """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): | |||
| 200 | 245 | contents into ours. This makes it impossible to write back an individual configuration file. | |
| 201 | 246 | Thus, if you want to modify a single configuration file, turn this off to leave the original | |
| 202 | 247 | dataset unaltered when reading it.""" | |
| 203 | - cp.RawConfigParser.__init__(self, dict_type=OrderedDict) | ||
| 248 | + cp.RawConfigParser.__init__(self, dict_type=_OMD) | ||
| 204 | 249 | ||
| 205 | 250 | # Used in python 3, needs to stay in sync with sections for underlying implementation to work | |
| 206 | 251 | if not hasattr(self, '_proxies'): | |
@@ -348,7 +393,8 @@ def string_decode(v): | |||
| 348 | 393 | is_multi_line = True | |
| 349 | 394 | optval = string_decode(optval[1:]) | |
| 350 | 395 | # end handle multi-line | |
| 351 | - cursect[optname] = optval | ||
| 396 | + # preserves multiple values for duplicate optnames | ||
| 397 | + cursect.add(optname, optval) | ||
| 352 | 398 | else: | |
| 353 | 399 | # check if it's an option with no value - it's just ignored by git | |
| 354 | 400 | if not self.OPTVALUEONLY.match(line): | |
@@ -362,7 +408,8 @@ def string_decode(v): | |||
| 362 | 408 | is_multi_line = False | |
| 363 | 409 | line = line[:-1] | |
| 364 | 410 | # end handle quotations | |
| 365 | - cursect[optname] += string_decode(line) | ||
| 411 | + optval = cursect.getlast(optname) | ||
| 412 | + cursect.setlast(optname, optval + string_decode(line)) | ||
| 366 | 413 | # END parse section or option | |
| 367 | 414 | # END while reading | |
| 368 | 415 | ||
@@ -442,9 +489,12 @@ def _write(self, fp): | |||
| 442 | 489 | git compatible format""" | |
| 443 | 490 | def write_section(name, section_dict): | |
| 444 | 491 | 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)) | ||
| 448 | 498 | # END if key is not __name__ | |
| 449 | 499 | # END section writing | |
| 450 | 500 | ||
@@ -457,6 +507,22 @@ def items(self, section_name): | |||
| 457 | 507 | """:return: list((option, value), ...) pairs of all items in the given section""" | |
| 458 | 508 | return [(k, v) for k, v in super(GitConfigParser, self).items(section_name) if k != '__name__'] | |
| 459 | 509 | ||
| 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 | + | ||
| 460 | 526 | @needs_values | |
| 461 | 527 | def write(self): | |
| 462 | 528 | """Write changes to our file, if there are changes at all | |
@@ -508,7 +574,11 @@ def read_only(self): | |||
| 508 | 574 | return self._read_only | |
| 509 | 575 | ||
| 510 | 576 | 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 | + | ||
| 512 | 582 | :param default: | |
| 513 | 583 | If not None, the given default value will be returned in case | |
| 514 | 584 | the option did not exist | |
@@ -523,6 +593,31 @@ def get_value(self, section, option, default=None): | |||
| 523 | 593 | return default | |
| 524 | 594 | raise | |
| 525 | 595 | ||
| 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): | ||
| 526 | 621 | types = (int, float) | |
| 527 | 622 | for numtype in types: | |
| 528 | 623 | try: | |
@@ -545,7 +640,9 @@ def get_value(self, section, option, default=None): | |||
| 545 | 640 | return True | |
| 546 | 641 | ||
| 547 | 642 | 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) | ||
| 549 | 646 | ||
| 550 | 647 | return valuestr | |
| 551 | 648 | ||
@@ -572,6 +669,25 @@ def set_value(self, section, option, value): | |||
| 572 | 669 | self.set(section, option, self._value_to_string(value)) | |
| 573 | 670 | return self | |
| 574 | 671 | ||
| 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 | + | ||
| 575 | 691 | def rename_section(self, section, new_name): | |
| 576 | 692 | """rename the given section to new_name | |
| 577 | 693 | :raise ValueError: if section doesn't exit | |
@@ -584,8 +700,9 @@ def rename_section(self, section, new_name): | |||
| 584 | 700 | raise ValueError("Destination section '%s' already exists" % new_name) | |
| 585 | 701 | ||
| 586 | 702 | 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) | ||
| 589 | 706 | # end for each value to copy | |
| 590 | 707 | ||
| 591 | 708 | # This call writes back the changes, which is why we don't have the respective decorator | |
| Original file line number | Diff line number | Diff 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 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -11,7 +11,7 @@ | |||
| 11 | 11 | GitConfigParser | |
| 12 | 12 | ) | |
| 13 | 13 | from git.compat import string_types | |
| 14 | - from git.config import cp | ||
| 14 | + from git.config import _OMD, cp | ||
| 15 | 15 | from git.test.lib import ( | |
| 16 | 16 | TestCase, | |
| 17 | 17 | fixture_path, | |
@@ -265,3 +265,110 @@ def test_empty_config_value(self): | |||
| 265 | 265 | ||
| 266 | 266 | with self.assertRaises(cp.NoOptionError): | |
| 267 | 267 | 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']) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments