diff options
Diffstat (limited to 'Lib/test/test_cfgparser.py')
| -rw-r--r-- | Lib/test/test_cfgparser.py | 1468 |
1 files changed, 1241 insertions, 227 deletions
diff --git a/Lib/test/test_cfgparser.py b/Lib/test/test_cfgparser.py index 299870c..1db9217 100644 --- a/Lib/test/test_cfgparser.py +++ b/Lib/test/test_cfgparser.py @@ -1,11 +1,16 @@ +import collections import configparser import io +import os +import sys +import textwrap import unittest -import collections +import warnings from test import support class SortedDict(collections.UserDict): + def items(self): return sorted(self.data.items()) @@ -15,72 +20,230 @@ class SortedDict(collections.UserDict): def values(self): return [i[1] for i in self.items()] - def iteritems(self): return iter(self.items()) - def iterkeys(self): return iter(self.keys()) + def iteritems(self): + return iter(self.items()) + + def iterkeys(self): + return iter(self.keys()) + + def itervalues(self): + return iter(self.values()) + __iter__ = iterkeys - def itervalues(self): return iter(self.values()) -class TestCaseBase(unittest.TestCase): + +class CfgParserTestCaseClass(unittest.TestCase): + allow_no_value = False + delimiters = ('=', ':') + comment_prefixes = (';', '#') + inline_comment_prefixes = (';', '#') + empty_lines_in_values = True + dict_type = configparser._default_dict + strict = False + default_section = configparser.DEFAULTSECT + interpolation = configparser._UNSET + def newconfig(self, defaults=None): - if defaults is None: - self.cf = self.config_class() - else: - self.cf = self.config_class(defaults) - return self.cf + arguments = dict( + defaults=defaults, + allow_no_value=self.allow_no_value, + delimiters=self.delimiters, + comment_prefixes=self.comment_prefixes, + inline_comment_prefixes=self.inline_comment_prefixes, + empty_lines_in_values=self.empty_lines_in_values, + dict_type=self.dict_type, + strict=self.strict, + default_section=self.default_section, + interpolation=self.interpolation, + ) + instance = self.config_class(**arguments) + return instance def fromstring(self, string, defaults=None): cf = self.newconfig(defaults) - sio = io.StringIO(string) - cf.readfp(sio) + cf.read_string(string) return cf - def test_basic(self): - cf = self.fromstring( - "[Foo Bar]\n" - "foo=bar\n" - "[Spacey Bar]\n" - "foo = bar\n" - "[Commented Bar]\n" - "foo: bar ; comment\n" - "[Long Line]\n" - "foo: this line is much, much longer than my editor\n" - " likes it.\n" - "[Section\\with$weird%characters[\t]\n" - "[Internationalized Stuff]\n" - "foo[bg]: Bulgarian\n" - "foo=Default\n" - "foo[en]=English\n" - "foo[de]=Deutsch\n" - "[Spaces]\n" - "key with spaces : value\n" - "another with spaces = splat!\n" - ) +class BasicTestCase(CfgParserTestCaseClass): + + def basic_test(self, cf): + E = ['Commented Bar', + 'Foo Bar', + 'Internationalized Stuff', + 'Long Line', + 'Section\\with$weird%characters[\t', + 'Spaces', + 'Spacey Bar', + 'Spacey Bar From The Beginning', + 'Types', + ] + + if self.allow_no_value: + E.append('NoValue') + E.sort() + F = [('baz', 'qwe'), ('foo', 'bar3')] + + # API access L = cf.sections() L.sort() eq = self.assertEqual - eq(L, [r'Commented Bar', - r'Foo Bar', - r'Internationalized Stuff', - r'Long Line', - r'Section\with$weird%characters[' '\t', - r'Spaces', - r'Spacey Bar', - ]) + eq(L, E) + L = cf.items('Spacey Bar From The Beginning') + L.sort() + eq(L, F) + + # mapping access + L = [section for section in cf] + L.sort() + E.append(self.default_section) + E.sort() + eq(L, E) + L = cf['Spacey Bar From The Beginning'].items() + L = sorted(list(L)) + eq(L, F) + L = cf.items() + L = sorted(list(L)) + self.assertEqual(len(L), len(E)) + for name, section in L: + eq(name, section.name) + eq(cf.defaults(), cf[self.default_section]) # The use of spaces in the section names serves as a # regression test for SourceForge bug #583248: # http://www.python.org/sf/583248 - eq(cf.get('Foo Bar', 'foo'), 'bar') - eq(cf.get('Spacey Bar', 'foo'), 'bar') - eq(cf.get('Commented Bar', 'foo'), 'bar') + + # API access + eq(cf.get('Foo Bar', 'foo'), 'bar1') + eq(cf.get('Spacey Bar', 'foo'), 'bar2') + eq(cf.get('Spacey Bar From The Beginning', 'foo'), 'bar3') + eq(cf.get('Spacey Bar From The Beginning', 'baz'), 'qwe') + eq(cf.get('Commented Bar', 'foo'), 'bar4') + eq(cf.get('Commented Bar', 'baz'), 'qwe') eq(cf.get('Spaces', 'key with spaces'), 'value') eq(cf.get('Spaces', 'another with spaces'), 'splat!') + eq(cf.getint('Types', 'int'), 42) + eq(cf.get('Types', 'int'), "42") + self.assertAlmostEqual(cf.getfloat('Types', 'float'), 0.44) + eq(cf.get('Types', 'float'), "0.44") + eq(cf.getboolean('Types', 'boolean'), False) + eq(cf.get('Types', '123'), 'strange but acceptable') + if self.allow_no_value: + eq(cf.get('NoValue', 'option-without-value'), None) - self.assertFalse('__name__' in cf.options("Foo Bar"), - '__name__ "option" should not be exposed by the API!') + # test vars= and fallback= + eq(cf.get('Foo Bar', 'foo', fallback='baz'), 'bar1') + eq(cf.get('Foo Bar', 'foo', vars={'foo': 'baz'}), 'baz') + with self.assertRaises(configparser.NoSectionError): + cf.get('No Such Foo Bar', 'foo') + with self.assertRaises(configparser.NoOptionError): + cf.get('Foo Bar', 'no-such-foo') + eq(cf.get('No Such Foo Bar', 'foo', fallback='baz'), 'baz') + eq(cf.get('Foo Bar', 'no-such-foo', fallback='baz'), 'baz') + eq(cf.get('Spacey Bar', 'foo', fallback=None), 'bar2') + eq(cf.get('No Such Spacey Bar', 'foo', fallback=None), None) + eq(cf.getint('Types', 'int', fallback=18), 42) + eq(cf.getint('Types', 'no-such-int', fallback=18), 18) + eq(cf.getint('Types', 'no-such-int', fallback="18"), "18") # sic! + with self.assertRaises(configparser.NoOptionError): + cf.getint('Types', 'no-such-int') + self.assertAlmostEqual(cf.getfloat('Types', 'float', + fallback=0.0), 0.44) + self.assertAlmostEqual(cf.getfloat('Types', 'no-such-float', + fallback=0.0), 0.0) + eq(cf.getfloat('Types', 'no-such-float', fallback="0.0"), "0.0") # sic! + with self.assertRaises(configparser.NoOptionError): + cf.getfloat('Types', 'no-such-float') + eq(cf.getboolean('Types', 'boolean', fallback=True), False) + eq(cf.getboolean('Types', 'no-such-boolean', fallback="yes"), + "yes") # sic! + eq(cf.getboolean('Types', 'no-such-boolean', fallback=True), True) + with self.assertRaises(configparser.NoOptionError): + cf.getboolean('Types', 'no-such-boolean') + eq(cf.getboolean('No Such Types', 'boolean', fallback=True), True) + if self.allow_no_value: + eq(cf.get('NoValue', 'option-without-value', fallback=False), None) + eq(cf.get('NoValue', 'no-such-option-without-value', + fallback=False), False) - # Make sure the right things happen for remove_option(); - # added to include check for SourceForge bug #123324: + # mapping access + eq(cf['Foo Bar']['foo'], 'bar1') + eq(cf['Spacey Bar']['foo'], 'bar2') + section = cf['Spacey Bar From The Beginning'] + eq(section.name, 'Spacey Bar From The Beginning') + self.assertIs(section.parser, cf) + with self.assertRaises(AttributeError): + section.name = 'Name is read-only' + with self.assertRaises(AttributeError): + section.parser = 'Parser is read-only' + eq(section['foo'], 'bar3') + eq(section['baz'], 'qwe') + eq(cf['Commented Bar']['foo'], 'bar4') + eq(cf['Commented Bar']['baz'], 'qwe') + eq(cf['Spaces']['key with spaces'], 'value') + eq(cf['Spaces']['another with spaces'], 'splat!') + eq(cf['Long Line']['foo'], + 'this line is much, much longer than my editor\nlikes it.') + if self.allow_no_value: + eq(cf['NoValue']['option-without-value'], None) + # test vars= and fallback= + eq(cf['Foo Bar'].get('foo', 'baz'), 'bar1') + eq(cf['Foo Bar'].get('foo', fallback='baz'), 'bar1') + eq(cf['Foo Bar'].get('foo', vars={'foo': 'baz'}), 'baz') + with self.assertRaises(KeyError): + cf['No Such Foo Bar']['foo'] + with self.assertRaises(KeyError): + cf['Foo Bar']['no-such-foo'] + with self.assertRaises(KeyError): + cf['No Such Foo Bar'].get('foo', fallback='baz') + eq(cf['Foo Bar'].get('no-such-foo', 'baz'), 'baz') + eq(cf['Foo Bar'].get('no-such-foo', fallback='baz'), 'baz') + eq(cf['Foo Bar'].get('no-such-foo'), None) + eq(cf['Spacey Bar'].get('foo', None), 'bar2') + eq(cf['Spacey Bar'].get('foo', fallback=None), 'bar2') + with self.assertRaises(KeyError): + cf['No Such Spacey Bar'].get('foo', None) + eq(cf['Types'].getint('int', 18), 42) + eq(cf['Types'].getint('int', fallback=18), 42) + eq(cf['Types'].getint('no-such-int', 18), 18) + eq(cf['Types'].getint('no-such-int', fallback=18), 18) + eq(cf['Types'].getint('no-such-int', "18"), "18") # sic! + eq(cf['Types'].getint('no-such-int', fallback="18"), "18") # sic! + eq(cf['Types'].getint('no-such-int'), None) + self.assertAlmostEqual(cf['Types'].getfloat('float', 0.0), 0.44) + self.assertAlmostEqual(cf['Types'].getfloat('float', + fallback=0.0), 0.44) + self.assertAlmostEqual(cf['Types'].getfloat('no-such-float', 0.0), 0.0) + self.assertAlmostEqual(cf['Types'].getfloat('no-such-float', + fallback=0.0), 0.0) + eq(cf['Types'].getfloat('no-such-float', "0.0"), "0.0") # sic! + eq(cf['Types'].getfloat('no-such-float', fallback="0.0"), "0.0") # sic! + eq(cf['Types'].getfloat('no-such-float'), None) + eq(cf['Types'].getboolean('boolean', True), False) + eq(cf['Types'].getboolean('boolean', fallback=True), False) + eq(cf['Types'].getboolean('no-such-boolean', "yes"), "yes") # sic! + eq(cf['Types'].getboolean('no-such-boolean', fallback="yes"), + "yes") # sic! + eq(cf['Types'].getboolean('no-such-boolean', True), True) + eq(cf['Types'].getboolean('no-such-boolean', fallback=True), True) + eq(cf['Types'].getboolean('no-such-boolean'), None) + if self.allow_no_value: + eq(cf['NoValue'].get('option-without-value', False), None) + eq(cf['NoValue'].get('option-without-value', fallback=False), None) + eq(cf['NoValue'].get('no-such-option-without-value', False), False) + eq(cf['NoValue'].get('no-such-option-without-value', + fallback=False), False) + + # Make sure the right things happen for remove_section() and + # remove_option(); added to include check for SourceForge bug #123324. + + cf[self.default_section]['this_value'] = '1' + cf[self.default_section]['that_value'] = '2' + + # API access + self.assertTrue(cf.remove_section('Spaces')) + self.assertFalse(cf.has_option('Spaces', 'key with spaces')) + self.assertFalse(cf.remove_section('Spaces')) + self.assertFalse(cf.remove_section(self.default_section)) self.assertTrue(cf.remove_option('Foo Bar', 'foo'), "remove_option() failed to report existence of option") self.assertFalse(cf.has_option('Foo Bar', 'foo'), @@ -88,26 +251,199 @@ class TestCaseBase(unittest.TestCase): self.assertFalse(cf.remove_option('Foo Bar', 'foo'), "remove_option() failed to report non-existence of option" " that was removed") + self.assertTrue(cf.has_option('Foo Bar', 'this_value')) + self.assertFalse(cf.remove_option('Foo Bar', 'this_value')) + self.assertTrue(cf.remove_option(self.default_section, 'this_value')) + self.assertFalse(cf.has_option('Foo Bar', 'this_value')) + self.assertFalse(cf.remove_option(self.default_section, 'this_value')) - self.assertRaises(configparser.NoSectionError, - cf.remove_option, 'No Such Section', 'foo') + with self.assertRaises(configparser.NoSectionError) as cm: + cf.remove_option('No Such Section', 'foo') + self.assertEqual(cm.exception.args, ('No Such Section',)) eq(cf.get('Long Line', 'foo'), 'this line is much, much longer than my editor\nlikes it.') + # mapping access + del cf['Types'] + self.assertFalse('Types' in cf) + with self.assertRaises(KeyError): + del cf['Types'] + with self.assertRaises(ValueError): + del cf[self.default_section] + del cf['Spacey Bar']['foo'] + self.assertFalse('foo' in cf['Spacey Bar']) + with self.assertRaises(KeyError): + del cf['Spacey Bar']['foo'] + self.assertTrue('that_value' in cf['Spacey Bar']) + with self.assertRaises(KeyError): + del cf['Spacey Bar']['that_value'] + del cf[self.default_section]['that_value'] + self.assertFalse('that_value' in cf['Spacey Bar']) + with self.assertRaises(KeyError): + del cf[self.default_section]['that_value'] + with self.assertRaises(KeyError): + del cf['No Such Section']['foo'] + + # Don't add new asserts below in this method as most of the options + # and sections are now removed. + + def test_basic(self): + config_string = """\ +[Foo Bar] +foo{0[0]}bar1 +[Spacey Bar] +foo {0[0]} bar2 +[Spacey Bar From The Beginning] + foo {0[0]} bar3 + baz {0[0]} qwe +[Commented Bar] +foo{0[1]} bar4 {1[1]} comment +baz{0[0]}qwe {1[0]}another one +[Long Line] +foo{0[1]} this line is much, much longer than my editor + likes it. +[Section\\with$weird%characters[\t] +[Internationalized Stuff] +foo[bg]{0[1]} Bulgarian +foo{0[0]}Default +foo[en]{0[0]}English +foo[de]{0[0]}Deutsch +[Spaces] +key with spaces {0[1]} value +another with spaces {0[0]} splat! +[Types] +int {0[1]} 42 +float {0[0]} 0.44 +boolean {0[0]} NO +123 {0[1]} strange but acceptable +""".format(self.delimiters, self.comment_prefixes) + if self.allow_no_value: + config_string += ( + "[NoValue]\n" + "option-without-value\n" + ) + cf = self.fromstring(config_string) + self.basic_test(cf) + if self.strict: + with self.assertRaises(configparser.DuplicateOptionError): + cf.read_string(textwrap.dedent("""\ + [Duplicate Options Here] + option {0[0]} with a value + option {0[1]} with another value + """.format(self.delimiters))) + with self.assertRaises(configparser.DuplicateSectionError): + cf.read_string(textwrap.dedent("""\ + [And Now For Something] + completely different {0[0]} True + [And Now For Something] + the larch {0[1]} 1 + """.format(self.delimiters))) + else: + cf.read_string(textwrap.dedent("""\ + [Duplicate Options Here] + option {0[0]} with a value + option {0[1]} with another value + """.format(self.delimiters))) + + cf.read_string(textwrap.dedent("""\ + [And Now For Something] + completely different {0[0]} True + [And Now For Something] + the larch {0[1]} 1 + """.format(self.delimiters))) + + def test_basic_from_dict(self): + config = { + "Foo Bar": { + "foo": "bar1", + }, + "Spacey Bar": { + "foo": "bar2", + }, + "Spacey Bar From The Beginning": { + "foo": "bar3", + "baz": "qwe", + }, + "Commented Bar": { + "foo": "bar4", + "baz": "qwe", + }, + "Long Line": { + "foo": "this line is much, much longer than my editor\nlikes " + "it.", + }, + "Section\\with$weird%characters[\t": { + }, + "Internationalized Stuff": { + "foo[bg]": "Bulgarian", + "foo": "Default", + "foo[en]": "English", + "foo[de]": "Deutsch", + }, + "Spaces": { + "key with spaces": "value", + "another with spaces": "splat!", + }, + "Types": { + "int": 42, + "float": 0.44, + "boolean": False, + 123: "strange but acceptable", + }, + } + if self.allow_no_value: + config.update({ + "NoValue": { + "option-without-value": None, + } + }) + cf = self.newconfig() + cf.read_dict(config) + self.basic_test(cf) + if self.strict: + with self.assertRaises(configparser.DuplicateSectionError): + cf.read_dict({ + '1': {'key': 'value'}, + 1: {'key2': 'value2'}, + }) + with self.assertRaises(configparser.DuplicateOptionError): + cf.read_dict({ + "Duplicate Options Here": { + 'option': 'with a value', + 'OPTION': 'with another value', + }, + }) + else: + cf.read_dict({ + 'section': {'key': 'value'}, + 'SECTION': {'key2': 'value2'}, + }) + cf.read_dict({ + "Duplicate Options Here": { + 'option': 'with a value', + 'OPTION': 'with another value', + }, + }) + def test_case_sensitivity(self): cf = self.newconfig() cf.add_section("A") cf.add_section("a") + cf.add_section("B") L = cf.sections() L.sort() eq = self.assertEqual - eq(L, ["A", "a"]) + eq(L, ["A", "B", "a"]) cf.set("a", "B", "value") eq(cf.options("a"), ["b"]) eq(cf.get("a", "b"), "value", "could not locate option, expecting case-insensitive option names") + with self.assertRaises(configparser.NoSectionError): + # section names are case-sensitive + cf.set("b", "A", "value") self.assertTrue(cf.has_option("a", "b")) + self.assertFalse(cf.has_option("b", "b")) cf.set("A", "A-B", "A-B value") for opt in ("a-b", "A-b", "a-B", "A-B"): self.assertTrue( @@ -120,62 +456,125 @@ class TestCaseBase(unittest.TestCase): # SF bug #432369: cf = self.fromstring( - "[MySection]\nOption: first line\n\tsecond line\n") + "[MySection]\nOption{} first line \n\tsecond line \n".format( + self.delimiters[0])) eq(cf.options("MySection"), ["option"]) eq(cf.get("MySection", "Option"), "first line\nsecond line") # SF bug #561822: - cf = self.fromstring("[section]\nnekey=nevalue\n", + cf = self.fromstring("[section]\n" + "nekey{}nevalue\n".format(self.delimiters[0]), defaults={"key":"value"}) self.assertTrue(cf.has_option("section", "Key")) + def test_case_sensitivity_mapping_access(self): + cf = self.newconfig() + cf["A"] = {} + cf["a"] = {"B": "value"} + cf["B"] = {} + L = [section for section in cf] + L.sort() + eq = self.assertEqual + elem_eq = self.assertCountEqual + eq(L, sorted(["A", "B", self.default_section, "a"])) + eq(cf["a"].keys(), {"b"}) + eq(cf["a"]["b"], "value", + "could not locate option, expecting case-insensitive option names") + with self.assertRaises(KeyError): + # section names are case-sensitive + cf["b"]["A"] = "value" + self.assertTrue("b" in cf["a"]) + cf["A"]["A-B"] = "A-B value" + for opt in ("a-b", "A-b", "a-B", "A-B"): + self.assertTrue( + opt in cf["A"], + "has_option() returned false for option which should exist") + eq(cf["A"].keys(), {"a-b"}) + eq(cf["a"].keys(), {"b"}) + del cf["a"]["B"] + elem_eq(cf["a"].keys(), {}) + + # SF bug #432369: + cf = self.fromstring( + "[MySection]\nOption{} first line \n\tsecond line \n".format( + self.delimiters[0])) + eq(cf["MySection"].keys(), {"option"}) + eq(cf["MySection"]["Option"], "first line\nsecond line") + + # SF bug #561822: + cf = self.fromstring("[section]\n" + "nekey{}nevalue\n".format(self.delimiters[0]), + defaults={"key":"value"}) + self.assertTrue("Key" in cf["section"]) + def test_default_case_sensitivity(self): cf = self.newconfig({"foo": "Bar"}) self.assertEqual( - cf.get("DEFAULT", "Foo"), "Bar", + cf.get(self.default_section, "Foo"), "Bar", "could not locate option, expecting case-insensitive option names") cf = self.newconfig({"Foo": "Bar"}) self.assertEqual( - cf.get("DEFAULT", "Foo"), "Bar", + cf.get(self.default_section, "Foo"), "Bar", "could not locate option, expecting case-insensitive defaults") def test_parse_errors(self): - self.newconfig() - self.parse_error(configparser.ParsingError, - "[Foo]\n extra-spaces: splat\n") - self.parse_error(configparser.ParsingError, - "[Foo]\n extra-spaces= splat\n") - self.parse_error(configparser.ParsingError, - "[Foo]\noption-without-value\n") - self.parse_error(configparser.ParsingError, - "[Foo]\n:value-without-option-name\n") - self.parse_error(configparser.ParsingError, - "[Foo]\n=value-without-option-name\n") - self.parse_error(configparser.MissingSectionHeaderError, - "No Section!\n") - - def parse_error(self, exc, src): - sio = io.StringIO(src) - self.assertRaises(exc, self.cf.readfp, sio) + cf = self.newconfig() + self.parse_error(cf, configparser.ParsingError, + "[Foo]\n" + "{}val-without-opt-name\n".format(self.delimiters[0])) + self.parse_error(cf, configparser.ParsingError, + "[Foo]\n" + "{}val-without-opt-name\n".format(self.delimiters[1])) + e = self.parse_error(cf, configparser.MissingSectionHeaderError, + "No Section!\n") + self.assertEqual(e.args, ('<???>', 1, "No Section!\n")) + if not self.allow_no_value: + e = self.parse_error(cf, configparser.ParsingError, + "[Foo]\n wrong-indent\n") + self.assertEqual(e.args, ('<???>',)) + # read_file on a real file + tricky = support.findfile("cfgparser.3") + if self.delimiters[0] == '=': + error = configparser.ParsingError + expected = (tricky,) + else: + error = configparser.MissingSectionHeaderError + expected = (tricky, 1, + ' # INI with as many tricky parts as possible\n') + with open(tricky, encoding='utf-8') as f: + e = self.parse_error(cf, error, f) + self.assertEqual(e.args, expected) + + def parse_error(self, cf, exc, src): + if hasattr(src, 'readline'): + sio = src + else: + sio = io.StringIO(src) + with self.assertRaises(exc) as cm: + cf.read_file(sio) + return cm.exception def test_query_errors(self): cf = self.newconfig() self.assertEqual(cf.sections(), [], "new ConfigParser should have no defined sections") self.assertFalse(cf.has_section("Foo"), - "new ConfigParser should have no acknowledged sections") - self.assertRaises(configparser.NoSectionError, - cf.options, "Foo") - self.assertRaises(configparser.NoSectionError, - cf.set, "foo", "bar", "value") - self.get_error(configparser.NoSectionError, "foo", "bar") + "new ConfigParser should have no acknowledged " + "sections") + with self.assertRaises(configparser.NoSectionError): + cf.options("Foo") + with self.assertRaises(configparser.NoSectionError): + cf.set("foo", "bar", "value") + e = self.get_error(cf, configparser.NoSectionError, "foo", "bar") + self.assertEqual(e.args, ("foo",)) cf.add_section("foo") - self.get_error(configparser.NoOptionError, "foo", "bar") + e = self.get_error(cf, configparser.NoOptionError, "foo", "bar") + self.assertEqual(e.args, ("bar", "foo")) - def get_error(self, exc, section, option): + def get_error(self, cf, exc, section, option): try: - self.cf.get(section, option) + cf.get(section, option) except exc as e: return e else: @@ -185,21 +584,21 @@ class TestCaseBase(unittest.TestCase): def test_boolean(self): cf = self.fromstring( "[BOOLTEST]\n" - "T1=1\n" - "T2=TRUE\n" - "T3=True\n" - "T4=oN\n" - "T5=yes\n" - "F1=0\n" - "F2=FALSE\n" - "F3=False\n" - "F4=oFF\n" - "F5=nO\n" - "E1=2\n" - "E2=foo\n" - "E3=-1\n" - "E4=0.1\n" - "E5=FALSE AND MORE" + "T1{equals}1\n" + "T2{equals}TRUE\n" + "T3{equals}True\n" + "T4{equals}oN\n" + "T5{equals}yes\n" + "F1{equals}0\n" + "F2{equals}FALSE\n" + "F3{equals}False\n" + "F4{equals}oFF\n" + "F5{equals}nO\n" + "E1{equals}2\n" + "E2{equals}foo\n" + "E3{equals}-1\n" + "E4{equals}0.1\n" + "E5{equals}FALSE AND MORE".format(equals=self.delimiters[0]) ) for x in range(1, 5): self.assertTrue(cf.getboolean('BOOLTEST', 't%d' % x)) @@ -210,35 +609,91 @@ class TestCaseBase(unittest.TestCase): def test_weird_errors(self): cf = self.newconfig() cf.add_section("Foo") - self.assertRaises(configparser.DuplicateSectionError, - cf.add_section, "Foo") + with self.assertRaises(configparser.DuplicateSectionError) as cm: + cf.add_section("Foo") + e = cm.exception + self.assertEqual(str(e), "Section 'Foo' already exists") + self.assertEqual(e.args, ("Foo", None, None)) + + if self.strict: + with self.assertRaises(configparser.DuplicateSectionError) as cm: + cf.read_string(textwrap.dedent("""\ + [Foo] + will this be added{equals}True + [Bar] + what about this{equals}True + [Foo] + oops{equals}this won't + """.format(equals=self.delimiters[0])), source='<foo-bar>') + e = cm.exception + self.assertEqual(str(e), "While reading from <foo-bar> [line 5]: " + "section 'Foo' already exists") + self.assertEqual(e.args, ("Foo", '<foo-bar>', 5)) + + with self.assertRaises(configparser.DuplicateOptionError) as cm: + cf.read_dict({'Bar': {'opt': 'val', 'OPT': 'is really `opt`'}}) + e = cm.exception + self.assertEqual(str(e), "While reading from <dict>: option 'opt' " + "in section 'Bar' already exists") + self.assertEqual(e.args, ("Bar", "opt", "<dict>", None)) def test_write(self): - cf = self.fromstring( + config_string = ( "[Long Line]\n" - "foo: this line is much, much longer than my editor\n" + "foo{0[0]} this line is much, much longer than my editor\n" " likes it.\n" - "[DEFAULT]\n" - "foo: another very\n" - " long line" + "[{default_section}]\n" + "foo{0[1]} another very\n" + " long line\n" + "[Long Line - With Comments!]\n" + "test {0[1]} we {comment} can\n" + " also {comment} place\n" + " comments {comment} in\n" + " multiline {comment} values" + "\n".format(self.delimiters, comment=self.comment_prefixes[0], + default_section=self.default_section) ) - output = io.StringIO() - cf.write(output) - self.assertEqual( - output.getvalue(), - "[DEFAULT]\n" - "foo = another very\n" - "\tlong line\n" - "\n" - "[Long Line]\n" - "foo = this line is much, much longer than my editor\n" - "\tlikes it.\n" - "\n" + if self.allow_no_value: + config_string += ( + "[Valueless]\n" + "option-without-value\n" ) + cf = self.fromstring(config_string) + for space_around_delimiters in (True, False): + output = io.StringIO() + cf.write(output, space_around_delimiters=space_around_delimiters) + delimiter = self.delimiters[0] + if space_around_delimiters: + delimiter = " {} ".format(delimiter) + expect_string = ( + "[{default_section}]\n" + "foo{equals}another very\n" + "\tlong line\n" + "\n" + "[Long Line]\n" + "foo{equals}this line is much, much longer than my editor\n" + "\tlikes it.\n" + "\n" + "[Long Line - With Comments!]\n" + "test{equals}we\n" + "\talso\n" + "\tcomments\n" + "\tmultiline\n" + "\n".format(equals=delimiter, + default_section=self.default_section) + ) + if self.allow_no_value: + expect_string += ( + "[Valueless]\n" + "option-without-value\n" + "\n" + ) + self.assertEqual(output.getvalue(), expect_string) + def test_set_string_types(self): cf = self.fromstring("[sect]\n" - "option1=foo\n") + "option1{eq}foo\n".format(eq=self.delimiters[0])) # Check that we don't get an exception when setting values in # an existing section using strings: class mystr(str): @@ -251,6 +706,9 @@ class TestCaseBase(unittest.TestCase): cf.set("sect", "option2", "splat") def test_read_returns_file_list(self): + if self.delimiters[0] != '=': + # skip reading the file if we're using an incompatible format + return file1 = support.findfile("cfgparser.1") # check when we pass a mix of readable and non-readable files: cf = self.newconfig() @@ -275,102 +733,237 @@ class TestCaseBase(unittest.TestCase): def get_interpolation_config(self): return self.fromstring( "[Foo]\n" - "bar=something %(with1)s interpolation (1 step)\n" - "bar9=something %(with9)s lots of interpolation (9 steps)\n" - "bar10=something %(with10)s lots of interpolation (10 steps)\n" - "bar11=something %(with11)s lots of interpolation (11 steps)\n" - "with11=%(with10)s\n" - "with10=%(with9)s\n" - "with9=%(with8)s\n" - "with8=%(With7)s\n" - "with7=%(WITH6)s\n" - "with6=%(with5)s\n" - "With5=%(with4)s\n" - "WITH4=%(with3)s\n" - "with3=%(with2)s\n" - "with2=%(with1)s\n" - "with1=with\n" + "bar{equals}something %(with1)s interpolation (1 step)\n" + "bar9{equals}something %(with9)s lots of interpolation (9 steps)\n" + "bar10{equals}something %(with10)s lots of interpolation (10 steps)\n" + "bar11{equals}something %(with11)s lots of interpolation (11 steps)\n" + "with11{equals}%(with10)s\n" + "with10{equals}%(with9)s\n" + "with9{equals}%(with8)s\n" + "with8{equals}%(With7)s\n" + "with7{equals}%(WITH6)s\n" + "with6{equals}%(with5)s\n" + "With5{equals}%(with4)s\n" + "WITH4{equals}%(with3)s\n" + "with3{equals}%(with2)s\n" + "with2{equals}%(with1)s\n" + "with1{equals}with\n" "\n" "[Mutual Recursion]\n" - "foo=%(bar)s\n" - "bar=%(foo)s\n" + "foo{equals}%(bar)s\n" + "bar{equals}%(foo)s\n" "\n" "[Interpolation Error]\n" - "name=%(reference)s\n", # no definition for 'reference' - defaults={"getname": "%(__name__)s"}) + "name{equals}%(reference)s\n".format(equals=self.delimiters[0])) def check_items_config(self, expected): - cf = self.fromstring( - "[section]\n" - "name = value\n" - "key: |%(name)s| \n" - "getdefault: |%(default)s|\n" - "getname: |%(__name__)s|", - defaults={"default": "<default>"}) - L = list(cf.items("section")) + cf = self.fromstring(""" + [section] + name {0[0]} %(value)s + key{0[1]} |%(name)s| + getdefault{0[1]} |%(default)s| + """.format(self.delimiters), defaults={"default": "<default>"}) + L = list(cf.items("section", vars={'value': 'value'})) L.sort() self.assertEqual(L, expected) + with self.assertRaises(configparser.NoSectionError): + cf.items("no such section") + + +class StrictTestCase(BasicTestCase): + config_class = configparser.RawConfigParser + strict = True -class ConfigParserTestCase(TestCaseBase): +class ConfigParserTestCase(BasicTestCase): config_class = configparser.ConfigParser def test_interpolation(self): cf = self.get_interpolation_config() eq = self.assertEqual - eq(cf.get("Foo", "getname"), "Foo") eq(cf.get("Foo", "bar"), "something with interpolation (1 step)") eq(cf.get("Foo", "bar9"), "something with lots of interpolation (9 steps)") eq(cf.get("Foo", "bar10"), "something with lots of interpolation (10 steps)") - self.get_error(configparser.InterpolationDepthError, "Foo", "bar11") + e = self.get_error(cf, configparser.InterpolationDepthError, "Foo", "bar11") + if self.interpolation == configparser._UNSET: + self.assertEqual(e.args, ("bar11", "Foo", "%(with1)s")) + elif isinstance(self.interpolation, configparser.LegacyInterpolation): + self.assertEqual(e.args, ("bar11", "Foo", + "something %(with11)s lots of interpolation (11 steps)")) def test_interpolation_missing_value(self): cf = self.get_interpolation_config() - e = self.get_error(configparser.InterpolationError, + e = self.get_error(cf, configparser.InterpolationMissingOptionError, "Interpolation Error", "name") self.assertEqual(e.reference, "reference") self.assertEqual(e.section, "Interpolation Error") self.assertEqual(e.option, "name") + if self.interpolation == configparser._UNSET: + self.assertEqual(e.args, ('name', 'Interpolation Error', + '', 'reference')) + elif isinstance(self.interpolation, configparser.LegacyInterpolation): + self.assertEqual(e.args, ('name', 'Interpolation Error', + '%(reference)s', 'reference')) def test_items(self): self.check_items_config([('default', '<default>'), ('getdefault', '|<default>|'), - ('getname', '|section|'), ('key', '|value|'), - ('name', 'value')]) + ('name', 'value'), + ('value', 'value')]) + + def test_safe_interpolation(self): + # See http://www.python.org/sf/511737 + cf = self.fromstring("[section]\n" + "option1{eq}xxx\n" + "option2{eq}%(option1)s/xxx\n" + "ok{eq}%(option1)s/%%s\n" + "not_ok{eq}%(option2)s/%%s".format( + eq=self.delimiters[0])) + self.assertEqual(cf.get("section", "ok"), "xxx/%s") + if self.interpolation == configparser._UNSET: + self.assertEqual(cf.get("section", "not_ok"), "xxx/xxx/%s") + elif isinstance(self.interpolation, configparser.LegacyInterpolation): + with self.assertRaises(TypeError): + cf.get("section", "not_ok") + + def test_set_malformatted_interpolation(self): + cf = self.fromstring("[sect]\n" + "option1{eq}foo\n".format(eq=self.delimiters[0])) + + self.assertEqual(cf.get('sect', "option1"), "foo") + + self.assertRaises(ValueError, cf.set, "sect", "option1", "%foo") + self.assertRaises(ValueError, cf.set, "sect", "option1", "foo%") + self.assertRaises(ValueError, cf.set, "sect", "option1", "f%oo") + + self.assertEqual(cf.get('sect', "option1"), "foo") + + # bug #5741: double percents are *not* malformed + cf.set("sect", "option2", "foo%%bar") + self.assertEqual(cf.get("sect", "option2"), "foo%bar") def test_set_nonstring_types(self): + cf = self.fromstring("[sect]\n" + "option1{eq}foo\n".format(eq=self.delimiters[0])) + # Check that we get a TypeError when setting non-string values + # in an existing section: + self.assertRaises(TypeError, cf.set, "sect", "option1", 1) + self.assertRaises(TypeError, cf.set, "sect", "option1", 1.0) + self.assertRaises(TypeError, cf.set, "sect", "option1", object()) + self.assertRaises(TypeError, cf.set, "sect", "option2", 1) + self.assertRaises(TypeError, cf.set, "sect", "option2", 1.0) + self.assertRaises(TypeError, cf.set, "sect", "option2", object()) + self.assertRaises(TypeError, cf.set, "sect", 123, "invalid opt name!") + self.assertRaises(TypeError, cf.add_section, 123) + + def test_add_section_default(self): cf = self.newconfig() - cf.add_section('non-string') - cf.set('non-string', 'int', 1) - cf.set('non-string', 'list', [0, 1, 1, 2, 3, 5, 8, 13, '%(']) - cf.set('non-string', 'dict', {'pi': 3.14159, '%(': 1, - '%(list)': '%(list)'}) - cf.set('non-string', 'string_with_interpolation', '%(list)s') - self.assertEqual(cf.get('non-string', 'int', raw=True), 1) - self.assertRaises(TypeError, cf.get, 'non-string', 'int') - self.assertEqual(cf.get('non-string', 'list', raw=True), - [0, 1, 1, 2, 3, 5, 8, 13, '%(']) - self.assertRaises(TypeError, cf.get, 'non-string', 'list') - self.assertEqual(cf.get('non-string', 'dict', raw=True), - {'pi': 3.14159, '%(': 1, '%(list)': '%(list)'}) - self.assertRaises(TypeError, cf.get, 'non-string', 'dict') - self.assertEqual(cf.get('non-string', 'string_with_interpolation', - raw=True), '%(list)s') - self.assertRaises(ValueError, cf.get, 'non-string', - 'string_with_interpolation', raw=False) - - -class RawConfigParserTestCase(TestCaseBase): + self.assertRaises(ValueError, cf.add_section, self.default_section) + + +class ConfigParserTestCaseNoInterpolation(BasicTestCase): + config_class = configparser.ConfigParser + interpolation = None + ini = textwrap.dedent(""" + [numbers] + one = 1 + two = %(one)s * 2 + three = ${common:one} * 3 + + [hexen] + sixteen = ${numbers:two} * 8 + """).strip() + + def assertMatchesIni(self, cf): + self.assertEqual(cf['numbers']['one'], '1') + self.assertEqual(cf['numbers']['two'], '%(one)s * 2') + self.assertEqual(cf['numbers']['three'], '${common:one} * 3') + self.assertEqual(cf['hexen']['sixteen'], '${numbers:two} * 8') + + def test_no_interpolation(self): + cf = self.fromstring(self.ini) + self.assertMatchesIni(cf) + + def test_empty_case(self): + cf = self.newconfig() + self.assertIsNone(cf.read_string("")) + + def test_none_as_default_interpolation(self): + class CustomConfigParser(configparser.ConfigParser): + _DEFAULT_INTERPOLATION = None + + cf = CustomConfigParser() + cf.read_string(self.ini) + self.assertMatchesIni(cf) + + +class ConfigParserTestCaseLegacyInterpolation(ConfigParserTestCase): + config_class = configparser.ConfigParser + interpolation = configparser.LegacyInterpolation() + + def test_set_malformatted_interpolation(self): + cf = self.fromstring("[sect]\n" + "option1{eq}foo\n".format(eq=self.delimiters[0])) + + self.assertEqual(cf.get('sect', "option1"), "foo") + + cf.set("sect", "option1", "%foo") + self.assertEqual(cf.get('sect', "option1"), "%foo") + cf.set("sect", "option1", "foo%") + self.assertEqual(cf.get('sect', "option1"), "foo%") + cf.set("sect", "option1", "f%oo") + self.assertEqual(cf.get('sect', "option1"), "f%oo") + + # bug #5741: double percents are *not* malformed + cf.set("sect", "option2", "foo%%bar") + self.assertEqual(cf.get("sect", "option2"), "foo%%bar") + +class ConfigParserTestCaseNonStandardDelimiters(ConfigParserTestCase): + delimiters = (':=', '$') + comment_prefixes = ('//', '"') + inline_comment_prefixes = ('//', '"') + +class ConfigParserTestCaseNonStandardDefaultSection(ConfigParserTestCase): + default_section = 'general' + +class MultilineValuesTestCase(BasicTestCase): + config_class = configparser.ConfigParser + wonderful_spam = ("I'm having spam spam spam spam " + "spam spam spam beaked beans spam " + "spam spam and spam!").replace(' ', '\t\n') + + def setUp(self): + cf = self.newconfig() + for i in range(100): + s = 'section{}'.format(i) + cf.add_section(s) + for j in range(10): + cf.set(s, 'lovely_spam{}'.format(j), self.wonderful_spam) + with open(support.TESTFN, 'w') as f: + cf.write(f) + + def tearDown(self): + os.unlink(support.TESTFN) + + def test_dominating_multiline_values(self): + # We're reading from file because this is where the code changed + # during performance updates in Python 3.2 + cf_from_file = self.newconfig() + with open(support.TESTFN) as f: + cf_from_file.read_file(f) + self.assertEqual(cf_from_file.get('section8', 'lovely_spam4'), + self.wonderful_spam.replace('\t\n', '\n')) + +class RawConfigParserTestCase(BasicTestCase): config_class = configparser.RawConfigParser def test_interpolation(self): cf = self.get_interpolation_config() eq = self.assertEqual - eq(cf.get("Foo", "getname"), "%(__name__)s") eq(cf.get("Foo", "bar"), "something %(with1)s interpolation (1 step)") eq(cf.get("Foo", "bar9"), @@ -383,9 +976,9 @@ class RawConfigParserTestCase(TestCaseBase): def test_items(self): self.check_items_config([('default', '<default>'), ('getdefault', '|%(default)s|'), - ('getname', '|%(__name__)s|'), ('key', '|%(name)s|'), - ('name', 'value')]) + ('name', '%(value)s'), + ('value', 'value')]) def test_set_nonstring_types(self): cf = self.newconfig() @@ -397,72 +990,302 @@ class RawConfigParserTestCase(TestCaseBase): self.assertEqual(cf.get('non-string', 'list'), [0, 1, 1, 2, 3, 5, 8, 13]) self.assertEqual(cf.get('non-string', 'dict'), {'pi': 3.14159}) + cf.add_section(123) + cf.set(123, 'this is sick', True) + self.assertEqual(cf.get(123, 'this is sick'), True) + if cf._dict is configparser._default_dict: + # would not work for SortedDict; only checking for the most common + # default dictionary (OrderedDict) + cf.optionxform = lambda x: x + cf.set('non-string', 1, 1) + self.assertEqual(cf.get('non-string', 1), 1) +class RawConfigParserTestCaseNonStandardDelimiters(RawConfigParserTestCase): + delimiters = (':=', '$') + comment_prefixes = ('//', '"') + inline_comment_prefixes = ('//', '"') -class SafeConfigParserTestCase(ConfigParserTestCase): - config_class = configparser.SafeConfigParser +class RawConfigParserTestSambaConf(CfgParserTestCaseClass): + config_class = configparser.RawConfigParser + comment_prefixes = ('#', ';', '----') + inline_comment_prefixes = ('//',) + empty_lines_in_values = False - def test_safe_interpolation(self): - # See http://www.python.org/sf/511737 - cf = self.fromstring("[section]\n" - "option1=xxx\n" - "option2=%(option1)s/xxx\n" - "ok=%(option1)s/%%s\n" - "not_ok=%(option2)s/%%s") - self.assertEqual(cf.get("section", "ok"), "xxx/%s") - self.assertEqual(cf.get("section", "not_ok"), "xxx/xxx/%s") + def test_reading(self): + smbconf = support.findfile("cfgparser.2") + # check when we pass a mix of readable and non-readable files: + cf = self.newconfig() + parsed_files = cf.read([smbconf, "nonexistent-file"], encoding='utf-8') + self.assertEqual(parsed_files, [smbconf]) + sections = ['global', 'homes', 'printers', + 'print$', 'pdf-generator', 'tmp', 'Agustin'] + self.assertEqual(cf.sections(), sections) + self.assertEqual(cf.get("global", "workgroup"), "MDKGROUP") + self.assertEqual(cf.getint("global", "max log size"), 50) + self.assertEqual(cf.get("global", "hosts allow"), "127.") + self.assertEqual(cf.get("tmp", "echo command"), "cat %s; rm %s") - def test_set_malformatted_interpolation(self): - cf = self.fromstring("[sect]\n" - "option1=foo\n") +class ConfigParserTestCaseExtendedInterpolation(BasicTestCase): + config_class = configparser.ConfigParser + interpolation = configparser.ExtendedInterpolation() + default_section = 'common' + strict = True - self.assertEqual(cf.get('sect', "option1"), "foo") + def fromstring(self, string, defaults=None, optionxform=None): + cf = self.newconfig(defaults) + if optionxform: + cf.optionxform = optionxform + cf.read_string(string) + return cf - self.assertRaises(ValueError, cf.set, "sect", "option1", "%foo") - self.assertRaises(ValueError, cf.set, "sect", "option1", "foo%") - self.assertRaises(ValueError, cf.set, "sect", "option1", "f%oo") + def test_extended_interpolation(self): + cf = self.fromstring(textwrap.dedent(""" + [common] + favourite Beatle = Paul + favourite color = green - self.assertEqual(cf.get('sect', "option1"), "foo") + [tom] + favourite band = ${favourite color} day + favourite pope = John ${favourite Beatle} II + sequel = ${favourite pope}I - # bug #5741: double percents are *not* malformed - cf.set("sect", "option2", "foo%%bar") - self.assertEqual(cf.get("sect", "option2"), "foo%bar") + [ambv] + favourite Beatle = George + son of Edward VII = ${favourite Beatle} V + son of George V = ${son of Edward VII}I - def test_set_nonstring_types(self): - cf = self.fromstring("[sect]\n" - "option1=foo\n") - # Check that we get a TypeError when setting non-string values - # in an existing section: - self.assertRaises(TypeError, cf.set, "sect", "option1", 1) - self.assertRaises(TypeError, cf.set, "sect", "option1", 1.0) - self.assertRaises(TypeError, cf.set, "sect", "option1", object()) - self.assertRaises(TypeError, cf.set, "sect", "option2", 1) - self.assertRaises(TypeError, cf.set, "sect", "option2", 1.0) - self.assertRaises(TypeError, cf.set, "sect", "option2", object()) + [stanley] + favourite Beatle = ${ambv:favourite Beatle} + favourite pope = ${tom:favourite pope} + favourite color = black + favourite state of mind = paranoid + favourite movie = soylent ${common:favourite color} + favourite song = ${favourite color} sabbath - ${favourite state of mind} + """).strip()) + + eq = self.assertEqual + eq(cf['common']['favourite Beatle'], 'Paul') + eq(cf['common']['favourite color'], 'green') + eq(cf['tom']['favourite Beatle'], 'Paul') + eq(cf['tom']['favourite color'], 'green') + eq(cf['tom']['favourite band'], 'green day') + eq(cf['tom']['favourite pope'], 'John Paul II') + eq(cf['tom']['sequel'], 'John Paul III') + eq(cf['ambv']['favourite Beatle'], 'George') + eq(cf['ambv']['favourite color'], 'green') + eq(cf['ambv']['son of Edward VII'], 'George V') + eq(cf['ambv']['son of George V'], 'George VI') + eq(cf['stanley']['favourite Beatle'], 'George') + eq(cf['stanley']['favourite color'], 'black') + eq(cf['stanley']['favourite state of mind'], 'paranoid') + eq(cf['stanley']['favourite movie'], 'soylent green') + eq(cf['stanley']['favourite pope'], 'John Paul II') + eq(cf['stanley']['favourite song'], + 'black sabbath - paranoid') + + def test_endless_loop(self): + cf = self.fromstring(textwrap.dedent(""" + [one for you] + ping = ${one for me:pong} + + [one for me] + pong = ${one for you:ping} + + [selfish] + me = ${me} + """).strip()) + + with self.assertRaises(configparser.InterpolationDepthError): + cf['one for you']['ping'] + with self.assertRaises(configparser.InterpolationDepthError): + cf['selfish']['me'] + + def test_strange_options(self): + cf = self.fromstring(""" + [dollars] + $var = $$value + $var2 = ${$var} + ${sick} = cannot interpolate me + + [interpolated] + $other = ${dollars:$var} + $trying = ${dollars:${sick}} + """) - def test_add_section_default_1(self): + self.assertEqual(cf['dollars']['$var'], '$value') + self.assertEqual(cf['interpolated']['$other'], '$value') + self.assertEqual(cf['dollars']['${sick}'], 'cannot interpolate me') + exception_class = configparser.InterpolationMissingOptionError + with self.assertRaises(exception_class) as cm: + cf['interpolated']['$trying'] + self.assertEqual(cm.exception.reference, 'dollars:${sick') + self.assertEqual(cm.exception.args[2], '}') #rawval + + def test_case_sensitivity_basic(self): + ini = textwrap.dedent(""" + [common] + optionlower = value + OptionUpper = Value + + [Common] + optionlower = a better ${common:optionlower} + OptionUpper = A Better ${common:OptionUpper} + + [random] + foolower = ${common:optionlower} redefined + FooUpper = ${Common:OptionUpper} Redefined + """).strip() + + cf = self.fromstring(ini) + eq = self.assertEqual + eq(cf['common']['optionlower'], 'value') + eq(cf['common']['OptionUpper'], 'Value') + eq(cf['Common']['optionlower'], 'a better value') + eq(cf['Common']['OptionUpper'], 'A Better Value') + eq(cf['random']['foolower'], 'value redefined') + eq(cf['random']['FooUpper'], 'A Better Value Redefined') + + def test_case_sensitivity_conflicts(self): + ini = textwrap.dedent(""" + [common] + option = value + Option = Value + + [Common] + option = a better ${common:option} + Option = A Better ${common:Option} + + [random] + foo = ${common:option} redefined + Foo = ${Common:Option} Redefined + """).strip() + with self.assertRaises(configparser.DuplicateOptionError): + cf = self.fromstring(ini) + + # raw options + cf = self.fromstring(ini, optionxform=lambda opt: opt) + eq = self.assertEqual + eq(cf['common']['option'], 'value') + eq(cf['common']['Option'], 'Value') + eq(cf['Common']['option'], 'a better value') + eq(cf['Common']['Option'], 'A Better Value') + eq(cf['random']['foo'], 'value redefined') + eq(cf['random']['Foo'], 'A Better Value Redefined') + + def test_other_errors(self): + cf = self.fromstring(""" + [interpolation fail] + case1 = ${where's the brace + case2 = ${does_not_exist} + case3 = ${wrong_section:wrong_value} + case4 = ${i:like:colon:characters} + case5 = $100 for Fail No 5! + """) + + with self.assertRaises(configparser.InterpolationSyntaxError): + cf['interpolation fail']['case1'] + with self.assertRaises(configparser.InterpolationMissingOptionError): + cf['interpolation fail']['case2'] + with self.assertRaises(configparser.InterpolationMissingOptionError): + cf['interpolation fail']['case3'] + with self.assertRaises(configparser.InterpolationSyntaxError): + cf['interpolation fail']['case4'] + with self.assertRaises(configparser.InterpolationSyntaxError): + cf['interpolation fail']['case5'] + with self.assertRaises(ValueError): + cf['interpolation fail']['case6'] = "BLACK $ABBATH" + + +class ConfigParserTestCaseNoValue(ConfigParserTestCase): + allow_no_value = True + +class ConfigParserTestCaseTrickyFile(CfgParserTestCaseClass): + config_class = configparser.ConfigParser + delimiters = {'='} + comment_prefixes = {'#'} + allow_no_value = True + + def test_cfgparser_dot_3(self): + tricky = support.findfile("cfgparser.3") cf = self.newconfig() - self.assertRaises(ValueError, cf.add_section, "default") + self.assertEqual(len(cf.read(tricky, encoding='utf-8')), 1) + self.assertEqual(cf.sections(), ['strange', + 'corruption', + 'yeah, sections can be ' + 'indented as well', + 'another one!', + 'no values here', + 'tricky interpolation', + 'more interpolation']) + self.assertEqual(cf.getint(self.default_section, 'go', + vars={'interpolate': '-1'}), -1) + with self.assertRaises(ValueError): + # no interpolation will happen + cf.getint(self.default_section, 'go', raw=True, + vars={'interpolate': '-1'}) + self.assertEqual(len(cf.get('strange', 'other').split('\n')), 4) + self.assertEqual(len(cf.get('corruption', 'value').split('\n')), 10) + longname = 'yeah, sections can be indented as well' + self.assertFalse(cf.getboolean(longname, 'are they subsections')) + self.assertEqual(cf.get(longname, 'lets use some Unicode'), '片仮名') + self.assertEqual(len(cf.items('another one!')), 5) # 4 in section and + # `go` from DEFAULT + with self.assertRaises(configparser.InterpolationMissingOptionError): + cf.items('no values here') + self.assertEqual(cf.get('tricky interpolation', 'lets'), 'do this') + self.assertEqual(cf.get('tricky interpolation', 'lets'), + cf.get('tricky interpolation', 'go')) + self.assertEqual(cf.get('more interpolation', 'lets'), 'go shopping') - def test_add_section_default_2(self): + def test_unicode_failure(self): + tricky = support.findfile("cfgparser.3") cf = self.newconfig() - self.assertRaises(ValueError, cf.add_section, "DEFAULT") + with self.assertRaises(UnicodeDecodeError): + cf.read(tricky, encoding='ascii') + + +class Issue7005TestCase(unittest.TestCase): + """Test output when None is set() as a value and allow_no_value == False. + + http://bugs.python.org/issue7005 + + """ + + expected_output = "[section]\noption = None\n\n" + + def prepare(self, config_class): + # This is the default, but that's the point. + cp = config_class(allow_no_value=False) + cp.add_section("section") + cp.set("section", "option", None) + sio = io.StringIO() + cp.write(sio) + return sio.getvalue() + + def test_none_as_value_stringified(self): + cp = configparser.ConfigParser(allow_no_value=False) + cp.add_section("section") + with self.assertRaises(TypeError): + cp.set("section", "option", None) + + def test_none_as_value_stringified_raw(self): + output = self.prepare(configparser.RawConfigParser) + self.assertEqual(output, self.expected_output) + class SortedTestCase(RawConfigParserTestCase): - def newconfig(self, defaults=None): - self.cf = self.config_class(defaults=defaults, dict_type=SortedDict) - return self.cf + dict_type = SortedDict def test_sorted(self): - self.fromstring("[b]\n" - "o4=1\n" - "o3=2\n" - "o2=3\n" - "o1=4\n" - "[a]\n" - "k=v\n") + cf = self.fromstring("[b]\n" + "o4=1\n" + "o3=2\n" + "o2=3\n" + "o1=4\n" + "[a]\n" + "k=v\n") output = io.StringIO() - self.cf.write(output) + cf.write(output) self.assertEqual(output.getvalue(), "[a]\n" "k = v\n\n" @@ -472,13 +1295,204 @@ class SortedTestCase(RawConfigParserTestCase): "o3 = 2\n" "o4 = 1\n\n") + +class CompatibleTestCase(CfgParserTestCaseClass): + config_class = configparser.RawConfigParser + comment_prefixes = '#;' + inline_comment_prefixes = ';' + + def test_comment_handling(self): + config_string = textwrap.dedent("""\ + [Commented Bar] + baz=qwe ; a comment + foo: bar # not a comment! + # but this is a comment + ; another comment + quirk: this;is not a comment + ; a space must precede an inline comment + """) + cf = self.fromstring(config_string) + self.assertEqual(cf.get('Commented Bar', 'foo'), + 'bar # not a comment!') + self.assertEqual(cf.get('Commented Bar', 'baz'), 'qwe') + self.assertEqual(cf.get('Commented Bar', 'quirk'), + 'this;is not a comment') + +class CopyTestCase(BasicTestCase): + config_class = configparser.ConfigParser + + def fromstring(self, string, defaults=None): + cf = self.newconfig(defaults) + cf.read_string(string) + cf_copy = self.newconfig() + cf_copy.read_dict(cf) + # we have to clean up option duplicates that appeared because of + # the magic DEFAULTSECT behaviour. + for section in cf_copy.values(): + if section.name == self.default_section: + continue + for default, value in cf[self.default_section].items(): + if section[default] == value: + del section[default] + return cf_copy + + +class FakeFile: + def __init__(self): + file_path = support.findfile("cfgparser.1") + with open(file_path) as f: + self.lines = f.readlines() + self.lines.reverse() + + def readline(self): + if len(self.lines): + return self.lines.pop() + return '' + + +def readline_generator(f): + """As advised in Doc/library/configparser.rst.""" + line = f.readline() + while line: + yield line + line = f.readline() + + +class ReadFileTestCase(unittest.TestCase): + def test_file(self): + file_path = support.findfile("cfgparser.1") + parser = configparser.ConfigParser() + with open(file_path) as f: + parser.read_file(f) + self.assertIn("Foo Bar", parser) + self.assertIn("foo", parser["Foo Bar"]) + self.assertEqual(parser["Foo Bar"]["foo"], "newbar") + + def test_iterable(self): + lines = textwrap.dedent(""" + [Foo Bar] + foo=newbar""").strip().split('\n') + parser = configparser.ConfigParser() + parser.read_file(lines) + self.assertIn("Foo Bar", parser) + self.assertIn("foo", parser["Foo Bar"]) + self.assertEqual(parser["Foo Bar"]["foo"], "newbar") + + def test_readline_generator(self): + """Issue #11670.""" + parser = configparser.ConfigParser() + with self.assertRaises(TypeError): + parser.read_file(FakeFile()) + parser.read_file(readline_generator(FakeFile())) + self.assertIn("Foo Bar", parser) + self.assertIn("foo", parser["Foo Bar"]) + self.assertEqual(parser["Foo Bar"]["foo"], "newbar") + + +class CoverageOneHundredTestCase(unittest.TestCase): + """Covers edge cases in the codebase.""" + + def test_duplicate_option_error(self): + error = configparser.DuplicateOptionError('section', 'option') + self.assertEqual(error.section, 'section') + self.assertEqual(error.option, 'option') + self.assertEqual(error.source, None) + self.assertEqual(error.lineno, None) + self.assertEqual(error.args, ('section', 'option', None, None)) + self.assertEqual(str(error), "Option 'option' in section 'section' " + "already exists") + + def test_interpolation_depth_error(self): + error = configparser.InterpolationDepthError('option', 'section', + 'rawval') + self.assertEqual(error.args, ('option', 'section', 'rawval')) + self.assertEqual(error.option, 'option') + self.assertEqual(error.section, 'section') + + def test_parsing_error(self): + with self.assertRaises(ValueError) as cm: + configparser.ParsingError() + self.assertEqual(str(cm.exception), "Required argument `source' not " + "given.") + with self.assertRaises(ValueError) as cm: + configparser.ParsingError(source='source', filename='filename') + self.assertEqual(str(cm.exception), "Cannot specify both `filename' " + "and `source'. Use `source'.") + error = configparser.ParsingError(filename='source') + self.assertEqual(error.source, 'source') + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", DeprecationWarning) + self.assertEqual(error.filename, 'source') + error.filename = 'filename' + self.assertEqual(error.source, 'filename') + for warning in w: + self.assertTrue(warning.category is DeprecationWarning) + + def test_interpolation_validation(self): + parser = configparser.ConfigParser() + parser.read_string(""" + [section] + invalid_percent = % + invalid_reference = %(() + invalid_variable = %(does_not_exist)s + """) + with self.assertRaises(configparser.InterpolationSyntaxError) as cm: + parser['section']['invalid_percent'] + self.assertEqual(str(cm.exception), "'%' must be followed by '%' or " + "'(', found: '%'") + with self.assertRaises(configparser.InterpolationSyntaxError) as cm: + parser['section']['invalid_reference'] + self.assertEqual(str(cm.exception), "bad interpolation variable " + "reference '%(()'") + + def test_readfp_deprecation(self): + sio = io.StringIO(""" + [section] + option = value + """) + parser = configparser.ConfigParser() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", DeprecationWarning) + parser.readfp(sio, filename='StringIO') + for warning in w: + self.assertTrue(warning.category is DeprecationWarning) + self.assertEqual(len(parser), 2) + self.assertEqual(parser['section']['option'], 'value') + + def test_safeconfigparser_deprecation(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", DeprecationWarning) + parser = configparser.SafeConfigParser() + for warning in w: + self.assertTrue(warning.category is DeprecationWarning) + + def test_sectionproxy_repr(self): + parser = configparser.ConfigParser() + parser.read_string(""" + [section] + key = value + """) + self.assertEqual(repr(parser['section']), '<Section: section>') + def test_main(): support.run_unittest( ConfigParserTestCase, + ConfigParserTestCaseNonStandardDelimiters, + ConfigParserTestCaseNoValue, + ConfigParserTestCaseExtendedInterpolation, + ConfigParserTestCaseLegacyInterpolation, + ConfigParserTestCaseNoInterpolation, + ConfigParserTestCaseTrickyFile, + MultilineValuesTestCase, RawConfigParserTestCase, - SafeConfigParserTestCase, - SortedTestCase - ) - -if __name__ == "__main__": - test_main() + RawConfigParserTestCaseNonStandardDelimiters, + RawConfigParserTestSambaConf, + SortedTestCase, + Issue7005TestCase, + StrictTestCase, + CompatibleTestCase, + CopyTestCase, + ConfigParserTestCaseNonStandardDefaultSection, + ReadFileTestCase, + CoverageOneHundredTestCase, + ) |
