1from iniparse import compat as ConfigParser
2from six import StringIO
3try:
4    import UserDict
5except ImportError:
6    import collections as UserDict
7import unittest
8
9import sys
10if sys.version_info[0] < 3:
11    from test import test_support
12else:
13    from test import support as test_support
14
15
16class SortedDict(UserDict.UserDict):
17    def items(self):
18        result = self.data.items()
19        result.sort()
20        return result
21
22    def keys(self):
23        result = self.data.keys()
24        result.sort()
25        return result
26
27    def values(self):
28        result = self.items()
29        return [i[1] for i in result]
30
31    def iteritems(self):
32        return iter(self.items())
33
34    def iterkeys(self):
35        return iter(self.keys())
36
37    __iter__ = iterkeys
38
39    def itervalues(self):
40        return iter(self.values())
41
42
43class TestCaseBase(unittest.TestCase):
44    def newconfig(self, defaults=None):
45        if defaults is None:
46            self.cf = self.config_class()
47        else:
48            self.cf = self.config_class(defaults)
49        return self.cf
50
51    def fromstring(self, string, defaults=None):
52        cf = self.newconfig(defaults)
53        sio = StringIO(string)
54        cf.readfp(sio)
55        return cf
56
57    def test_basic(self):
58        cf = self.fromstring(
59            "[Foo Bar]\n"
60            "foo=bar\n"
61            "[Spacey Bar]\n"
62            "foo = bar\n"
63            "[Commented Bar]\n"
64            "foo: bar ; comment\n"
65            "[Long Line]\n"
66            "foo: this line is much, much longer than my editor\n"
67            "   likes it.\n"
68            "[Section\\with$weird%characters[\t]\n"
69            "[Internationalized Stuff]\n"
70            "foo[bg]: Bulgarian\n"
71            "foo=Default\n"
72            "foo[en]=English\n"
73            "foo[de]=Deutsch\n"
74            "[Spaces]\n"
75            "key with spaces : value\n"
76            "another with spaces = splat!\n"
77            )
78        L = cf.sections()
79        L.sort()
80        eq = self.assertEqual
81        eq(L, [r'Commented Bar',
82               r'Foo Bar',
83               r'Internationalized Stuff',
84               r'Long Line',
85               r'Section\with$weird%characters[' '\t',
86               r'Spaces',
87               r'Spacey Bar',
88               ])
89
90        # The use of spaces in the section names serves as a
91        # regression test for SourceForge bug #583248:
92        # http://www.python.org/sf/583248
93        eq(cf.get('Foo Bar', 'foo'), 'bar')
94        eq(cf.get('Spacey Bar', 'foo'), 'bar')
95        eq(cf.get('Commented Bar', 'foo'), 'bar')
96        eq(cf.get('Spaces', 'key with spaces'), 'value')
97        eq(cf.get('Spaces', 'another with spaces'), 'splat!')
98
99        self.failIf('__name__' in cf.options("Foo Bar"),
100                    '__name__ "option" should not be exposed by the API!')
101
102        # Make sure the right things happen for remove_option();
103        # added to include check for SourceForge bug #123324:
104        self.failUnless(cf.remove_option('Foo Bar', 'foo'),
105                        "remove_option() failed to report existance of option")
106        self.failIf(cf.has_option('Foo Bar', 'foo'),
107                    "remove_option() failed to remove option")
108        self.failIf(cf.remove_option('Foo Bar', 'foo'),
109                    "remove_option() failed to report non-existance of option"
110                    " that was removed")
111
112        self.assertRaises(ConfigParser.NoSectionError,
113                          cf.remove_option, 'No Such Section', 'foo')
114
115        eq(cf.get('Long Line', 'foo'),
116           'this line is much, much longer than my editor\nlikes it.')
117
118    def test_case_sensitivity(self):
119        cf = self.newconfig()
120        cf.add_section("A")
121        cf.add_section("a")
122        L = cf.sections()
123        L.sort()
124        eq = self.assertEqual
125        eq(L, ["A", "a"])
126        cf.set("a", "B", "value")
127        eq(cf.options("a"), ["b"])
128        eq(cf.get("a", "b"), "value",
129           "could not locate option, expecting case-insensitive option names")
130        self.failUnless(cf.has_option("a", "b"))
131        cf.set("A", "A-B", "A-B value")
132        for opt in ("a-b", "A-b", "a-B", "A-B"):
133            self.failUnless(
134                cf.has_option("A", opt),
135                "has_option() returned false for option which should exist")
136        eq(cf.options("A"), ["a-b"])
137        eq(cf.options("a"), ["b"])
138        cf.remove_option("a", "B")
139        eq(cf.options("a"), [])
140
141        # SF bug #432369:
142        cf = self.fromstring(
143            "[MySection]\nOption: first line\n\tsecond line\n")
144        eq(cf.options("MySection"), ["option"])
145        eq(cf.get("MySection", "Option"), "first line\nsecond line")
146
147        # SF bug #561822:
148        cf = self.fromstring("[section]\nnekey=nevalue\n",
149                             defaults={"key":"value"})
150        self.failUnless(cf.has_option("section", "Key"))
151
152    def test_default_case_sensitivity(self):
153        cf = self.newconfig({"foo": "Bar"})
154        self.assertEqual(
155            cf.get("DEFAULT", "Foo"), "Bar",
156            "could not locate option, expecting case-insensitive option names")
157        cf = self.newconfig({"Foo": "Bar"})
158        self.assertEqual(
159            cf.get("DEFAULT", "Foo"), "Bar",
160            "could not locate option, expecting case-insensitive defaults")
161
162    def test_parse_errors(self):
163        self.newconfig()
164        self.parse_error(ConfigParser.ParsingError,
165                         "[Foo]\n  extra-spaces: splat\n")
166        self.parse_error(ConfigParser.ParsingError,
167                         "[Foo]\n  extra-spaces= splat\n")
168        self.parse_error(ConfigParser.ParsingError,
169                         "[Foo]\noption-without-value\n")
170        self.parse_error(ConfigParser.ParsingError,
171                         "[Foo]\n:value-without-option-name\n")
172        self.parse_error(ConfigParser.ParsingError,
173                         "[Foo]\n=value-without-option-name\n")
174        self.parse_error(ConfigParser.MissingSectionHeaderError,
175                         "No Section!\n")
176
177    def parse_error(self, exc, src):
178        sio = StringIO(src)
179        self.assertRaises(exc, self.cf.readfp, sio)
180
181    def test_query_errors(self):
182        cf = self.newconfig()
183        self.assertEqual(cf.sections(), [],
184                         "new ConfigParser should have no defined sections")
185        self.failIf(cf.has_section("Foo"),
186                    "new ConfigParser should have no acknowledged sections")
187        self.assertRaises(ConfigParser.NoSectionError,
188                          cf.options, "Foo")
189        self.assertRaises(ConfigParser.NoSectionError,
190                          cf.set, "foo", "bar", "value")
191        self.get_error(ConfigParser.NoSectionError, "foo", "bar")
192        cf.add_section("foo")
193        self.get_error(ConfigParser.NoOptionError, "foo", "bar")
194
195    def get_error(self, exc, section, option):
196        try:
197            self.cf.get(section, option)
198        except exc as e:
199            return e
200        else:
201            self.fail("expected exception type %s.%s"
202                      % (exc.__module__, exc.__name__))
203
204    def test_boolean(self):
205        cf = self.fromstring(
206            "[BOOLTEST]\n"
207            "T1=1\n"
208            "T2=TRUE\n"
209            "T3=True\n"
210            "T4=oN\n"
211            "T5=yes\n"
212            "F1=0\n"
213            "F2=FALSE\n"
214            "F3=False\n"
215            "F4=oFF\n"
216            "F5=nO\n"
217            "E1=2\n"
218            "E2=foo\n"
219            "E3=-1\n"
220            "E4=0.1\n"
221            "E5=FALSE AND MORE"
222            )
223        for x in range(1, 5):
224            self.failUnless(cf.getboolean('BOOLTEST', 't%d' % x))
225            self.failIf(cf.getboolean('BOOLTEST', 'f%d' % x))
226            self.assertRaises(ValueError,
227                              cf.getboolean, 'BOOLTEST', 'e%d' % x)
228
229    def test_weird_errors(self):
230        cf = self.newconfig()
231        cf.add_section("Foo")
232        self.assertRaises(ConfigParser.DuplicateSectionError,
233                          cf.add_section, "Foo")
234
235    def test_write(self):
236        cf = self.fromstring(
237            "[Long Line]\n"
238            "foo: this line is much, much longer than my editor\n"
239            "   likes it.\n"
240            "[DEFAULT]\n"
241            "foo: another very\n"
242            " long line"
243            )
244        output = StringIO()
245        cf.write(output)
246        self.assertEqual(
247            output.getvalue(),
248            "[Long Line]\n"
249            "foo: this line is much, much longer than my editor\n"
250            "   likes it.\n"
251            "[DEFAULT]\n"
252            "foo: another very\n"
253            " long line"
254            )
255
256    def test_set_string_types(self):
257        cf = self.fromstring("[sect]\n"
258                             "option1=foo\n")
259        # Check that we don't get an exception when setting values in
260        # an existing section using strings:
261        class mystr(str):
262            pass
263        cf.set("sect", "option1", "splat")
264        cf.set("sect", "option1", mystr("splat"))
265        cf.set("sect", "option2", "splat")
266        cf.set("sect", "option2", mystr("splat"))
267        try:
268            unicode
269        except NameError:
270            pass
271        else:
272            cf.set("sect", "option1", unicode("splat"))
273            cf.set("sect", "option2", unicode("splat"))
274
275    def test_read_returns_file_list(self):
276        file1 = test_support.findfile("cfgparser.1")
277        # check when we pass a mix of readable and non-readable files:
278        cf = self.newconfig()
279        parsed_files = cf.read([file1, "nonexistant-file"])
280        self.assertEqual(parsed_files, [file1])
281        self.assertEqual(cf.get("Foo Bar", "foo"), "newbar")
282        # check when we pass only a filename:
283        cf = self.newconfig()
284        parsed_files = cf.read(file1)
285        self.assertEqual(parsed_files, [file1])
286        self.assertEqual(cf.get("Foo Bar", "foo"), "newbar")
287        # check when we pass only missing files:
288        cf = self.newconfig()
289        parsed_files = cf.read(["nonexistant-file"])
290        self.assertEqual(parsed_files, [])
291        # check when we pass no files:
292        cf = self.newconfig()
293        parsed_files = cf.read([])
294        self.assertEqual(parsed_files, [])
295
296    # shared by subclasses
297    def get_interpolation_config(self):
298        return self.fromstring(
299            "[Foo]\n"
300            "bar=something %(with1)s interpolation (1 step)\n"
301            "bar9=something %(with9)s lots of interpolation (9 steps)\n"
302            "bar10=something %(with10)s lots of interpolation (10 steps)\n"
303            "bar11=something %(with11)s lots of interpolation (11 steps)\n"
304            "with11=%(with10)s\n"
305            "with10=%(with9)s\n"
306            "with9=%(with8)s\n"
307            "with8=%(With7)s\n"
308            "with7=%(WITH6)s\n"
309            "with6=%(with5)s\n"
310            "With5=%(with4)s\n"
311            "WITH4=%(with3)s\n"
312            "with3=%(with2)s\n"
313            "with2=%(with1)s\n"
314            "with1=with\n"
315            "\n"
316            "[Mutual Recursion]\n"
317            "foo=%(bar)s\n"
318            "bar=%(foo)s\n"
319            "\n"
320            "[Interpolation Error]\n"
321            "name=%(reference)s\n",
322            # no definition for 'reference'
323            defaults={"getname": "%(__name__)s"})
324
325    def check_items_config(self, expected):
326        cf = self.fromstring(
327            "[section]\n"
328            "name = value\n"
329            "key: |%(name)s| \n"
330            "getdefault: |%(default)s|\n"
331            "getname: |%(__name__)s|",
332            defaults={"default": "<default>"})
333        L = list(cf.items("section"))
334        L.sort()
335        self.assertEqual(L, expected)
336
337
338class ConfigParserTestCase(TestCaseBase):
339    config_class = ConfigParser.ConfigParser
340
341    def test_interpolation(self):
342        cf = self.get_interpolation_config()
343        eq = self.assertEqual
344        eq(cf.get("Foo", "getname"), "Foo")
345        eq(cf.get("Foo", "bar"), "something with interpolation (1 step)")
346        eq(cf.get("Foo", "bar9"),
347           "something with lots of interpolation (9 steps)")
348        eq(cf.get("Foo", "bar10"),
349           "something with lots of interpolation (10 steps)")
350        self.get_error(ConfigParser.InterpolationDepthError, "Foo", "bar11")
351
352    def test_interpolation_missing_value(self):
353        cf = self.get_interpolation_config()
354        e = self.get_error(ConfigParser.InterpolationError,
355                           "Interpolation Error", "name")
356        self.assertEqual(e.reference, "reference")
357        self.assertEqual(e.section, "Interpolation Error")
358        self.assertEqual(e.option, "name")
359
360    def test_items(self):
361        self.check_items_config([('default', '<default>'),
362                                 ('getdefault', '|<default>|'),
363                                 ('getname', '|section|'),
364                                 ('key', '|value|'),
365                                 ('name', 'value')])
366
367    def test_set_nonstring_types(self):
368        cf = self.newconfig()
369        cf.add_section('non-string')
370        cf.set('non-string', 'int', 1)
371        cf.set('non-string', 'list', [0, 1, 1, 2, 3, 5, 8, 13, '%('])
372        cf.set('non-string', 'dict', {'pi': 3.14159, '%(': 1,
373                                      '%(list)': '%(list)'})
374        cf.set('non-string', 'string_with_interpolation', '%(list)s')
375        self.assertEqual(cf.get('non-string', 'int', raw=True), 1)
376        self.assertRaises(TypeError, cf.get, 'non-string', 'int')
377        self.assertEqual(cf.get('non-string', 'list', raw=True),
378                         [0, 1, 1, 2, 3, 5, 8, 13, '%('])
379        self.assertRaises(TypeError, cf.get, 'non-string', 'list')
380        self.assertEqual(cf.get('non-string', 'dict', raw=True),
381                         {'pi': 3.14159, '%(': 1, '%(list)': '%(list)'})
382        self.assertRaises(TypeError, cf.get, 'non-string', 'dict')
383        self.assertEqual(cf.get('non-string', 'string_with_interpolation',
384                                raw=True), '%(list)s')
385        self.assertRaises(ValueError, cf.get, 'non-string',
386                          'string_with_interpolation', raw=False)
387
388
389class RawConfigParserTestCase(TestCaseBase):
390    config_class = ConfigParser.RawConfigParser
391
392    def test_interpolation(self):
393        cf = self.get_interpolation_config()
394        eq = self.assertEqual
395        eq(cf.get("Foo", "getname"), "%(__name__)s")
396        eq(cf.get("Foo", "bar"),
397           "something %(with1)s interpolation (1 step)")
398        eq(cf.get("Foo", "bar9"),
399           "something %(with9)s lots of interpolation (9 steps)")
400        eq(cf.get("Foo", "bar10"),
401           "something %(with10)s lots of interpolation (10 steps)")
402        eq(cf.get("Foo", "bar11"),
403           "something %(with11)s lots of interpolation (11 steps)")
404
405    def test_items(self):
406        self.check_items_config([('default', '<default>'),
407                                 ('getdefault', '|%(default)s|'),
408                                 ('getname', '|%(__name__)s|'),
409                                 ('key', '|%(name)s|'),
410                                 ('name', 'value')])
411
412    def test_set_nonstring_types(self):
413        cf = self.newconfig()
414        cf.add_section('non-string')
415        cf.set('non-string', 'int', 1)
416        cf.set('non-string', 'list', [0, 1, 1, 2, 3, 5, 8, 13])
417        cf.set('non-string', 'dict', {'pi': 3.14159})
418        self.assertEqual(cf.get('non-string', 'int'), 1)
419        self.assertEqual(cf.get('non-string', 'list'),
420                         [0, 1, 1, 2, 3, 5, 8, 13])
421        self.assertEqual(cf.get('non-string', 'dict'), {'pi': 3.14159})
422
423
424class SafeConfigParserTestCase(ConfigParserTestCase):
425    config_class = ConfigParser.SafeConfigParser
426
427    def test_safe_interpolation(self):
428        # See http://www.python.org/sf/511737
429        cf = self.fromstring("[section]\n"
430                             "option1=xxx\n"
431                             "option2=%(option1)s/xxx\n"
432                             "ok=%(option1)s/%%s\n"
433                             "not_ok=%(option2)s/%%s")
434        self.assertEqual(cf.get("section", "ok"), "xxx/%s")
435        self.assertEqual(cf.get("section", "not_ok"), "xxx/xxx/%s")
436
437    def test_set_malformatted_interpolation(self):
438        cf = self.fromstring("[sect]\n"
439                             "option1=foo\n")
440
441        self.assertEqual(cf.get('sect', "option1"), "foo")
442
443        self.assertRaises(ValueError, cf.set, "sect", "option1", "%foo")
444        self.assertRaises(ValueError, cf.set, "sect", "option1", "foo%")
445        self.assertRaises(ValueError, cf.set, "sect", "option1", "f%oo")
446
447        self.assertEqual(cf.get('sect', "option1"), "foo")
448
449    def test_set_nonstring_types(self):
450        cf = self.fromstring("[sect]\n"
451                             "option1=foo\n")
452        # Check that we get a TypeError when setting non-string values
453        # in an existing section:
454        self.assertRaises(TypeError, cf.set, "sect", "option1", 1)
455        self.assertRaises(TypeError, cf.set, "sect", "option1", 1.0)
456        self.assertRaises(TypeError, cf.set, "sect", "option1", object())
457        self.assertRaises(TypeError, cf.set, "sect", "option2", 1)
458        self.assertRaises(TypeError, cf.set, "sect", "option2", 1.0)
459        self.assertRaises(TypeError, cf.set, "sect", "option2", object())
460
461    def test_add_section_default_1(self):
462        cf = self.newconfig()
463        self.assertRaises(ValueError, cf.add_section, "default")
464
465    def test_add_section_default_2(self):
466        cf = self.newconfig()
467        self.assertRaises(ValueError, cf.add_section, "DEFAULT")
468
469
470class SortedTestCase(RawConfigParserTestCase):
471    def newconfig(self, defaults=None):
472        self.cf = self.config_class(defaults=defaults, dict_type=SortedDict)
473        return self.cf
474
475    def test_sorted(self):
476        self.fromstring("[b]\n"
477                        "o4=1\n"
478                        "o3=2\n"
479                        "o2=3\n"
480                        "o1=4\n"
481                        "[a]\n"
482                        "k=v\n")
483        output = StringIO()
484        self.cf.write(output)
485        self.assertEquals(output.getvalue(),
486                          "[a]\n"
487                          "k = v\n\n"
488                          "[b]\n"
489                          "o1 = 4\n"
490                          "o2 = 3\n"
491                          "o3 = 2\n"
492                          "o4 = 1\n\n")
493
494
495def test_main():
496    test_support.run_unittest(
497        ConfigParserTestCase,
498        RawConfigParserTestCase,
499        SafeConfigParserTestCase,
500        SortedTestCase
501    )
502
503
504class Suite(unittest.TestSuite):
505    def __init__(self):
506        unittest.TestSuite.__init__(self, [
507                unittest.makeSuite(RawConfigParserTestCase, 'test'),
508                unittest.makeSuite(ConfigParserTestCase, 'test'),
509                unittest.makeSuite(SafeConfigParserTestCase, 'test'),
510                # unittest.makeSuite(SortedTestCase, 'test')
511        ])
512
513
514if __name__ == "__main__":
515    test_main()
516