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
45
46
47
48
49
50 try:
51 import configparser
52 except ImportError:
53
54 import ConfigParser as configparser
55 import re
56 import os
57 import tempfile
58 import warnings
59 import weakref
60
61 defaultSection = "general"
62
63
64 BAD_CONFIG_ITEM_JUST_WARNS = False
65
66
68 """is the base class of the user visible exceptions from this module.
69 """
70 fileName = "<internal>"
71
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
81 """is raised by Configuration if a non-existing configuration
82 item is set or requested.
83 """
84
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
94 """is raised when the input file syntax is bad (i.e., on
95 configparser.ParsingErrors)
96 """
97
98
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"):
147
148 - def set(self, value, origin="user"):
149 self.value, self.origin = self._parse(value), origin
150
152 return self._unparse(self.value)
153
155 raise ParseError("Internal error: Base config item used.")
156
159
160
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
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
191
192
194 """A config item containing byte strings. No characters outside
195 of ASCII are allowed.
196 """
197
198 typedesc = "ASCII string"
199 default = ""
200
205
208
209
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
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
238
239
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
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
268
269
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
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
304
305
307 """A set-valued ListConfigItem for quick existence lookups.
308 """
309 typedesc = "set of strings"
310
313
314
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
339
342
343
345 """A set-valued IntListConfigItem for fast existence lookups.
346 """
347 typedesc = "set of integers"
348
351
352
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
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
392
393
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"])
412
414 return {True: "True", False: "False"}[value]
415
416
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=[]):
434
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
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
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
467
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
485
486 value = property(_getValue, _setValue)
487
488
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
510
513
514 value = property(_getValue, _setValue)
515
516
518 """A configuration item in that returns its value expandusered.
519 """
521 val = StringConfigItem._parse(self, value)
522 if val is not None:
523 val = os.path.expanduser(val)
524 return val
525
526
528 """A sentinel for section.get.
529 """
530
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):
545
547 for name in sorted(self.items):
548 yield self.items[name]
549
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
579 for item in self.items.values():
580 item.parent = parent
581
582
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
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 """
612
613 - def set(self, name, value, origin="user"):
617
618
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 """
640 self.sections = {}
641 for section in sections:
642 self.sections[section.name.lower()] = section
643 section.setParent(weakref.proxy(self))
644
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):
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
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
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
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
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
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
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
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