[Python-checkins] cpython (merge default -> default): Closes #18159: ConfigParser getters not available on SectionProxy

lukasz.langa python-checkins at python.org
Mon Sep 15 11:10:55 CEST 2014


http://hg.python.org/cpython/rev/5eb95d41ee43
changeset:   92434:5eb95d41ee43
parent:      92430:49e4e3b74334
parent:      92433:2c46a4ded259
user:        Łukasz Langa <lukasz at langa.pl>
date:        Mon Sep 15 02:10:01 2014 -0700
summary:
  Closes #18159: ConfigParser getters not available on SectionProxy

files:
  Doc/library/configparser.rst  |  118 +++++++----
  Lib/configparser.py           |  170 ++++++++++++----
  Lib/test/test_configparser.py |  223 ++++++++++++++++++++++
  3 files changed, 423 insertions(+), 88 deletions(-)


diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst
--- a/Doc/library/configparser.rst
+++ b/Doc/library/configparser.rst
@@ -144,12 +144,13 @@
    >>> float(topsecret['CompressionLevel'])
    9.0
 
-Extracting Boolean values is not that simple, though.  Passing the value
-to ``bool()`` would do no good since ``bool('False')`` is still
-``True``.  This is why config parsers also provide :meth:`getboolean`.
-This method is case-insensitive and recognizes Boolean values from
-``'yes'``/``'no'``, ``'on'``/``'off'`` and ``'1'``/``'0'`` [1]_.
-For example:
+Since this task is so common, config parsers provide a range of handy getter
+methods to handle integers, floats and booleans.  The last one is the most
+interesting because simply passing the value to ``bool()`` would do no good
+since ``bool('False')`` is still ``True``.  This is why config parsers also
+provide :meth:`getboolean`.  This method is case-insensitive and recognizes
+Boolean values from ``'yes'``/``'no'``, ``'on'``/``'off'``,
+``'true'``/``'false'`` and ``'1'``/``'0'`` [1]_.  For example:
 
 .. doctest::
 
@@ -161,10 +162,8 @@
    True
 
 Apart from :meth:`getboolean`, config parsers also provide equivalent
-:meth:`getint` and :meth:`getfloat` methods, but these are far less
-useful since conversion using :func:`int` and :func:`float` is
-sufficient for these types.
-
+:meth:`getint` and :meth:`getfloat` methods.  You can register your own
+converters and customize the provided ones. [1]_
 
 Fallback Values
 ---------------
@@ -319,11 +318,11 @@
 .. class:: ExtendedInterpolation()
 
    An alternative handler for interpolation which implements a more advanced
-   syntax, used for instance in ``zc.buildout``. Extended interpolation is
+   syntax, used for instance in ``zc.buildout``.  Extended interpolation is
    using ``${section:option}`` to denote a value from a foreign section.
-   Interpolation can span multiple levels. For convenience, if the ``section:``
-   part is omitted, interpolation defaults to the current section (and possibly
-   the default values from the special section).
+   Interpolation can span multiple levels.  For convenience, if the
+   ``section:`` part is omitted, interpolation defaults to the current section
+   (and possibly the default values from the special section).
 
    For example, the configuration specified above with basic interpolation,
    would look like this with extended interpolation:
@@ -401,13 +400,13 @@
   * ``parser.popitem()`` never returns it.
 
 * ``parser.get(section, option, **kwargs)`` - the second argument is **not**
-  a fallback value. Note however that the section-level ``get()`` methods are
+  a fallback value.  Note however that the section-level ``get()`` methods are
   compatible both with the mapping protocol and the classic configparser API.
 
 * ``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
+  vars)``.  The latter call returns a list of *option*, *value* pairs for
   a specified ``section``, with all interpolations expanded (unless
   ``raw=True`` is provided).
 
@@ -541,9 +540,9 @@
 
 * *delimiters*, default value: ``('=', ':')``
 
-  Delimiters are substrings that delimit keys from values within a section. The
-  first occurrence of a delimiting substring on a line is considered a delimiter.
-  This means values (but not keys) can contain the delimiters.
+  Delimiters are substrings that delimit keys from values within a section.
+  The first occurrence of a delimiting substring on a line is considered
+  a delimiter.  This means values (but not keys) can contain the delimiters.
 
   See also the *space_around_delimiters* argument to
   :meth:`ConfigParser.write`.
