Index: Doc/library/configparser.rst =================================================================== --- Doc/library/configparser.rst (revision 83889) +++ Doc/library/configparser.rst (working copy) @@ -365,38 +365,43 @@ 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. + 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. .. versionadded:: 3.2 -.. method:: RawConfigParser.get(section, option) +.. method:: RawConfigParser.get(section, option, vars=None, default=_UNSET) - Get an *option* value for the named *section*. + Get an *option* value for the named *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. -.. method:: RawConfigParser.getint(section, option) +.. method:: RawConfigParser.getint(section, option, vars=None, default=_UNSET) - A convenience method which coerces the *option* in the specified *section* to an - integer. + A convenience method which coerces the *option* in the specified *section* + to an integer. -.. method:: RawConfigParser.getfloat(section, option) +.. method:: RawConfigParser.getfloat(section, option, vars=None, default=_UNSET) - A convenience method which coerces the *option* in the specified *section* to a - floating point number. + A convenience method which coerces the *option* in the specified *section* + to a floating point number. -.. method:: RawConfigParser.getboolean(section, option) +.. method:: RawConfigParser.getboolean(section, option, vars=None, default=_UNSET) - A convenience method which coerces the *option* in the specified *section* to a - Boolean value. Note that the accepted values for the option are ``"1"``, - ``"yes"``, ``"true"``, and ``"on"``, which cause this method to return ``True``, - and ``"0"``, ``"no"``, ``"false"``, and ``"off"``, which cause it to return - ``False``. These string values are checked in a case-insensitive manner. Any - other value will cause it to raise :exc:`ValueError`. + A convenience method which coerces the *option* in the specified *section* + to a Boolean value. Note that the accepted values for the option are + ``"1"``, ``"yes"``, ``"true"``, and ``"on"``, which cause this method to + return ``True``, and ``"0"``, ``"no"``, ``"false"``, and ``"off"``, which + cause it to return ``False``. These string values are checked in + a case-insensitive manner. Any other value will cause it to raise + :exc:`ValueError`. .. method:: RawConfigParser.items(section) @@ -471,17 +476,42 @@ for the interpolation. -.. method:: ConfigParser.get(section, option, raw=False, vars=None) +.. method:: ConfigParser.get(section, option, raw=False, vars=None, default=_UNSET) Get an *option* value for the named *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. + *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 the ``'%'`` interpolations are expanded in the return values, unless the *raw* argument is true. Values for interpolation keys are looked up in the same manner as the option. +.. method:: ConfigParser.getint(section, option, raw=False, vars=None, default=_UNSET) + + A convenience method which coerces the *option* in the specified *section* + to an integer. + + +.. method:: ConfigParser.getfloat(section, option, raw=False, vars=None, default=_UNSET) + + A convenience method which coerces the *option* in the specified *section* + to a floating point number. + + +.. method:: ConfigParser.getboolean(section, option, raw=False, vars=None, default=_UNSET) + + A convenience method which coerces the *option* in the specified *section* + to a Boolean value. Note that the accepted values for the option are + ``"1"``, ``"yes"``, ``"true"``, and ``"on"``, which cause this method to + return ``True``, and ``"0"``, ``"no"``, ``"false"``, and ``"off"``, which + cause it to return ``False``. These string values are checked in + a case-insensitive manner. Any other value will cause it to raise + :exc:`ValueError`. + + .. method:: ConfigParser.items(section, raw=False, vars=None) Return a list of ``(name, value)`` pairs for each option in the given Index: Lib/configparser.py =================================================================== --- Lib/configparser.py (revision 83889) +++ Lib/configparser.py (working copy) @@ -25,7 +25,7 @@ methods: __init__(defaults=None, dict_type=_default_dict, - delimiters=('=', ':'), comment_prefixes=('#', ';'), + delimiters=_COMPATIBLE, comment_prefixes=('#', ';'), strict=False, empty_lines_in_values=True, allow_no_value=False): Create the parser. When `defaults' is given, it is initialized into the dictionary or intrinsic defaults. The keys must be strings, the values @@ -82,22 +82,24 @@ 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. @@ -392,7 +394,15 @@ # Select backwards-compatible inline comment character behavior # (; and # are comments at the start of a line, but ; only inline) _COMPATIBLE = object() + # Used in 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() + # 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=('=', ':'), comment_prefixes=_COMPATIBLE, strict=False, @@ -528,6 +538,7 @@ elements_added.add(section) for key, value in keys.items(): key = self.optionxform(key) + value = str(value) if value is not None else None if self._strict and (section, key) in elements_added: raise DuplicateOptionError(section, key, source) elements_added.add((section, key)) @@ -542,21 +553,29 @@ ) 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 self._UNSET: + raise else: + return default + option = self.optionxform(option) + try: + return d[option] + except KeyError: + if default is self._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,24 +590,18 @@ 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 getint(self, section, option, vars=None, default=_UNSET): + return self._get(section, int, option, vars, default) - def getfloat(self, section, option): - return self._get(section, float, option) + def getfloat(self, section, option, vars=None, default=_UNSET): + return self._get(section, float, option, vars, default) - _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, - '0': False, 'no': False, 'false': False, 'off': False} + def getboolean(self, section, option, vars=None, default=_UNSET): + return self._get(section, self._unify_boolean, option, vars, 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 optionxform(self, optionstr): return optionstr.lower() @@ -797,15 +810,47 @@ 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(): + value = str(value) if value is not None else None + d[self.optionxform(key)] = value + return d + def _unify_boolean(self, value): + """Return a boolean value translating from other types if + necessary. + """ + if type(value) is bool: + # if the provided default= fallback is a bool + return value + elif 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): + _UNSET = RawConfigParser._UNSET + + 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 +858,38 @@ 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 self._UNSET: + raise + else: + return default option = self.optionxform(option) try: value = d[option] except KeyError: - raise NoOptionError(option, section) + if default is self._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): + return self._get(section, int, option, raw, vars, default) + + def getfloat(self, section, option, raw=False, vars=None, default=_UNSET): + return self._get(section, float, option, raw, vars, default) + + def getboolean(self, section, option, raw=False, vars=None, + default=_UNSET): + return self._get(section, self._unify_boolean, option, raw, vars, + default) + def items(self, section, raw=False, vars=None): """Return a list of (name, value) tuples for each option in a section. @@ -893,7 +949,6 @@ else: return "%%(%s)s" % self.optionxform(s) - class SafeConfigParser(ConfigParser): """ConfigParser implementing sane interpolation.""" Index: Lib/test/test_cfgparser.py =================================================================== --- Lib/test/test_cfgparser.py (revision 83889) +++ Lib/test/test_cfgparser.py (working copy) @@ -62,9 +62,10 @@ '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,37 @@ 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) + self.assertAlmostEqual(cf.getfloat('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) + 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.getboolean('Types', 'boolean', default=True), False) + 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 +156,10 @@ [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 +227,12 @@ "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 +770,11 @@ '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'