Package gavo :: Package utils :: Module fancyconfig
[frames] | no frames]

Source Code for Module gavo.utils.fancyconfig

  1  r""" 
  2  A wrapper around ConfigParser that defines syntax and types within 
  3  the configuration options. 
  4   
  5  This tries to do for configuration processing what optparse did for 
  6  command line option processing: A declarative way of handling the main 
  7  chores. 
  8   
  9  The idea is that, in a client program, you say something like:: 
 10   
 11          from pftf.fancyconfig import (Configuration, Section, ConfigError, 
 12                  ...(items you want)...) 
 13           
 14          _config = Config( 
 15                  Section(... 
 16                          XYConfigItem(...) 
 17                  ), 
 18                  Section(... 
 19                  ... 
 20                  ) 
 21          ) 
 22                           
 23          get = _config.get 
 24          set = _config.set 
 25                           
 26          if __name__=="__main__": 
 27                  print fancyconfig.makeTxtDocs(_config) 
 28          else: 
 29                  try: 
 30                          fancyconfig.readConfiguration(_config, None,  
 31                                  os.path.join(dataDir, "config")) 
 32                  except ConfigError, msg: 
 33                          import sys 
 34                          sys.stderr.write("%s: %s\n"%( 
 35                                  sys.argv[0], unicode(msg).encode("utf-8"))) 
 36                          sys.exit(0) 
 37   
 38  and be done with most of it. 
 39   
 40  For examples of how this is used, see pftf (http://www.tfiu.de/pftf) 
 41  or pysmap (link coming up). 
 42  """ 
 43   
 44  #c Copyright 2008-2019, the GAVO project 
 45  #c 
 46  #c This program is free software, covered by the GNU GPL.  See the 
 47  #c COPYING file in the source distribution. 
 48   
 49   
 50  try: 
 51          import configparser 
 52  except ImportError: 
 53          # python2 compatibility 
 54          import ConfigParser as configparser 
 55  import re 
 56  import os 
 57  import tempfile 
 58  import warnings 
 59  import weakref 
 60   
 61  defaultSection = "general"  # must be all lowercase 
 62   
 63  # Set to true if avoid making a bad config item bomb out 
 64  BAD_CONFIG_ITEM_JUST_WARNS = False 
 65   
 66   