@@ -555,7 +554,7 @@
   Comment prefixes are strings that indicate the start of a valid comment within
   a config file. *comment_prefixes* are used only on otherwise empty lines
   (optionally indented) whereas *inline_comment_prefixes* can be used after
-  every valid value (e.g.  section names, options and empty lines as well). By
+  every valid value (e.g. section names, options and empty lines as well).  By
   default inline comments are disabled and ``'#'`` and ``';'`` are used as
   prefixes for whole line comments.
 
@@ -565,10 +564,10 @@
 
   Please note that config parsers don't support escaping of comment prefixes so
   using *inline_comment_prefixes* may prevent users from specifying option
-  values with characters used as comment prefixes. When in doubt, avoid setting
-  *inline_comment_prefixes*. In any circumstances, the only way of storing
-  comment prefix characters at the beginning of a line in multiline values is to
-  interpolate the prefix, for example::
+  values with characters used as comment prefixes.  When in doubt, avoid
+  setting *inline_comment_prefixes*.  In any circumstances, the only way of
+  storing comment prefix characters at the beginning of a line in multiline
+  values is to interpolate the prefix, for example::
 
     >>> from configparser import ConfigParser, ExtendedInterpolation
     >>> parser = ConfigParser(interpolation=ExtendedInterpolation())
@@ -613,7 +612,7 @@
 
   When set to ``True``, the parser will not allow for any section or option
   duplicates while reading from a single source (using :meth:`read_file`,
-  :meth:`read_string` or :meth:`read_dict`). It is recommended to use strict
+  :meth:`read_string` or :meth:`read_dict`).  It is recommended to use strict
   parsers in new applications.
 
   .. versionchanged:: 3.2
@@ -648,12 +647,12 @@
 
   The convention of allowing a special section of default values for other
   sections or interpolation purposes is a powerful concept of this library,
-  letting users create complex declarative configurations. This section is
+  letting users create complex declarative configurations.  This section is
   normally called ``"DEFAULT"`` but this can be customized to point to any
-  other valid section name. Some typical values include: ``"general"`` or
-  ``"common"``. The name provided is used for recognizing default sections when
-  reading from any source and is used when writing configuration back to
-  a file. Its current value can be retrieved using the
+  other valid section name.  Some typical values include: ``"general"`` or
+  ``"common"``.  The name provided is used for recognizing default sections
+  when reading from any source and is used when writing configuration back to
+  a file.  Its current value can be retrieved using the
   ``parser_instance.default_section`` attribute and may be modified at runtime
   (i.e. to convert files from one format to another).
 
@@ -662,14 +661,30 @@
   Interpolation behaviour may be customized by providing a custom handler
   through the *interpolation* argument. ``None`` can be used to turn off
   interpolation completely, ``ExtendedInterpolation()`` provides a more
-  advanced variant inspired by ``zc.buildout``. More on the subject in the
+  advanced variant inspired by ``zc.buildout``.  More on the subject in the
   `dedicated documentation section <#interpolation-of-values>`_.
   :class:`RawConfigParser` has a default value of ``None``.
 
+* *converters*, default value: not set
+
+  Config parsers provide option value getters that perform type conversion.  By
+  default :meth:`getint`, :meth:`getfloat`, and :meth:`getboolean` are
+  implemented.  Should other getters be desirable, users may define them in
+  a subclass or pass a dictionary where each key is a name of the converter and
+  each value is a callable implementing said conversion.  For instance, passing
+  ``{'decimal': decimal.Decimal}`` would add :meth:`getdecimal` on both the
+  parser object and all section proxies.  In other words, it will be possible
+  to write both ``parser_instance.getdecimal('section', 'key', fallback=0)``
+  and ``parser_instance['section'].getdecimal('key', 0)``.
+
+  If the converter needs to access the state of the parser, it can be
+  implemented as a method on a config parser subclass.  If the name of this
+  method starts with ``get``, it will be available on all section proxies, in
+  the dict-compatible form (see the ``getdecimal()`` example above).
 
 More advanced customization may be achieved by overriding default values of
-these parser attributes.  The defaults are defined on the classes, so they
-may be overridden by subclasses or by attribute assignment.
+these parser attributes.  The defaults are defined on the classes, so they may
+be overridden by subclasses or by attribute assignment.
 
 .. attribute:: BOOLEAN_STATES
 
