diff options
author | Fred Drake <fdrake@acm.org> | 2010-09-04 04:35:34 (GMT) |
---|---|---|
committer | Fred Drake <fdrake@acm.org> | 2010-09-04 04:35:34 (GMT) |
commit | cc645b9a59ce56aaa9b411c335c819ce31e26e85 (patch) | |
tree | 44a6d2ca80ec2d5f76de779a5cd63b8ec8bb5be3 /Lib | |
parent | c934f32e0a2b5f9f2bc0b55573a6860626025d63 (diff) | |
download | cpython-cc645b9a59ce56aaa9b411c335c819ce31e26e85.zip cpython-cc645b9a59ce56aaa9b411c335c819ce31e26e85.tar.gz cpython-cc645b9a59ce56aaa9b411c335c819ce31e26e85.tar.bz2 |
add consistent support for the vars and default arguments on all
configuration parser classes
(http://bugs.python.org/issue9421)
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/configparser.py | 196 | ||||
-rw-r--r-- | Lib/test/test_cfgparser.py | 57 |
2 files changed, 196 insertions, 57 deletions
diff --git a/Lib/configparser.py b/Lib/configparser.py index fb39ac3..7f1514f 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -24,9 +24,9 @@ ConfigParser -- responsible for parsing a list of methods: - __init__(defaults=None, dict_type=_default_dict, - delimiters=('=', ':'), comment_prefixes=('#', ';'), - strict=False, empty_lines_in_values=True, allow_no_value=False): + __init__(defaults=None, dict_type=_default_dict, allow_no_value=False, + delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, + strict=False, empty_lines_in_values=True): Create the parser. When `defaults' is given, it is initialized into the dictionary or intrinsic defaults. The keys must be strings, the values must be appropriate for %()s string interpolation. Note that `__name__' @@ -82,22 +82,24 @@ ConfigParser -- responsible for parsing a list of Read 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. + and their keys will be added in order. Values are automatically + converted to strings. - get(section, option, raw=False, vars=None) + get(section, option, raw=False, vars=None, default=_UNSET) Return a string value for the named option. All % interpolations are expanded in the return values, based on the defaults passed into the constructor and the DEFAULT section. Additional substitutions may be provided using the `vars' argument, which must be a dictionary whose - contents override any pre-existing defaults. + contents override any pre-existing defaults. If `option' is a key in + `vars', the value from `vars' is used. - getint(section, options) + getint(section, options, raw=False, vars=None, default=_UNSET) Like get(), but convert value to an integer. - getfloat(section, options) + getfloat(section, options, raw=False, vars=None, default=_UNSET) Like get(), but convert value to a float. - getboolean(section, options) + getboolean(section, options, raw=False, vars=None, default=_UNSET) Like get(), but convert value to a boolean (currently case insensitively defined as 0, false, no, off for False, and 1, true, yes, on for True). Returns False or True. @@ -353,6 +355,17 @@ class MissingSectionHeaderError(ParsingError): self.args = (filename, lineno, line) +# Used in parsers to denote selecting a backwards-compatible inline comment +# character behavior (; and # are comments at the start of a line, but ; only +# inline) +_COMPATIBLE = object() + +# Used in parser getters to indicate the default behaviour when a specific +# option is not found it to raise an exception. Created to enable `None' as +# a valid fallback value. +_UNSET = object() + + class RawConfigParser: """ConfigParser that does not do interpolation.""" @@ -389,9 +402,9 @@ class RawConfigParser: OPTCRE_NV = re.compile(_OPT_NV_TMPL.format(delim="=|:"), re.VERBOSE) # Compiled regular expression for matching leading whitespace in a line NONSPACECRE = re.compile(r"\S") - # Select backwards-compatible inline comment character behavior - # (; and # are comments at the start of a line, but ; only inline) - _COMPATIBLE = object() + # Possible boolean values in the configuration. + BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True, + '0': False, 'no': False, 'false': False, 'off': False} def __init__(self, defaults=None, dict_type=_default_dict, allow_no_value=False, *, delimiters=('=', ':'), @@ -414,7 +427,7 @@ class RawConfigParser: else: self._optcre = re.compile(self._OPT_TMPL.format(delim=d), re.VERBOSE) - if comment_prefixes is self._COMPATIBLE: + if comment_prefixes is _COMPATIBLE: self._startonly_comment_prefixes = ('#',) self._comment_prefixes = (';',) else: @@ -528,6 +541,8 @@ class RawConfigParser: elements_added.add(section) for key, value in keys.items(): key = self.optionxform(key) + if value is not None: + value = str(value) if self._strict and (section, key) in elements_added: raise DuplicateOptionError(section, key, source) elements_added.add((section, key)) @@ -542,21 +557,29 @@ class RawConfigParser: ) self.read_file(fp, source=filename) - def get(self, section, option): - opt = self.optionxform(option) - if section not in self._sections: - if section != DEFAULTSECT: - raise NoSectionError(section) - if opt in self._defaults: - return self._defaults[opt] + def get(self, section, option, vars=None, default=_UNSET): + """Get an option value for a given section. + + If `vars' is provided, it must be a dictionary. The option is looked up + in `vars' (if provided), `section', and in `DEFAULTSECT' in that order. + If the key is not found and `default' is provided, it is used as + a fallback value. `None' can be provided as a `default' value. + """ + try: + d = self._unify_values(section, vars) + except NoSectionError: + if default is _UNSET: + raise else: + return default + option = self.optionxform(option) + try: + return d[option] + except KeyError: + if default is _UNSET: raise NoOptionError(option, section) - elif opt in self._sections[section]: - return self._sections[section][opt] - elif opt in self._defaults: - return self._defaults[opt] - else: - raise NoOptionError(option, section) + else: + return default def items(self, section): try: @@ -571,23 +594,35 @@ class RawConfigParser: del d["__name__"] return d.items() - def _get(self, section, conv, option): - return conv(self.get(section, option)) + def _get(self, section, conv, option, *args, **kwargs): + return conv(self.get(section, option, *args, **kwargs)) - def getint(self, section, option): - return self._get(section, int, option) - - def getfloat(self, section, option): - return self._get(section, float, option) + def getint(self, section, option, vars=None, default=_UNSET): + try: + return self._get(section, int, option, vars) + except (NoSectionError, NoOptionError): + if default is _UNSET: + raise + else: + return default - _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, - '0': False, 'no': False, 'false': False, 'off': False} + def getfloat(self, section, option, vars=None, default=_UNSET): + try: + return self._get(section, float, option, vars) + except (NoSectionError, NoOptionError): + if default is _UNSET: + raise + else: + return default - def getboolean(self, section, option): - v = self.get(section, option) - if v.lower() not in self._boolean_states: - raise ValueError('Not a boolean: %s' % v) - return self._boolean_states[v.lower()] + def getboolean(self, section, option, vars=None, default=_UNSET): + try: + return self._get(section, self._convert_to_boolean, option, vars) + except (NoSectionError, NoOptionError): + if default is _UNSET: + raise + else: + return default def optionxform(self, optionstr): return optionstr.lower() @@ -797,15 +832,43 @@ class RawConfigParser: exc.append(lineno, repr(line)) return exc + def _unify_values(self, section, vars): + """Create a copy of the DEFAULTSECT with values from a specific + `section' and the `vars' dictionary. If provided, values in `vars' + take precendence. + """ + d = self._defaults.copy() + try: + d.update(self._sections[section]) + except KeyError: + if section != DEFAULTSECT: + raise NoSectionError(section) + # Update with the entry specific variables + if vars: + for key, value in vars.items(): + if value is not None: + value = str(value) + d[self.optionxform(key)] = value + return d + + def _convert_to_boolean(self, value): + """Return a boolean value translating from other types if necessary. + """ + if value.lower() not in self.BOOLEAN_STATES: + raise ValueError('Not a boolean: %s' % value) + return self.BOOLEAN_STATES[value.lower()] + class ConfigParser(RawConfigParser): """ConfigParser implementing interpolation.""" - def get(self, section, option, raw=False, vars=None): + def get(self, section, option, raw=False, vars=None, default=_UNSET): """Get an option value for a given section. If `vars' is provided, it must be a dictionary. The option is looked up - in `vars' (if provided), `section', and in `defaults' in that order. + in `vars' (if provided), `section', and in `DEFAULTSECT' in that order. + If the key is not found and `default' is provided, it is used as + a fallback value. `None' can be provided as a `default' value. All % interpolations are expanded in the return values, unless the optional argument `raw' is true. Values for interpolation keys are @@ -813,27 +876,56 @@ class ConfigParser(RawConfigParser): The section DEFAULT is special. """ - d = self._defaults.copy() try: - d.update(self._sections[section]) - except KeyError: - if section != DEFAULTSECT: - raise NoSectionError(section) - # Update with the entry specific variables - if vars: - for key, value in vars.items(): - d[self.optionxform(key)] = value + d = self._unify_values(section, vars) + except NoSectionError: + if default is _UNSET: + raise + else: + return default option = self.optionxform(option) try: value = d[option] except KeyError: - raise NoOptionError(option, section) + if default is _UNSET: + raise NoOptionError(option, section) + else: + return default if raw or value is None: return value else: return self._interpolate(section, option, value, d) + def getint(self, section, option, raw=False, vars=None, default=_UNSET): + try: + return self._get(section, int, option, raw, vars) + except (NoSectionError, NoOptionError): + if default is _UNSET: + raise + else: + return default + + def getfloat(self, section, option, raw=False, vars=None, default=_UNSET): + try: + return self._get(section, float, option, raw, vars) + except (NoSectionError, NoOptionError): + if default is _UNSET: + raise + else: + return default + + def getboolean(self, section, option, raw=False, vars=None, + default=_UNSET): + try: + return self._get(section, self._convert_to_boolean, option, raw, + vars) + except (NoSectionError, NoOptionError): + if default is _UNSET: + raise + else: + return default + def items(self, section, raw=False, vars=None): """Return a list of (name, value) tuples for each option in a section. diff --git a/Lib/test/test_cfgparser.py b/Lib/test/test_cfgparser.py index a20678d..3079cfb 100644 --- a/Lib/test/test_cfgparser.py +++ b/Lib/test/test_cfgparser.py @@ -62,9 +62,10 @@ class BasicTestCase(CfgParserTestCaseClass): 'Spaces', 'Spacey Bar', 'Spacey Bar From The Beginning', + 'Types', ] if self.allow_no_value: - E.append(r'NoValue') + E.append('NoValue') E.sort() eq = self.assertEqual eq(L, E) @@ -80,9 +81,43 @@ class BasicTestCase(CfgParserTestCaseClass): 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) if self.allow_no_value: eq(cf.get('NoValue', 'option-without-value'), None) + # test vars= and default= + eq(cf.get('Foo Bar', 'foo', default='baz'), 'bar') + 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', default='baz'), 'baz') + eq(cf.get('Foo Bar', 'no-such-foo', default='baz'), 'baz') + eq(cf.get('Spacey Bar', 'foo', default=None), 'bar') + eq(cf.get('No Such Spacey Bar', 'foo', default=None), None) + eq(cf.getint('Types', 'int', default=18), 42) + eq(cf.getint('Types', 'no-such-int', default=18), 18) + eq(cf.getint('Types', 'no-such-int', default="18"), "18") # sic! + self.assertAlmostEqual(cf.getfloat('Types', 'float', + default=0.0), 0.44) + self.assertAlmostEqual(cf.getfloat('Types', 'no-such-float', + default=0.0), 0.0) + eq(cf.getfloat('Types', 'no-such-float', default="0.0"), "0.0") # sic! + eq(cf.getboolean('Types', 'boolean', default=True), False) + eq(cf.getboolean('Types', 'no-such-boolean', default="yes"), + "yes") # sic! + eq(cf.getboolean('Types', 'no-such-boolean', default=True), True) + eq(cf.getboolean('No Such Types', 'boolean', default=True), True) + if self.allow_no_value: + eq(cf.get('NoValue', 'option-without-value', default=False), None) + eq(cf.get('NoValue', 'no-such-option-without-value', + default=False), False) + self.assertNotIn('__name__', cf.options("Foo Bar"), '__name__ "option" should not be exposed by the API!') @@ -127,6 +162,10 @@ 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 """.format(self.delimiters, self.comment_prefixes) if self.allow_no_value: config_string += ( @@ -194,7 +233,12 @@ another with spaces {0[0]} splat! "Spaces": { "key with spaces": "value", "another with spaces": "splat!", - } + }, + "Types": { + "int": 42, + "float": 0.44, + "boolean": False, + }, } if self.allow_no_value: config.update({ @@ -732,8 +776,11 @@ class SafeConfigParserTestCaseTrickyFile(CfgParserTestCaseClass): 'no values here', 'tricky interpolation', 'more interpolation']) - #self.assertEqual(cf.getint('DEFAULT', 'go', vars={'interpolate': '-1'}), - # -1) + self.assertEqual(cf.getint('DEFAULT', 'go', + vars={'interpolate': '-1'}), -1) + with self.assertRaises(ValueError): + # no interpolation will happen + cf.getint('DEFAULT', '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' @@ -808,7 +855,7 @@ class SortedTestCase(RawConfigParserTestCase): class CompatibleTestCase(CfgParserTestCaseClass): config_class = configparser.RawConfigParser - comment_prefixes = configparser.RawConfigParser._COMPATIBLE + comment_prefixes = configparser._COMPATIBLE def test_comment_handling(self): config_string = textwrap.dedent("""\ |