67 -class ConfigError(Exception):
68 """is the base class of the user visible exceptions from this module. 69 """ 70 fileName = "<internal>"
71
72 -class ParseError(ConfigError):
73 """is raised by ConfigItem's parse methods if there is a problem with 74 the input. 75 76 These should only escape to users of this module unless they call 77 ConfigItem.set themselves (which they shouldn't). 78 """
79
80 -class NoConfigItem(ConfigError):
81 """is raised by Configuration if a non-existing configuration 82 item is set or requested. 83 """
84
85 -class BadConfigValue(ConfigError):
86 """is raised by getConfiguration when there is a syntax error or the 87 like in a value. 88 89 The error message gives a hint at the reason of the error and is intended 90 for human consumption. 91 """
92
93 -class SyntaxError(ConfigError):
94 """is raised when the input file syntax is bad (i.e., on 95 configparser.ParsingErrors) 96 """
97 98
99 -class ConfigItem(object):
100 """A description of a configuration item including methods 101 to parse and unparse them. 102 103 This class is an abstract base class for options with real syntax 104 (_parse and _unparse methods). 105 106 ConfigItems have a section and a name (as in configparser), a 107 value (that defaults to default), an origin (which is "default", 108 if the value has not been changed and otherwise can be freely 109 used by clients), and a description. The origin is important 110 for distinguishing what to save. 111 112 You need to define the _parse and _unparse methods in deriving 113 classes. The _parse methods must take a byte string (the encoding 114 has to be utf-8) and return anything or raise ParseErrors (with 115 a sensible description of the problem) if there is a problem 116 with the input; the must not raise other exceptions when passed a 117 string (but may do anything when passed something else. _unparse 118 methods must not raise exceptions, take a value as returned 119 by parse (nothing else must be passed in) and return a 120 string that _parse would parse into this value. 121 122 Thus, the set method *only* takes strings as values. To set 123 parsed values, assign to value directly. However, _unparse 124 methods are not required to cope with any crazy stuff you enter 125 in this way, and thus you suddenly may receive all kinds of 126 funny exceptions when serializing a Configuration. 127 128 Inheriting classes need to specify a class attribute default that 129 kicks in when no default has been specified during construction. 130 These must be strings parseable by _parse. 131 132 Finally, you should provide a typedesc class attribute, a description 133 of the type intended for human consumption. See the documentation 134 functions below to get an idea how the would be shown. 135 """ 136 137 typedesc = "unspecified value" 138
139 - def __init__(self, name, default=None, description="Undocumented"):
140 self.name = name 141 if default is None: 142 default = self.default 143 self.default = default 144 self.set(default, "default") 145 self.description = description 146 self.parent = None # will be set on adoption by a Configuration
147
148 - def set(self, value, origin="user"):
149 self.value, self.origin = self._parse(value), origin
150
151 - def getAsString(self):
152 return self._unparse(self.value)
153
154 - def _parse(self, value):
155 raise ParseError("Internal error: Base config item used.")
156
157 - def _unparse(self, value):
158 return value
159 160
161 -class StringConfigItem(ConfigItem):
162 """A config item containing unicode strings. 163 164 The serialization of the config file is supposed to be utf-8. 165 166 The special value None is used as a Null value literal. 167 168 Tests are below. 169 """ 170 171 typedesc = "string" 172 default = "" 173
174 - def _parse(self, value):
175 if value=="None": 176 return None 177 if isinstance(value, unicode): 178 return value 179 try: 180 return value.decode("utf-8") 181 except UnicodeError: 182 raise ParseError("Not a valid utf-8 string: %s"%repr(value)) 183 except AttributeError: 184 raise ParseError("Only strings are allowed in %s, not %s"%( 185 self.__class__.__name__, repr(value)))
186
187 - def _unparse(self, value):
188 if value is None: 189 return "None" 190 return value.encode("utf-8")
191 192
193 -class BytestringConfigItem(ConfigItem):
194 """A config item containing byte strings. No characters outside 195 of ASCII are allowed. 196 """ 197 198 typedesc = "ASCII string" 199 default = "" 200
201 - def _parse(self, value):
202 if value=="None": 203 return None 204 return str(value)
205
206 - def _unparse(self, value):
207 return str(value)
208 209
210 -class IntConfigItem(ConfigItem):
211 """A config item containing an integer. 212 213 It supports a Null value through the special None literal. 214 215 >>> ci = IntConfigItem("foo"); print ci.value 216 None 217 >>> ci = IntConfigItem("foo", default="23"); ci.value 218 23 219 >>> ci.set("42"); ci.value 220 42 221 >>> ci.getAsString() 222 '42' 223 """ 224 225 typedesc = "integer" 226 default = "None" 227
228 - def _parse(self, value):
229 if value=="None": 230 return None 231 try: 232 return int(value) 233 except ValueError: 234 raise ParseError("%s is not an integer literal"%value)
235
236 - def _unparse(self, value):
237 return str(value)
238 239
240 -class FloatConfigItem(ConfigItem):
241 """A config item containing a float. 242 243 It supports a Null value through the special None literal. 244 245 >>> ci = FloatConfigItem("foo"); print ci.value 246 None 247 >>> ci = FloatConfigItem("foo", default="23"); ci.value 248 23.0 249 >>> ci.set("42.25"); ci.value 250 42.25 251 >>> ci.getAsString() 252 '42.25' 253 """ 254 255 typedesc = "floating point value" 256 default = "None" 257
258 - def _parse(self, value):
259 if value=="None": 260 return None 261 try: 262 return float(value) 263 except ValueError: 264 raise ParseError("%s is not an floating point literal"%value)
265
266 - def _unparse(self, value):
267 return repr(value)
268 269
270 -class ListConfigItem(StringConfigItem):
271 r"""A ConfigItem containing a list of strings, comma separated. 272 273 The values are space-normalized. Trailing whitespace-only items are 274 discarded, so "" is an empty list, "," is a list containing one 275 empty string. 276 277 There is currently no way to embed commas in the values. If that 278 should become necessary, I'd probably go for backslash escaping. 279 280 >>> ci = ListConfigItem("foo"); ci.value, ci.getAsString() 281 ([], '') 282 >>> ci.set(ci.getAsString());ci.value 283 [] 284 >>> ci.set("3, 2, 1, Z\xc3\xbcndung"); ci.value, ci.getAsString() 285 ([u'3', u'2', u'1', u'Z\xfcndung'], '3, 2, 1, Z\xc3\xbcndung, ') 286 >>> ci.set(",");ci.value 287 [u''] 288 >>> ci.set(ci.getAsString());ci.value 289 [u''] 290 """ 291 292 typedesc = "list of strings" 293 default = "" 294
295 - def _parse(self, value):
296 res = [s.strip() 297 for s in StringConfigItem._parse(self, value).split(",")] 298 if not res[-1]: 299 del res[-1] 300 return res
301
302 - def _unparse(self, value):
303 return StringConfigItem._unparse(self, ", ".join(value+[""]))
304 305
306 -class SetConfigItem(ListConfigItem):
307 """A set-valued ListConfigItem for quick existence lookups. 308 """ 309 typedesc = "set of strings" 310
311 - def _parse(self, value):
312 return set(ListConfigItem._parse(self, value))
313 314
315 -class IntListConfigItem(ListConfigItem):
316 """A ConfigItem containing a comma separated list of ints. 317 318 Literal handling is analoguos to ListConfigItem. 319 320 >>> ci = IntListConfigItem("foo"); ci.value, ci.getAsString() 321 ([], '') 322 >>> ci.set("3,2, 1"); ci.value, ci.getAsString() 323 ([3, 2, 1], '3, 2, 1, ') 324 >>> ci.set(ci.getAsString()); ci.value 325 [3, 2, 1] 326 >>> ci.set("1, 2, 3, rubbish") 327 Traceback (most recent call last): 328 ParseError: Non-integer in integer list 329 """ 330 331 typedesc = "list of integers" 332 default = "" 333
334 - def _parse(self, value):
335 try: 336 return [int(s) for s in ListConfigItem._parse(self, value)] 337 except ValueError: 338 raise ParseError("Non-integer in integer list")
339
340 - def _unparse(self, value):
341 return ListConfigItem._unparse(self, [str(n) for n in value])
342 343
344 -class IntSetConfigItem(IntListConfigItem):
345 """A set-valued IntListConfigItem for fast existence lookups. 346 """ 347 typedesc = "set of integers" 348
349 - def _parse(self, value):
350 return set(IntListConfigItem._parse(self, value))
351 352
353 -class DictConfigItem(ListConfigItem):
354 r"""A config item that contains a concise representation of 355 a string-string mapping. 356 357 The literal format is {<key>:<value>,}, where whitespace is ignored 358 between tokens and the last comma may be ommitted. 359 360 No commas and colons are allowed within keys and values. To lift this, 361 I'd probably go for backslash escaping. 362 363 >>> ci = DictConfigItem("foo"); ci.value 364 {} 365 >>> ci.set("ab:cd, foo:Fu\xc3\x9f"); ci.value 366 {u'ab': u'cd', u'foo': u'Fu\xdf'} 367 >>> ci.getAsString();ci.set(ci.getAsString()); ci.value 368 'ab:cd, foo:Fu\xc3\x9f, ' 369 {u'ab': u'cd', u'foo': u'Fu\xdf'} 370 >>> ci.set("ab:cd, rubbish") 371 Traceback (most recent call last): 372 ParseError: 'rubbish' is not a valid mapping literal element 373 """ 374 375 typedesc = "mapping" 376 default = "" 377
378 - def _parse(self, value):
379 res = {} 380 for item in ListConfigItem._parse(self, value): 381 try: 382 k, v = item.split(":") 383 res[k.strip()] = v.strip() 384 except ValueError: 385 raise ParseError("'%s' is not a valid mapping literal element"% 386 item) 387 return res
388
389 - def _unparse(self, value):
390 return ListConfigItem._unparse(self, 391 ["%s:%s"%(k, v) for k, v in value.iteritems()])
392 393
394 -class BooleanConfigItem(ConfigItem):
395 """A config item that contains a boolean and can be parsed from 396 many fancy representations. 397 """ 398 399 typedesc = "boolean" 400 default = "False" 401 402 trueLiterals = set(["true", "yes", "t", "on", "enabled", "1"]) 403 falseLiterals = set(["false", "no", "f", "off", "disabled", "0"])
404 - def _parse(self, value):
405 value = value.lower() 406 if value in self.trueLiterals: 407 return True 408 elif value in self.falseLiterals: 409 return False 410 else: 411 raise ParseError("'%s' is no recognized boolean literal."%value)
412
413 - def _unparse(self, value):
414 return {True: "True", False: "False"}[value]
415 416
417 -class EnumeratedConfigItem(StringConfigItem):
418 """A ConfigItem taking string values out of a set of possible strings. 419 420 Use the keyword argument options to pass in the possible strings. 421 The first item becomes the default unless you give a default. 422 You must give a non-empty list of strings as options. 423 """ 424 425 typedesc = "value from a defined set" 426
427 - def __init__(self, name, default=None, description="Undocumented", 428 options=[]):
429 if default is None: 430 default = options[0] 431 self.options = set(options) 432 self.typedesc = "value from the list %s"%(", ".join(self.options)) 433 StringConfigItem.__init__(self, name, default, description)
434
435 - def _parse(self, value):
436 encVal = StringConfigItem._parse(self, value) 437 if encVal not in self.options: 438 raise ParseError("%s is not an allowed value. Choose one of" 439 " %s"%(value, ", ".join([o.encode("utf-8") for o in self.options]))) 440 return encVal
441 442
443 -class PathConfigItem(StringConfigItem):
444 """A ConfigItem for a unix shell-type path. 445 446 The individual items are separated by colons, ~ is replaced by the 447 current value of $HOME (or "/", if unset), and $<key> substitutions 448 are supported, with key having to point to a key in the defaultSection. 449 450 To embed a real $ sign, double it. 451 452 This is parented ConfigItem, i.e., it needs a Configuration parent 453 before its value can be accessed. 454 """ 455 456 typedesc = "shell-type path" 457
458 - def _parse(self, value):
459 self._unparsed = StringConfigItem._parse(self, value) 460 if self._unparsed is None: 461 return [] 462 else: 463 return [s.strip() for s in self._unparsed.split(":")]
464
465 - def _unparse(self, value):
466 return StringConfigItem._unparse(self, self._unparsed)
467
468 - def _getValue(self):
469 def resolveReference(mat): 470 if mat.group(1)=='$': 471 return '$' 472 else: 473 return self.parent.get(mat.group(1))
474 res = [] 475 for p in self._value: 476 if p.startswith("~"): 477 p = os.environ.get("HOME", "")+p[1:] 478 if '$' in p: 479 p = re.sub(r"\$(\w+)", resolveReference, p) 480 res.append(p) 481 return res
482
483 - def _setValue(self, val):
484 self._value = val
485 486 value = property(_getValue, _setValue) 487 488
489 -class PathRelativeConfigItem(StringConfigItem):
490 """A configuration item interpreted relative to a path 491 given in the general section. 492 493 Basically, this is a replacement for configparser's %(x)s interpolation. 494 In addition, we expand ~ in front of a value to the current value of 495 $HOME. 496 497 To enable general-type interpolation, override the baseKey class Attribute. 498 """ 499 baseKey = None 500 _value = "" 501
502 - def _getValue(self):
503 if self._value is None: 504 return None 505 if self._value.startswith("~"): 506 return os.environ.get("HOME", "/no_home")+self._value[1:] 507 if self.baseKey: 508 return os.path.join(self.parent.get(self.baseKey), self._value) 509 return self._value
510
511 - def _setValue(self, val):
512 self._value = val
513 514 value = property(_getValue, _setValue)
515 516
517 -class ExpandedPathConfigItem(StringConfigItem):
518 """A configuration item in that returns its value expandusered. 519 """
520 - def _parse(self, value):
521 val = StringConfigItem._parse(self, value) 522 if val is not None: 523 val = os.path.expanduser(val) 524 return val
525 526
527 -class _Undefined(object):
528 """A sentinel for section.get. 529 """
530
531 -class Section(object):
532 """A section within the configuration. 533 534 It is constructed with a name, a documentation, and the configuration 535 items. 536 537 They double as proxies between the configuration and their items 538 via the setParent method. 539 """
540 - def __init__(self, name, documentation, *items):
541 self.name, self.documentation = name, documentation 542 self.items = {} 543 for item in items: 544 self.items[item.name.lower()] = item
545
546 - def __iter__(self):
547 for name in sorted(self.items): 548 yield self.items[name]
549
550 - def getitem(self, name):
551 if name.lower() in self.items: 552 return self.items[name.lower()] 553 else: 554 raise NoConfigItem("No such configuration item: [%s] %s"%( 555 self.name, name))
556
557 - def get(self, name):
558 """returns the value of the configuration item name. 559 560 If it does not exist, a NoConfigItem exception will be raised. 561 """ 562 return self.getitem(name).value
563
564 - def set(self, name, value, origin="user"):
565 """set the value of the configuration item name. 566 567 value must always be a string, regardless of the item's actual type. 568 """ 569 try: 570 self.getitem(name).set(value, origin) 571 except NoConfigItem: 572 if BAD_CONFIG_ITEM_JUST_WARNS: 573 warnings.warn("Unknown configuration item [%s] %s ignored."%( 574 self.name, name)) 575 else: 576 raise
577
578 - def setParent(self, parent):
579 for item in self.items.values(): 580 item.parent = parent
581 582
583 -class DefaultSection(Section):
584 """is the default section, named by defaultSection above. 585 586 The only difference to Section is that you leave out the name. 587 """
588 - def __init__(self, documentation, *items):
590 591
592 -class MagicSection(Section):
593 """A section that creates new keys on the fly. 594 595 Use this a dictionary-like thing when successive edits are 596 necessary or the DictConfigItem becomes too unwieldy. 597 598 A MagicSection is constructed with the section name, an item 599 factory, which has to be a subclass of ConfigItem (you may 600 want to write a special constructor to provide documentation, 601 etc.), and defaults as a sequence of pairs of keys and values. 602 And there should be documentation, too, of course. 603 """
604 - def __init__(self, name, documentation="Undocumented", 605 itemFactory=StringConfigItem, defaults=[]):
606 self.itemFactory = itemFactory 607 items = [] 608 for key, value in defaults: 609 items.append(self.itemFactory(key)) 610 items[-1].set(value, origin="defaults") 611 Section.__init__(self, name, documentation, *items)
612
613 - def set(self, name, value, origin="user"):
614 if name not in self.items: 615 self.items[name.lower()] = self.itemFactory(name) 616 Section.set(self, name, value, origin)
617 618
619 -class Configuration(object):
620 """A collection of config Sections and provides an interface to access 621 them and their items. 622 623 You construct it with the Sections you want and then use the get 624 method to access their content. You can either use get(section, name) 625 or just get(name), which implies the defaultSection section defined 626 at the top (right now, "general"). 627 628 To read configuration items, use addFromFp. addFromFp should only 629 raise subclasses of ConfigError. 630 631 You can also set individual items using set. 632 633 The class follows the default behaviour of configparser in that section 634 and item names are lowercased. 635 636 Note that direct access to sections is not forbidden, but you have to 637 keep case mangling of keys into account when doing so. 638 """
639 - def __init__(self, *sections):
640 self.sections = {} 641 for section in sections: 642 self.sections[section.name.lower()] = section 643 section.setParent(weakref.proxy(self))
644
645 - def __iter__(self):
646 sectHeads = self.sections.keys() 647 if defaultSection in sectHeads: 648 sectHeads.remove(defaultSection) 649 yield self.sections[defaultSection] 650 for h in sorted(sectHeads): 651 yield self.sections[h]
652
653 - def getitem(self, arg1, arg2=None):
654 """returns the *item* described by section, name or just name. 655 """ 656 if arg2 is None: 657 section, name = defaultSection, arg1 658 else: 659 section, name = arg1, arg2 660 if section.lower() in self.sections: 661 return self.sections[section.lower()].getitem(name) 662 raise NoConfigItem("No such configuration item: [%s] %s"%( 663 section, name))
664
665 - def get(self, arg1, arg2=None, default=_Undefined):
666 try: 667 return self.getitem(arg1, arg2).value 668 except NoConfigItem: 669 if default is _Undefined: 670 raise 671 return default
672
673 - def set(self, arg1, arg2, arg3=None, origin="user"):
674 """sets a configuration item to a value. 675 676 arg1 can be a section, in which case arg2 is a key and arg3 is a 677 value; alternatively, if arg3 is not given, arg1 is a key in 678 the defaultSection, and arg2 is the value. 679 680 All arguments are strings that must be parseable by the referenced 681 item's _parse method. 682 683 Origin is a tag you can use to, e.g., determine what to save. 684 """ 685 if arg3 is None: 686 section, name, value = defaultSection, arg1, arg2 687 else: 688 section, name, value = arg1, arg2, arg3 689 690 if section.lower() in self.sections: 691 return self.sections[section.lower()].set(name, value, origin) 692 else: 693 raise NoConfigItem("No such configuration item: [%s] %s"%( 694 section, name))
695
696 - def addFromFp(self, fp, origin="user", fName="<internal>"):
697 """adds the config items in the file fp to self. 698 """ 699 p = configparser.SafeConfigParser() 700 try: 701 p.readfp(fp, fName) 702 except configparser.ParsingError as msg: 703 raise SyntaxError("Config syntax error in %s: %s"%(fName, 704 unicode(msg))) 705 sections = p.sections() 706 for section in sections: 707 for name, value in p.items(section): 708 try: 709 self.set(section, name, value, origin) 710 except ParseError as msg: 711 raise BadConfigValue("While parsing value of %s in section %s," 712 " file %s:\n%s"% 713 (name, section, fName, unicode(msg)))
714
715 - def getUserConfig(self):
716 """returns a configparser containing the user set config items. 717 """ 718 userConf = configparser.SafeConfigParser() 719 for section in self.sections.values(): 720 for item in section: 721 if item.origin=="user": 722 if not userConf.has_section(section.name): 723 userConf.add_section(section.name) 724 userConf.set(section.name, item.name, item.getAsString()) 725 return userConf
726
727 - def saveUserConfig(self, destName):
728 """writes the config items changed by the user to destName. 729 """ 730 uc = self.getUserConfig() 731 fd, tmpName = tempfile.mkstemp("temp", "", dir=os.path.dirname(destName)) 732 f = os.fdopen(fd, "w") 733 uc.write(f) 734 f.flush() 735 os.fsync(fd) 736 f.close() 737 os.rename(tmpName, destName)
738 739
740 -def _addToConfig(config, fName, origin):
741 """adds the config items in the file named in fName to the Configuration, 742 tagging them with origin. 743 744 fName can be None or point to a non-exisiting file. In both cases, 745 the function does nothing. 746 """ 747 if not fName or not os.path.exists(fName): 748 return 749 f = open(fName) 750 config.addFromFp(f, origin=origin, fName=fName) 751 f.close()
752 753
754 -def readConfiguration(config, systemFName, userFName):
755 """fills the Configuration config with values from the the two locations. 756 757 File names that are none or point to non-existing locations are 758 ignored. 759 """ 760 try: 761 _addToConfig(config, systemFName, "system") 762 except ConfigError as ex: 763 ex.fileName = systemFName 764 raise 765 try: 766 _addToConfig(config, userFName, "user") 767 except ConfigError as ex: 768 ex.fileName = userFName 769 raise
770 771
772 -def makeTxtDocs(config, underlineChar="."):
773 import textwrap 774 docs = [] 775 for section in config: 776 if isinstance(section, MagicSection): 777 hdr = "Magic Section [%s]"%(section.name) 778 body = (section.documentation+ 779 "\n\nThe items in this section are all of type %s. You can add keys" 780 " as required.\n"% 781 section.itemFactory.typedesc) 782 else: 783 hdr = "Section [%s]"%(section.name) 784 body = section.documentation 785 docs.append("\n%s\n%s\n\n%s\n"%(hdr, underlineChar*len(hdr), 786 textwrap.fill(body, width=72))) 787 for ci in section: 788 docs.append("* %s: %s; "%(ci.name, ci.typedesc)) 789 if ci.default is not None: 790 docs.append(" defaults to '%s' --"%ci.default) 791 docs.append(textwrap.fill(ci.description, width=72, initial_indent=" ", 792 subsequent_indent=" ")) 793 return "\n".join(docs)
794 795
796 -def _getTestSuite():
797 """returns a unittest suite for this module. 798 799 It's in-file since I want to keep the thing in a single file. 800 """ 801 import unittest 802 from cStringIO import StringIO 803 804 class TestConfigItems(unittest.TestCase): 805 """tests for individual config items. 806 """ 807 def testStringConfigItemDefaultArgs(self): 808 ci = StringConfigItem("foo") 809 self.assertEqual(ci.name, "foo") 810 self.assertEqual(ci.value, "") 811 self.assertEqual(ci.description, "Undocumented") 812 self.assertEqual(ci.origin, "default") 813 ci.set("bar", "user") 814 self.assertEqual(ci.value, "bar") 815 self.assertEqual(ci.origin, "user") 816 self.assertEqual(ci.name, "foo") 817 self.assertEqual(ci.getAsString(), "bar")
818 819 def testStringConfigItemNoDefaults(self): 820 ci = StringConfigItem("foo", default="quux", 821 description="An expressionist config item") 822 self.assertEqual(ci.name, "foo") 823 self.assertEqual(ci.value, "quux") 824 self.assertEqual(ci.description, "An expressionist config item") 825 self.assertEqual(ci.origin, "default") 826 ci.set("None", "user") 827 self.assertEqual(ci.value, None) 828 self.assertEqual(ci.origin, "user") 829 self.assertEqual(ci.getAsString(), "None") 830 831 def testStringConfigItemEncoding(self): 832 ci = StringConfigItem("foo", default='F\xc3\xbc\xc3\x9fe') 833 self.assertEqual(ci.value.encode("iso-8859-1"), 'F\xfc\xdfe') 834 self.assertEqual(ci.getAsString(), 'F\xc3\xbc\xc3\x9fe') 835 self.assertRaises(ParseError, ci.set, 'Fu\xdf') 836 837 def testIntConfigItem(self): 838 ci = IntConfigItem("foo") 839 self.assertEqual(ci.value, None) 840 ci = IntConfigItem("foo", default="0") 841 self.assertEqual(ci.value, 0) 842 ci.set("42") 843 self.assertEqual(ci.value, 42) 844 self.assertEqual(ci.getAsString(), "42") 845 846 def testBooleanConfigItem(self): 847 ci = BooleanConfigItem("foo", default="0") 848 self.assertEqual(ci.value, False) 849 self.assertEqual(ci.getAsString(), "False") 850 ci.set("true") 851 self.assertEqual(ci.value, True) 852 ci.set("on") 853 self.assertEqual(ci.value, True) 854 self.assertEqual(ci.getAsString(), "True") 855 self.assertRaises(ParseError, ci.set, "undecided") 856 857 def testEnumeratedConfigItem(self): 858 ci = EnumeratedConfigItem("foo", options=["bar", "foo", u"Fu\xdf"]) 859 self.assertEqual(ci.value, "bar") 860 self.assertRaises(ParseError, ci.set, "quux") 861 self.assertRaises(ParseError, ci.set, "gr\xc3\x9f") 862 ci.set('Fu\xc3\x9f') 863 self.assertEqual(ci.value, u"Fu\xdf") 864 self.assertEqual(ci.getAsString(), 'Fu\xc3\x9f') 865 866 867 def getSampleConfig(): 868 return Configuration( 869 DefaultSection("General Settings", 870 StringConfigItem("emptyDefault", description="is empty by default"), 871 StringConfigItem("fooDefault", default="foo", 872 description="is foo by default"),), 873 Section("types", "Various Types", 874 IntConfigItem("count", default="0", description= 875 "is an integer"), 876 ListConfigItem("enum", description="is a list", 877 default="foo, bar"), 878 IntListConfigItem("intenum", description="is a list of ints", 879 default="1,2,3"), 880 DictConfigItem("map", description="is a mapping", 881 default="intLit:1, floatLit:0.1, bla: wurg"),)) 882 883 884 class ReadConfigTest(unittest.TestCase): 885 """tests for reading complete configurations. 886 """ 887 def testDefaults(self): 888 config = getSampleConfig() 889 self.assertEqual(config.get("emptyDefault"), "") 890 self.assertEqual(config.get("fooDefault"), "foo") 891 self.assertEqual(config.get("types", "count"), 0) 892 self.assertEqual(config.get("types", "enum"), [u"foo", u"bar"]) 893 self.assertEqual(config.get("types", "intenum"), [1,2,3]) 894 self.assertEqual(config.get("types", "map"), {"intLit": "1", 895 "floatLit": "0.1", "bla": "wurg"}) 896 897 def testSetting(self): 898 config = getSampleConfig() 899 config.set("emptyDefault", "foo") 900 self.assertEqual(config.get("emptyDefault"), "foo") 901 self.assertEqual(config.getitem("emptydefault").origin, "user") 902 self.assertEqual(config.getitem("foodefault").origin, "default") 903 904 def testReading(self): 905 config = getSampleConfig() 906 config.addFromFp(StringIO("[general]\n" 907 "emptyDefault: bar\n" 908 "fooDefault: quux\n" 909 "[types]\n" 910 "count: 7\n" 911 "enum: one, two,three:3\n" 912 "intenum: 1, 1,3,3\n" 913 "map: Fu\xc3\x9f: y, x:Fu\xc3\x9f\n")) 914 self.assertEqual(config.get("emptyDefault"), "bar") 915 self.assertEqual(config.get("fooDefault"), "quux") 916 self.assertEqual(config.get("types", "count"), 7) 917 self.assertEqual(config.get("types", "enum"), ["one", "two", "three:3"]) 918 self.assertEqual(config.get("types", "intenum"), [1,1,3,3]) 919 self.assertEqual(config.get("types", "map"), {u'Fu\xdf': "y", 920 "x": u'Fu\xdf'}) 921 self.assertEqual(config.getitem("types", "map").origin, "user") 922 923 def testRaising(self): 924 config = getSampleConfig() 925 self.assertRaises(BadConfigValue, config.addFromFp, 926 StringIO("[types]\nintenum: brasel\n")) 927 self.assertRaises(SyntaxError, config.addFromFp, 928 StringIO("intenum: brasel\n")) 929 self.assertRaises(NoConfigItem, config.addFromFp, 930 StringIO("[types]\nnonexisting: True\n")) 931 self.assertRaises(ParseError, config.getitem("types", "count").set, 932 "abc") 933 934 935 class MagicFactoryTest(unittest.TestCase): 936 """tests for function of MagicFactories. 937 """ 938 def testMagic(self): 939 config = Configuration( 940 MagicSection("profiles", "Some magic Section", 941 defaults=(('a', 'b'), ('c', 'd')))) 942 self.assertEqual(config.get('profiles', 'c'), 'd') 943 self.assertRaises(NoConfigItem, config.get, 'profiles', 'd') 944 config.set('profiles', 'new', 'shining', origin="user") 945 item = config.getitem('profiles', 'new') 946 self.assertEqual(item.value, 'shining') 947 self.assertEqual(item.origin, 'user') 948 949 950 class UserConfigTest(unittest.TestCase): 951 """tests for extraction of user-supplied config items. 952 """ 953 def testNoUserConfig(self): 954 config = getSampleConfig() 955 cp = config.getUserConfig() 956 self.assertEqual(cp.sections(), []) 957 958 def testSomeUserConfig(self): 959 config = getSampleConfig() 960 config.set("emptyDefault", "not empty any more") 961 config.set("types", "count", "4") 962 config.set("types", "intenum", "3,2,1") 963 cp = config.getUserConfig() 964 self.assertEqual([s for s in sorted(cp.sections())], 965 ["general", "types"]) 966 self.assertEqual(len(cp.items("general")), 1) 967 self.assertEqual(len(cp.items("types")), 2) 968 self.assertEqual(cp.get("general", "emptyDefault"), "not empty any more") 969 self.assertEqual(cp.get("types", "count"), "4") 970 self.assertEqual(cp.get("types", "intenum"), "3, 2, 1, ") 971 972 l = locals() 973 tests = [l[name] for name in l 974 if isinstance(l[name], type) and issubclass(l[name], unittest.TestCase)] 975 loader = unittest.TestLoader() 976 suite = unittest.TestSuite([loader.loadTestsFromTestCase(t) 977 for t in tests]) 978 return suite 979 980
981 -def _test():
982 import fancyconfig, doctest, unittest 983 suite = _getTestSuite() 984 suite.addTest(doctest.DocTestSuite(fancyconfig)) 985 unittest.TextTestRunner().run(suite)
986 987 988 if __name__=="__main__": 989 _test() 990