@@ -727,10 +742,11 @@
 
 .. attribute:: SECTCRE
 
-  A compiled regular expression used to parse section headers. The default
-  matches ``[section]`` to the name ``"section"``. Whitespace is considered part
-  of the section name, thus ``[  larch  ]`` will be read as a section of name
-  ``"  larch  "``. Override this attribute if that's unsuitable.  For example:
+  A compiled regular expression used to parse section headers.  The default
+  matches ``[section]`` to the name ``"section"``.  Whitespace is considered
+  part of the section name, thus ``[  larch  ]`` will be read as a section of
+  name ``"  larch  "``.  Override this attribute if that's unsuitable.  For
+  example:
 
   .. doctest::
 
@@ -861,7 +877,7 @@
 ConfigParser Objects
 --------------------
 
-.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT, interpolation=BasicInterpolation())
+.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT, interpolation=BasicInterpolation(), converters={})
 
    The main configuration parser.  When *defaults* is given, it is initialized
    into the dictionary of intrinsic defaults.  When *dict_type* is given, it
@@ -871,8 +887,8 @@
    When *delimiters* is given, it is used as the set of substrings that
    divide keys from values.  When *comment_prefixes* is given, it will be used
    as the set of substrings that prefix comments in otherwise empty lines.
-   Comments can be indented. When *inline_comment_prefixes* is given, it will be
-   used as the set of substrings that prefix comments in non-empty lines.
+   Comments can be indented.  When *inline_comment_prefixes* is given, it will
+   be used as the set of substrings that prefix comments in non-empty lines.
 
    When *strict* is ``True`` (the default), the parser won't allow for
    any section or option duplicates while reading from a single source (file,
@@ -886,13 +902,13 @@
 
    When *default_section* is given, it specifies the name for the special
    section holding default values for other sections and interpolation purposes
-   (normally named ``"DEFAULT"``). This value can be retrieved and changed on
+   (normally named ``"DEFAULT"``).  This value can be retrieved and changed on
    runtime using the ``default_section`` instance attribute.
 
    Interpolation behaviour may be customized by providing a custom handler
    through the *interpolation* argument. ``None`` can be used to turn off
    interpolation completely, ``ExtendedInterpolation()`` provides a more
-   advanced variant inspired by ``zc.buildout``. More on the subject in the
+   advanced variant inspired by ``zc.buildout``.  More on the subject in the
    `dedicated documentation section <#interpolation-of-values>`_.
 
    All option names used in interpolation will be passed through the
@@ -901,6 +917,12 @@
    converts option names to lower case), the values ``foo %(bar)s`` and ``foo
    %(BAR)s`` are equivalent.
 
+   When *converters* is given, it should be a dictionary where each key
+   represents the name of a type converter and each value is a callable
+   implementing the conversion from string to the desired datatype.  Every
+   converter gets its own corresponding :meth:`get*()` method on the parser
+   object and section proxies.
+
    .. versionchanged:: 3.1
       The default *dict_type* is :class:`collections.OrderedDict`.
 
@@ -909,6 +931,9 @@
       *empty_lines_in_values*, *default_section* and *interpolation* were
       added.
 
+   .. versionchanged:: 3.5
+      The *converters* argument was added.
+
 
    .. method:: defaults()
 
@@ -946,7 +971,7 @@
    .. method:: has_option(section, option)
 
       If the given *section* exists, and contains the given *option*, return
-      :const:`True`; otherwise return :const:`False`. If the specified
+      :const:`True`; otherwise return :const:`False`.  If the specified
       *section* is :const:`None` or an empty string, DEFAULT is assumed.
 
 
@@ -1071,7 +1096,7 @@
       :meth:`get` method.
 
       .. versionchanged:: 3.2
-         Items present in *vars* no longer appear in the result. The previous
+         Items present in *vars* no longer appear in the result.  The previous
          behaviour mixed actual parser options with variables provided for
          interpolation.
 
@@ -1172,7 +1197,7 @@
 
    .. note::
       Consider using :class:`ConfigParser` instead which checks types of
-      the values to be stored internally. If you don't want interpolation, you
+      the values to be stored internally.  If you don't want interpolation, you
       can use ``ConfigParser(interpolation=None)``.
 
 
