100% test coverage, better mapping protocol compatibility, some minor bugfixes
This commit is contained in:
parent
0e74cacdff
commit
71b37a5d6d
|
@ -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='<dict>')
|
||||
|
||||
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, ``<dict>`` 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.
|
||||
|
||||
|
||||
|
|
|
@ -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)),
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,23 +654,27 @@ boolean {0[0]} NO
|
|||
)
|
||||
|
||||
cf = self.fromstring(config_string)
|
||||
for space_around_delimiters in (True, False):
|
||||
output = io.StringIO()
|
||||
cf.write(output)
|
||||
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"
|
||||
"foo{equals}another very\n"
|
||||
"\tlong line\n"
|
||||
"\n"
|
||||
"[Long Line]\n"
|
||||
"foo {equals} this line is much, much longer than my editor\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"
|
||||
"test{equals}we\n"
|
||||
"\talso\n"
|
||||
"\tcomments\n"
|
||||
"\tmultiline\n"
|
||||
"\n".format(equals=self.delimiters[0],
|
||||
"\n".format(equals=delimiter,
|
||||
default_section=self.default_section)
|
||||
)
|
||||
if self.allow_no_value:
|
||||
|
@ -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": "<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):
|
||||
|
@ -739,7 +806,8 @@ class ConfigParserTestCase(BasicTestCase):
|
|||
self.check_items_config([('default', '<default>'),
|
||||
('getdefault', '|<default>|'),
|
||||
('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', '<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']), '<Section: 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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue