diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst index 9472b88eb3f..d0d159bfeb6 100644 --- a/Doc/library/configparser.rst +++ b/Doc/library/configparser.rst @@ -391,17 +391,20 @@ However, there are a few differences that should be taken into account: * Trying to delete the ``DEFAULTSECT`` raises ``ValueError``. -* There are two parser-level methods in the legacy API that hide the dictionary - interface and are incompatible: +* ``parser.get(section, option, **kwargs)`` - the second argument is **not** + a fallback value. Note however that the section-level ``get()`` methods are + compatible both with the mapping protocol and the classic configparser API. - * ``parser.get(section, option, **kwargs)`` - the second argument is **not** a - fallback value - - * ``parser.items(section)`` - this returns a list of *option*, *value* pairs - for a specified ``section`` +* ``parser.items()`` is compatible with the mapping protocol (returns a list of + *section_name*, *section_proxy* pairs including the DEFAULTSECT). However, + this method can also be invoked with arguments: ``parser.items(section, raw, + vars)``. The latter call returns a list of *option*, *value* pairs for + a specified ``section``, with all interpolations expanded (unless + ``raw=True`` is provided). The mapping protocol is implemented on top of the existing legacy API so that -subclassing the original interface makes the mappings work as expected as well. +subclasses overriding the original interface still should have mappings working +as expected. Customizing Parser Behaviour @@ -906,7 +909,8 @@ ConfigParser Objects .. method:: has_option(section, option) If the given *section* exists, and contains the given *option*, return - :const:`True`; otherwise return :const:`False`. + :const:`True`; otherwise return :const:`False`. If the specified + *section* is :const:`None` or an empty string, DEFAULT is assumed. .. method:: read(filenames, encoding=None) @@ -964,14 +968,17 @@ ConfigParser Objects .. method:: read_dict(dictionary, source='') - Load configuration from a dictionary. Keys are section names, values are - dictionaries with keys and values that should be present in the section. - If the used dictionary type preserves order, sections and their keys will - be added in order. Values are automatically converted to strings. + Load configuration from any object that provides a dict-like ``items()`` + method. Keys are section names, values are dictionaries with keys and + values that should be present in the section. If the used dictionary + type preserves order, sections and their keys will be added in order. + Values are automatically converted to strings. Optional argument *source* specifies a context-specific name of the dictionary passed. If not given, ```` is used. + This method can be used to copy state between parsers. + .. versionadded:: 3.2 @@ -1019,10 +1026,13 @@ ConfigParser Objects *fallback*. - .. method:: items(section, raw=False, vars=None) + .. method:: items([section], raw=False, vars=None) - Return a list of *name*, *value* pairs for the options in the given - *section*. Optional arguments have the same meaning as for the + When *section* is not given, return a list of *section_name*, + *section_proxy* pairs, including DEFAULTSECT. + + Otherwise, return a list of *name*, *value* pairs for the options in the + given *section*. Optional arguments have the same meaning as for the :meth:`get` method. diff --git a/Lib/configparser.py b/Lib/configparser.py index 0e41d2f1b5f..f1866eb1c92 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -98,8 +98,10 @@ ConfigParser -- responsible for parsing a list of insensitively defined as 0, false, no, off for False, and 1, true, yes, on for True). Returns False or True. - items(section, raw=False, vars=None) - Return a list of tuples with (name, value) for each option + items(section=_UNSET, raw=False, vars=None) + If section is given, return a list of tuples with (section_name, + section_proxy) for each section, including DEFAULTSECT. Otherwise, + return a list of tuples with (name, value) for each option in the section. remove_section(section) @@ -495,9 +497,9 @@ class ExtendedInterpolation(Interpolation): raise InterpolationSyntaxError( option, section, "More than one ':' found: %r" % (rest,)) - except KeyError: + except (KeyError, NoSectionError, NoOptionError): raise InterpolationMissingOptionError( - option, section, rest, var) + option, section, rest, ":".join(path)) if "$" in v: self._interpolate_some(parser, opt, accum, v, sect, dict(parser.items(sect, raw=True)), @@ -730,7 +732,7 @@ class RawConfigParser(MutableMapping): except (DuplicateSectionError, ValueError): if self._strict and section in elements_added: raise - elements_added.add(section) + elements_added.add(section) for key, value in keys.items(): key = self.optionxform(str(key)) if value is not None: @@ -820,7 +822,7 @@ class RawConfigParser(MutableMapping): else: return fallback - def items(self, section, raw=False, vars=None): + def items(self, section=_UNSET, raw=False, vars=None): """Return a list of (name, value) tuples for each option in a section. All % interpolations are expanded in the return values, based on the @@ -831,6 +833,8 @@ class RawConfigParser(MutableMapping): The section DEFAULT is special. """ + if section is _UNSET: + return super().items() d = self._defaults.copy() try: d.update(self._sections[section]) @@ -851,7 +855,9 @@ class RawConfigParser(MutableMapping): return optionstr.lower() def has_option(self, section, option): - """Check for the existence of a given option in a given section.""" + """Check for the existence of a given option in a given section. + If the specified `section' is None or an empty string, DEFAULT is + assumed. If the specified `section' does not exist, returns False.""" if not section or section == self.default_section: option = self.optionxform(option) return option in self._defaults @@ -1059,9 +1065,6 @@ class RawConfigParser(MutableMapping): # match if it would set optval to None if optval is not None: optval = optval.strip() - # allow empty values - if optval == '""': - optval = '' cursect[optname] = [optval] else: # valueless option handling @@ -1196,21 +1199,24 @@ class SectionProxy(MutableMapping): return self._parser.set(self._name, key, value) def __delitem__(self, key): - if not self._parser.has_option(self._name, key): + if not (self._parser.has_option(self._name, key) and + self._parser.remove_option(self._name, key)): raise KeyError(key) - return self._parser.remove_option(self._name, key) def __contains__(self, key): return self._parser.has_option(self._name, key) def __len__(self): - # XXX weak performance - return len(self._parser.options(self._name)) + return len(self._options()) def __iter__(self): - # XXX weak performance - # XXX does not break when underlying container state changed - return self._parser.options(self._name).__iter__() + return self._options().__iter__() + + def _options(self): + if self._name != self._parser.default_section: + return self._parser.options(self._name) + else: + return self._parser.defaults() def get(self, option, fallback=None, *, raw=False, vars=None): return self._parser.get(self._name, option, raw=raw, vars=vars, diff --git a/Lib/test/test_cfgparser.py b/Lib/test/test_cfgparser.py index 4b2d2dfb6eb..f7d9a26e896 100644 --- a/Lib/test/test_cfgparser.py +++ b/Lib/test/test_cfgparser.py @@ -5,6 +5,7 @@ import os import sys import textwrap import unittest +import warnings from test import support @@ -74,12 +75,16 @@ class BasicTestCase(CfgParserTestCaseClass): 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, E) + L = cf.items('Spacey Bar From The Beginning') + L.sort() + eq(L, F) # mapping access L = [section for section in cf] @@ -87,6 +92,15 @@ class BasicTestCase(CfgParserTestCaseClass): 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: @@ -124,15 +138,21 @@ class BasicTestCase(CfgParserTestCaseClass): 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) @@ -171,6 +191,7 @@ class BasicTestCase(CfgParserTestCaseClass): 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): @@ -181,6 +202,7 @@ class BasicTestCase(CfgParserTestCaseClass): 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) @@ -189,6 +211,7 @@ class BasicTestCase(CfgParserTestCaseClass): 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! @@ -196,6 +219,7 @@ class BasicTestCase(CfgParserTestCaseClass): "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) @@ -203,10 +227,17 @@ class BasicTestCase(CfgParserTestCaseClass): eq(cf['NoValue'].get('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: + # Make sure the right things happen for remove_section() and + # remove_option(); added to include check for SourceForge bug #123324. - # API acceess + 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'), @@ -214,6 +245,11 @@ class BasicTestCase(CfgParserTestCaseClass): 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')) with self.assertRaises(configparser.NoSectionError) as cm: cf.remove_option('No Such Section', 'foo') @@ -223,13 +259,29 @@ class BasicTestCase(CfgParserTestCaseClass): '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] @@ -344,6 +396,11 @@ boolean {0[0]} NO 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": { @@ -352,6 +409,10 @@ boolean {0[0]} NO }, }) else: + cf.read_dict({ + 'section': {'key': 'value'}, + 'SECTION': {'key2': 'value2'}, + }) cf.read_dict({ "Duplicate Options Here": { 'option': 'with a value', @@ -359,7 +420,6 @@ boolean {0[0]} NO }, }) - def test_case_sensitivity(self): cf = self.newconfig() cf.add_section("A") @@ -377,6 +437,7 @@ boolean {0[0]} NO # 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( @@ -593,32 +654,36 @@ boolean {0[0]} NO ) cf = self.fromstring(config_string) - output = io.StringIO() - cf.write(output) - 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=self.delimiters[0], - default_section=self.default_section) - ) - if self.allow_no_value: - expect_string += ( - "[Valueless]\n" - "option-without-value\n" + 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) ) - self.assertEqual(output.getvalue(), expect_string) + 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" @@ -687,15 +752,17 @@ boolean {0[0]} NO "name{equals}%(reference)s\n".format(equals=self.delimiters[0])) def check_items_config(self, expected): - cf = self.fromstring( - "[section]\n" - "name {0[0]} value\n" - "key{0[1]} |%(name)s| \n" - "getdefault{0[1]} |%(default)s|\n".format(self.delimiters), - defaults={"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": ""}) + 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): @@ -739,7 +806,8 @@ class ConfigParserTestCase(BasicTestCase): self.check_items_config([('default', ''), ('getdefault', '||'), ('key', '|value|'), - ('name', 'value')]) + ('name', 'value'), + ('value', 'value')]) def test_safe_interpolation(self): # See http://www.python.org/sf/511737 @@ -866,7 +934,8 @@ class RawConfigParserTestCase(BasicTestCase): self.check_items_config([('default', ''), ('getdefault', '|%(default)s|'), ('key', '|%(name)s|'), - ('name', 'value')]) + ('name', '%(value)s'), + ('value', 'value')]) def test_set_nonstring_types(self): cf = self.newconfig() @@ -970,11 +1039,60 @@ class ConfigParserTestCaseExtendedInterpolation(BasicTestCase): [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}} + """) + + 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_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): @@ -1093,10 +1211,114 @@ class CompatibleTestCase(CfgParserTestCaseClass): ; 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', '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') + 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 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']), '') def test_main(): support.run_unittest( @@ -1114,20 +1336,7 @@ def test_main(): Issue7005TestCase, StrictTestCase, CompatibleTestCase, + CopyTestCase, ConfigParserTestCaseNonStandardDefaultSection, + CoverageOneHundredTestCase, ) - -def test_coverage(coverdir): - trace = support.import_module('trace') - tracer=trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix,], trace=0, - count=1) - tracer.run('test_main()') - r=tracer.results() - print("Writing coverage results...") - r.write_results(show_missing=True, summary=True, coverdir=coverdir) - -if __name__ == "__main__": - if "-c" in sys.argv: - test_coverage('/tmp/configparser.cover') - else: - test_main() diff --git a/Misc/NEWS b/Misc/NEWS index 784eb67983b..c486a5901ca 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -293,6 +293,8 @@ Library - Issue #10467: Fix BytesIO.readinto() after seeking into a position after the end of the file. +- configparser: 100% test coverage. + - Issue #10499: configparser supports pluggable interpolation handlers. The default classic interpolation handler is called BasicInterpolation. Another interpolation handler added (ExtendedInterpolation) which supports the syntax @@ -314,7 +316,9 @@ Library - Issue #9421: configparser's getint(), getfloat() and getboolean() methods accept vars and default arguments just like get() does. -- Issue #9452: configparser supports reading from strings and dictionaries. +- Issue #9452: configparser supports reading from strings and dictionaries + (thanks to the mapping protocol API, the latter can be used to copy data + between parsers). - configparser: accepted INI file structure is now customizable, including comment prefixes, name of the DEFAULT section, empty lines in multiline