@@ -1183,7 +1208,7 @@
       *default section* name is passed, :exc:`ValueError` is raised.
 
       Type of *section* is not checked which lets users create non-string named
-      sections. This behaviour is unsupported and may cause internal errors.
+      sections.  This behaviour is unsupported and may cause internal errors.
 
 
    .. method:: set(section, option, value)
@@ -1284,3 +1309,4 @@
 .. [1] Config parsers allow for heavy customization.  If you are interested in
        changing the behaviour outlined by the footnote reference, consult the
        `Customizing Parser Behaviour`_ section.
+
diff --git a/Lib/configparser.py b/Lib/configparser.py
--- a/Lib/configparser.py
+++ b/Lib/configparser.py
@@ -17,7 +17,8 @@
     __init__(defaults=None, dict_type=_default_dict, allow_no_value=False,
              delimiters=('=', ':'), comment_prefixes=('#', ';'),
              inline_comment_prefixes=None, strict=True,
-             empty_lines_in_values=True):
+             empty_lines_in_values=True, default_section='DEFAULT',
+             interpolation=<unset>, converters=<unset>):
         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.
@@ -47,6 +48,25 @@
         When `allow_no_value' is True (default: False), options without
         values are accepted; the value presented for these is None.
 
+        When `default_section' is given, the name of the special section is
+        named accordingly. By default it is called ``"DEFAULT"`` but this can
+        be customized to point to any other valid section name. Its current
+        value can be retrieved using the ``parser_instance.default_section``
+        attribute and may be modified at runtime.
+
+        When `interpolation` is given, it should be an Interpolation subclass
+        instance. It will be used as the handler for option value
+        pre-processing when using getters. RawConfigParser object s don't do
+        any sort of interpolation, whereas ConfigParser uses an instance of
+        BasicInterpolation. The library also provides a ``zc.buildbot``
+        inspired ExtendedInterpolation implementation.
+
+        When `converters` is given, it should be a dictionary where each key
+        represents the name of a type converter and each value is a callable
+        implementing the conversion from string to the desired datatype. Every
+        converter gets its corresponding get*() method on the parser object and
+        section proxies.
+
     sections()
         Return all the configuration section names, sans DEFAULT.
 
@@ -129,9 +149,11 @@
 
 __all__ = ["NoSectionError", "DuplicateOptionError", "DuplicateSectionError",
            "NoOptionError", "InterpolationError", "InterpolationDepthError",
-           "InterpolationSyntaxError", "ParsingError",
-           "MissingSectionHeaderError",
+           "InterpolationMissingOptionError", "InterpolationSyntaxError",
+           "ParsingError", "MissingSectionHeaderError",
            "ConfigParser", "SafeConfigParser", "RawConfigParser",
+           "Interpolation", "BasicInterpolation",  "ExtendedInterpolation",
+           "LegacyInterpolation", "SectionProxy", "ConverterMapping",
            "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH"]
 
 DEFAULTSECT = "DEFAULT"
@@ -580,11 +602,12 @@
                  comment_prefixes=('#', ';'), inline_comment_prefixes=None,
                  strict=True, empty_lines_in_values=True,
                  default_section=DEFAULTSECT,
-                 interpolation=_UNSET):
+                 interpolation=_UNSET, converters=_UNSET):
 
         self._dict = dict_type
         self._sections = self._dict()
         self._defaults = self._dict()
+        self._converters = ConverterMapping(self)
         self._proxies = self._dict()
         self._proxies[default_section] = SectionProxy(self, default_section)
         if defaults:
@@ -612,6 +635,8 @@
             self._interpolation = self._DEFAULT_INTERPOLATION
         if self._interpolation is None:
             self._interpolation = Interpolation()
+        if converters is not _UNSET:
+            self._converters.update(converters)
 
     def defaults(self):
         return self._defaults
@@ -775,36 +800,31 @@
     def _get(self, section, conv, option, **kwargs):
         return conv(self.get(section, option, **kwargs))
 
-    def getint(self, section, option, *, raw=False, vars=None,
-               fallback=_UNSET):
+    def _get_conv(self, section, option, conv, *, raw=False, vars=None,
+                  fallback=_UNSET, **kwargs):
         try:
-            return self._get(section, int, option, raw=raw, vars=vars)
+            return self._get(section, conv, option, raw=raw, vars=vars,
+                             **kwargs)
         except (NoSectionError, NoOptionError):
             if fallback is _UNSET:
                 raise
-            else:
-                return fallback
+            return fallback
+
+    # getint, getfloat and getboolean provided directly for backwards compat
+    def getint(self, section, option, *, raw=False, vars=None,
+               fallback=_UNSET, **kwargs):
+        return self._get_conv(section, option, int, raw=raw, vars=vars,
+                              fallback=fallback, **kwargs)
 
     def getfloat(self, section, option, *, raw=False, vars=None,
-                 fallback=_UNSET):
-        try:
-            return self._get(section, float, option, raw=raw, vars=vars)
-        except (NoSectionError, NoOptionError):
-            if fallback is _UNSET:
-                raise
-            else:
-                return fallback
+                 fallback=_UNSET, **kwargs):
+        return self._get_conv(section, option, float, raw=raw, vars=vars,
+                              fallback=fallback, **kwargs)
 
     def getboolean(self, section, option, *, raw=False, vars=None,
-                   fallback=_UNSET):
-        try:
-            return self._get(section, self._convert_to_boolean, option,
-                             raw=raw, vars=vars)
-        except (NoSectionError, NoOptionError):
-            if fallback is _UNSET:
-                raise
-            else:
-                return fallback
+                   fallback=_UNSET, **kwargs):
+        return self._get_conv(section, option, self._convert_to_boolean,
+                              raw=raw, vars=vars, fallback=fallback, **kwargs)
 
     def items(self, section=_UNSET, raw=False, vars=None):
         """Return a list of (name, value) tuples for each option in a section.
@@ -1154,6 +1174,10 @@
             if not isinstance(value, str):
                 raise TypeError("option values must be strings")
 
+    @property
+    def converters(self):
+        return self._converters
+
 
 class ConfigParser(RawConfigParser):
     """ConfigParser implementing interpolation."""
@@ -1194,6 +1218,10 @@
         """Creates a view on a section of the specified `name` in `parser`."""
         self._parser = parser
         self._name = name
+        for conv in parser.converters:
+            key = 'get' + conv
+            getter = functools.partial(self.get, _impl=getattr(parser, key))
+            setattr(self, key, getter)
 
     def __repr__(self):
         return '<Section: {}>'.format(self._name)
@@ -1227,22 +1255,6 @@
         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,
-                                fallback=fallback)
-
-    def getint(self, option, fallback=None, *, raw=False, vars=None):
-        return self._parser.getint(self._name, option, raw=raw, vars=vars,
-                                   fallback=fallback)
-
-    def getfloat(self, option, fallback=None, *, raw=False, vars=None):
-        return self._parser.getfloat(self._name, option, raw=raw, vars=vars,
-                                     fallback=fallback)
-
-    def getboolean(self, option, fallback=None, *, raw=False, vars=None):
-        return self._parser.getboolean(self._name, option, raw=raw, vars=vars,
-                                       fallback=fallback)
-
     @property
     def parser(self):
         # The parser object of the proxy is read-only.
@@ -1252,3 +1264,77 @@
     def name(self):
         # The name of the section on a proxy is read-only.
         return self._name
+
+    def get(self, option, fallback=None, *, raw=False, vars=None,
+            _impl=None, **kwargs):
+        """Get an option value.
+
+        Unless `fallback` is provided, `None` will be returned if the option
+        is not found.
+
+        """
+        # If `_impl` is provided, it should be a getter method on the parser
+        # object that provides the desired type conversion.
+        if not _impl:
+            _impl = self._parser.get
+        return _impl(self._name, option, raw=raw, vars=vars,
+                     fallback=fallback, **kwargs)
+
+
+class ConverterMapping(MutableMapping):
+    """Enables reuse of get*() methods between the parser and section proxies.
+
+    If a parser class implements a getter directly, the value for the given
+    key will be ``None``. The presence of the converter name here enables
+    section proxies to find and use the implementation on the parser class.
+    """
+
+    GETTERCRE = re.compile(r"^get(?P<name>.+)$")
+
+    def __init__(self, parser):
+        self._parser = parser
+        self._data = {}
+        for getter in dir(self._parser):
+            m = self.GETTERCRE.match(getter)
+            if not m or not callable(getattr(self._parser, getter)):
+                continue
+            self._data[m.group('name')] = None   # See class docstring.
+
+    def __getitem__(self, key):
+        return self._data[key]
+
+    def __setitem__(self, key, value):
+        try:
+            k = 'get' + key
+        except TypeError:
+            raise ValueError('Incompatible key: {} (type: {})'
+                             ''.format(key, type(key)))
+        if k == 'get':
+            raise ValueError('Incompatible key: cannot use "" as a name')
+        self._data[key] = value
+        func = functools.partial(self._parser._get_conv, conv=value)
+        func.converter = value
+        setattr(self._parser, k, func)
+        for proxy in self._parser.values():
+            getter = functools.partial(proxy.get, _impl=func)
+            setattr(proxy, k, getter)
+
+    def __delitem__(self, key):
+        try:
+            k = 'get' + (key or None)
+        except TypeError:
+            raise KeyError(key)
+        del self._data[key]
+        for inst in itertools.chain((self._parser,), self._parser.values()):
+            try:
+                delattr(inst, k)
+            except AttributeError:
+                # don't raise since the entry was present in _data, silently
+                # clean up
+                continue
+
+    def __iter__(self):
+        return iter(self._data)
+
+    def __len__(self):
+        return len(self._data)
diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py
--- a/Lib/test/test_configparser.py
+++ b/Lib/test/test_configparser.py
@@ -1584,6 +1584,34 @@
         """)
         self.assertEqual(repr(parser['section']), '<Section: section>')
 
+    def test_inconsistent_converters_state(self):
+        parser = configparser.ConfigParser()
+        import decimal
+        parser.converters['decimal'] = decimal.Decimal
+        parser.read_string("""
+            [s1]
+            one = 1
+            [s2]
+            two = 2
+        """)
+        self.assertIn('decimal', parser.converters)
+        self.assertEqual(parser.getdecimal('s1', 'one'), 1)
+        self.assertEqual(parser.getdecimal('s2', 'two'), 2)
+        self.assertEqual(parser['s1'].getdecimal('one'), 1)
+        self.assertEqual(parser['s2'].getdecimal('two'), 2)
+        del parser.getdecimal
+        with self.assertRaises(AttributeError):
+            parser.getdecimal('s1', 'one')
+        self.assertIn('decimal', parser.converters)
+        del parser.converters['decimal']
+        self.assertNotIn('decimal', parser.converters)
+        with self.assertRaises(AttributeError):
+            parser.getdecimal('s1', 'one')
+        with self.assertRaises(AttributeError):
+            parser['s1'].getdecimal('one')
+        with self.assertRaises(AttributeError):
+            parser['s2'].getdecimal('two')
+
 
 class ExceptionPicklingTestCase(unittest.TestCase):
     """Tests for issue #13760: ConfigParser exceptions are not picklable."""
@@ -1763,6 +1791,7 @@
         self.assertEqual(s['k2'], 'v2')
         self.assertEqual(s['k3'], 'v3;#//still v3# and still v3')
 
+
 class ExceptionContextTestCase(unittest.TestCase):
     """ Test that implementation details doesn't leak
     through raising exceptions. """
@@ -1816,5 +1845,199 @@
             config.remove_option('Section1', 'an_int')
         self.assertIs(cm.exception.__suppress_context__, True)
 
+
+class ConvertersTestCase(BasicTestCase, unittest.TestCase):
+    """Introduced in 3.5, issue #18159."""
+
+    config_class = configparser.ConfigParser
+
+    def newconfig(self, defaults=None):
+        instance = super().newconfig(defaults=defaults)
+        instance.converters['list'] = lambda v: [e.strip() for e in v.split()
+                                                 if e.strip()]
+        return instance
+
+    def test_converters(self):
+        cfg = self.newconfig()
+        self.assertIn('boolean', cfg.converters)
+        self.assertIn('list', cfg.converters)
+        self.assertIsNone(cfg.converters['int'])
+        self.assertIsNone(cfg.converters['float'])
+        self.assertIsNone(cfg.converters['boolean'])
+        self.assertIsNotNone(cfg.converters['list'])
+        self.assertEqual(len(cfg.converters), 4)
+        with self.assertRaises(ValueError):
+            cfg.converters[''] = lambda v: v
+        with self.assertRaises(ValueError):
+            cfg.converters[None] = lambda v: v
+        cfg.read_string("""
+        [s]
+        str = string
+        int = 1
+        float = 0.5
+        list = a b c d e f g
+        bool = yes
+        """)
+        s = cfg['s']
+        self.assertEqual(s['str'], 'string')
+        self.assertEqual(s['int'], '1')
+        self.assertEqual(s['float'], '0.5')
+        self.assertEqual(s['list'], 'a b c d e f g')
+        self.assertEqual(s['bool'], 'yes')
+        self.assertEqual(cfg.get('s', 'str'), 'string')
+        self.assertEqual(cfg.get('s', 'int'), '1')
+        self.assertEqual(cfg.get('s', 'float'), '0.5')
+        self.assertEqual(cfg.get('s', 'list'), 'a b c d e f g')
+        self.assertEqual(cfg.get('s', 'bool'), 'yes')
+        self.assertEqual(cfg.get('s', 'str'), 'string')
+        self.assertEqual(cfg.getint('s', 'int'), 1)
+        self.assertEqual(cfg.getfloat('s', 'float'), 0.5)
+        self.assertEqual(cfg.getlist('s', 'list'), ['a', 'b', 'c', 'd',
+                                                    'e', 'f', 'g'])
+        self.assertEqual(cfg.getboolean('s', 'bool'), True)
+        self.assertEqual(s.get('str'), 'string')
+        self.assertEqual(s.getint('int'), 1)
+        self.assertEqual(s.getfloat('float'), 0.5)
+        self.assertEqual(s.getlist('list'), ['a', 'b', 'c', 'd',
+                                             'e', 'f', 'g'])
+        self.assertEqual(s.getboolean('bool'), True)
+        with self.assertRaises(AttributeError):
+            cfg.getdecimal('s', 'float')
+        with self.assertRaises(AttributeError):
+            s.getdecimal('float')
+        import decimal
+        cfg.converters['decimal'] = decimal.Decimal
+        self.assertIn('decimal', cfg.converters)
+        self.assertIsNotNone(cfg.converters['decimal'])
+        self.assertEqual(len(cfg.converters), 5)
+        dec0_5 = decimal.Decimal('0.5')
+        self.assertEqual(cfg.getdecimal('s', 'float'), dec0_5)
+        self.assertEqual(s.getdecimal('float'), dec0_5)
+        del cfg.converters['decimal']
+        self.assertNotIn('decimal', cfg.converters)
+        self.assertEqual(len(cfg.converters), 4)
+        with self.assertRaises(AttributeError):
+            cfg.getdecimal('s', 'float')
+        with self.assertRaises(AttributeError):
+            s.getdecimal('float')
+        with self.assertRaises(KeyError):
+            del cfg.converters['decimal']
+        with self.assertRaises(KeyError):
+            del cfg.converters['']
+        with self.assertRaises(KeyError):
+            del cfg.converters[None]
+
+
+class BlatantOverrideConvertersTestCase(unittest.TestCase):
+    """What if somebody overrode a getboolean()? We want to make sure that in
+    this case the automatic converters do not kick in."""
+
+    config = """
+        [one]
+        one = false
+        two = false
+        three = long story short
+
+        [two]
+        one = false
+        two = false
+        three = four
+    """
+
+    def test_converters_at_init(self):
+        cfg = configparser.ConfigParser(converters={'len': len})
+        cfg.read_string(self.config)
+        self._test_len(cfg)
+        self.assertIsNotNone(cfg.converters['len'])
+
+    def test_inheritance(self):
+        class StrangeConfigParser(configparser.ConfigParser):
+            gettysburg = 'a historic borough in south central Pennsylvania'
+
+            def getboolean(self, section, option, *, raw=False, vars=None,
+                        fallback=configparser._UNSET):
+                if section == option:
+                    return True
+                return super().getboolean(section, option, raw=raw, vars=vars,
+                                          fallback=fallback)
+            def getlen(self, section, option, *, raw=False, vars=None,
+                       fallback=configparser._UNSET):
+                return self._get_conv(section, option, len, raw=raw, vars=vars,
+                                      fallback=fallback)
+
+        cfg = StrangeConfigParser()
+        cfg.read_string(self.config)
+        self._test_len(cfg)
+        self.assertIsNone(cfg.converters['len'])
+        self.assertTrue(cfg.getboolean('one', 'one'))
+        self.assertTrue(cfg.getboolean('two', 'two'))
+        self.assertFalse(cfg.getboolean('one', 'two'))
+        self.assertFalse(cfg.getboolean('two', 'one'))
+        cfg.converters['boolean'] = cfg._convert_to_boolean
+        self.assertFalse(cfg.getboolean('one', 'one'))
+        self.assertFalse(cfg.getboolean('two', 'two'))
+        self.assertFalse(cfg.getboolean('one', 'two'))
+        self.assertFalse(cfg.getboolean('two', 'one'))
+
+    def _test_len(self, cfg):
+        self.assertEqual(len(cfg.converters), 4)
+        self.assertIn('boolean', cfg.converters)
+        self.assertIn('len', cfg.converters)
+        self.assertNotIn('tysburg', cfg.converters)
+        self.assertIsNone(cfg.converters['int'])
+        self.assertIsNone(cfg.converters['float'])
+        self.assertIsNone(cfg.converters['boolean'])
+        self.assertEqual(cfg.getlen('one', 'one'), 5)
+        self.assertEqual(cfg.getlen('one', 'two'), 5)
+        self.assertEqual(cfg.getlen('one', 'three'), 16)
+        self.assertEqual(cfg.getlen('two', 'one'), 5)
+        self.assertEqual(cfg.getlen('two', 'two'), 5)
+        self.assertEqual(cfg.getlen('two', 'three'), 4)
+        self.assertEqual(cfg.getlen('two', 'four', fallback=0), 0)
+        with self.assertRaises(configparser.NoOptionError):
+            cfg.getlen('two', 'four')
+        self.assertEqual(cfg['one'].getlen('one'), 5)
+        self.assertEqual(cfg['one'].getlen('two'), 5)
+        self.assertEqual(cfg['one'].getlen('three'), 16)
+        self.assertEqual(cfg['two'].getlen('one'), 5)
+        self.assertEqual(cfg['two'].getlen('two'), 5)
+        self.assertEqual(cfg['two'].getlen('three'), 4)
+        self.assertEqual(cfg['two'].getlen('four', 0), 0)
+        self.assertEqual(cfg['two'].getlen('four'), None)
+
+    def test_instance_assignment(self):
+        cfg = configparser.ConfigParser()
+        cfg.getboolean = lambda section, option: True
+        cfg.getlen = lambda section, option: len(cfg[section][option])
+        cfg.read_string(self.config)
+        self.assertEqual(len(cfg.converters), 3)
+        self.assertIn('boolean', cfg.converters)
+        self.assertNotIn('len', cfg.converters)
+        self.assertIsNone(cfg.converters['int'])
+        self.assertIsNone(cfg.converters['float'])
+        self.assertIsNone(cfg.converters['boolean'])
+        self.assertTrue(cfg.getboolean('one', 'one'))
+        self.assertTrue(cfg.getboolean('two', 'two'))
+        self.assertTrue(cfg.getboolean('one', 'two'))
+        self.assertTrue(cfg.getboolean('two', 'one'))
+        cfg.converters['boolean'] = cfg._convert_to_boolean
+        self.assertFalse(cfg.getboolean('one', 'one'))
+        self.assertFalse(cfg.getboolean('two', 'two'))
+        self.assertFalse(cfg.getboolean('one', 'two'))
+        self.assertFalse(cfg.getboolean('two', 'one'))
+        self.assertEqual(cfg.getlen('one', 'one'), 5)
+        self.assertEqual(cfg.getlen('one', 'two'), 5)
+        self.assertEqual(cfg.getlen('one', 'three'), 16)
+        self.assertEqual(cfg.getlen('two', 'one'), 5)
+        self.assertEqual(cfg.getlen('two', 'two'), 5)
+        self.assertEqual(cfg.getlen('two', 'three'), 4)
+        # If a getter impl is assigned straight to the instance, it won't
+        # be available on the section proxies.
+        with self.assertRaises(AttributeError):
+            self.assertEqual(cfg['one'].getlen('one'), 5)
+        with self.assertRaises(AttributeError):
+            self.assertEqual(cfg['two'].getlen('one'), 5)
+
+
 if __name__ == '__main__':
     unittest.main()

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list