[Python-checkins] r83889 - in python/branches/py3k: Doc/library/configparser.rst Lib/configparser.py Lib/test/test_cfgparser.py Misc/NEWS

fred.drake python-checkins at python.org
Mon Aug 9 14:52:45 CEST 2010


Author: fred.drake
Date: Mon Aug  9 14:52:45 2010
New Revision: 83889

Log:
issue #9452:
Add read_file, read_string, and read_dict to the configparser API;
new source attribute to exceptions.


Modified:
   python/branches/py3k/Doc/library/configparser.rst
   python/branches/py3k/Lib/configparser.py
   python/branches/py3k/Lib/test/test_cfgparser.py
   python/branches/py3k/Misc/NEWS

Modified: python/branches/py3k/Doc/library/configparser.rst
==============================================================================
--- python/branches/py3k/Doc/library/configparser.rst	(original)
+++ python/branches/py3k/Doc/library/configparser.rst	Mon Aug  9 14:52:45 2010
@@ -27,8 +27,9 @@
    the Windows Registry extended version of INI syntax.
 
 A configuration file consists of sections, each led by a ``[section]`` header,
-followed by name/value entries separated by a specific string (``=`` or ``:`` by
-default).  Note that leading whitespace is removed from values.  Values can be
+followed by key/value entries separated by a specific string (``=`` or ``:`` by
+default). By default, section names are case sensitive but keys are not. Leading
+und trailing whitespace is removed from keys and from values.  Values can be
 ommitted, in which case the key/value delimiter may also be left out.  Values
 can also span multiple lines, as long as they are indented deeper than the first
 line of the value.  Depending on the parser's mode, blank lines may be treated
@@ -101,7 +102,7 @@
 keys within each section.
 
 
-.. class:: RawConfigParser(defaults=None, dict_type=collections.OrderedDict, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, empty_lines_in_values=True, allow_no_value=False)
+.. class:: RawConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, strict=False, empty_lines_in_values=True)
 
    The basic configuration object.  When *defaults* is given, it is initialized
    into the dictionary of intrinsic defaults.  When *dict_type* is given, it
@@ -115,11 +116,14 @@
    *comment_prefixes* is a special value that indicates that ``;`` and ``#`` can
    start whole line comments while only ``;`` can start inline comments.
 
-   When *empty_lines_in_values* is ``False`` (default: ``True``), each empty
-   line marks the end of an option.  Otherwise, internal empty lines of a
-   multiline option are kept as part of the value.  When *allow_no_value* is
-   true (default: ``False``), options without values are accepted; the value
-   presented for these is ``None``.
+   When *strict* is ``True`` (default: ``False``), the parser won't allow for
+   any section or option duplicates while reading from a single source (file,
+   string or dictionary), raising :exc:`DuplicateSectionError` or
+   :exc:`DuplicateOptionError`. When *empty_lines_in_values* is ``False``
+   (default: ``True``), each empty line marks the end of an option.  Otherwise,
+   internal empty lines of a multiline option are kept as part of the value.
+   When *allow_no_value* is ``True`` (default: ``False``), options without
+   values are accepted; the value presented for these is ``None``.
 
    This class does not support the magical interpolation behavior.
 
@@ -127,11 +131,11 @@
       The default *dict_type* is :class:`collections.OrderedDict`.
 
    .. versionchanged:: 3.2
-      *delimiters*, *comment_prefixes*, *empty_lines_in_values* and
-      *allow_no_value* were added.
+      *allow_no_value*, *delimiters*, *comment_prefixes*, *strict* and
+      *empty_lines_in_values* were added.
 
 
-.. class:: SafeConfigParser(defaults=None, dict_type=collections.OrderedDict, delimiters=('=', ':'), comment_prefixes=('#', ';'), empty_lines_in_values=True, allow_no_value=False)
+.. class:: SafeConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=('#', ';'), strict=False, empty_lines_in_values=True)
 
    Derived class of :class:`ConfigParser` that implements a sane variant of the
    magical interpolation feature.  This implementation is more predictable as it
@@ -147,11 +151,11 @@
       The default *dict_type* is :class:`collections.OrderedDict`.
 
    .. versionchanged:: 3.2
-      *delimiters*, *comment_prefixes*, *empty_lines_in_values* and
-      *allow_no_value* were added.
+      *allow_no_value*, *delimiters*, *comment_prefixes*, *strict* and
+      *empty_lines_in_values* were added.
 
 
-.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, delimiters=('=', ':'), comment_prefixes=('#', ';'), empty_lines_in_values=True, allow_no_value=False)
+.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=('#', ';'), strict=False, empty_lines_in_values=True)
 
    Derived class of :class:`RawConfigParser` that implements the magical
    interpolation feature and adds optional arguments to the :meth:`get` and
@@ -174,8 +178,8 @@
       The default *dict_type* is :class:`collections.OrderedDict`.
 
    .. versionchanged:: 3.2
-      *delimiters*, *comment_prefixes*, *empty_lines_in_values* and
-      *allow_no_value* were added.
+      *allow_no_value*, *delimiters*, *comment_prefixes*,
+      *strict* and *empty_lines_in_values* were added.
 
 
 .. exception:: Error
@@ -191,12 +195,26 @@
 .. exception:: DuplicateSectionError
 
    Exception raised if :meth:`add_section` is called with the name of a section
-   that is already present.
+   that is already present or in strict parsers when a section if found more
+   than once in a single input file, string or dictionary.
+
+   .. versionadded:: 3.2
+      Optional ``source`` and ``lineno`` attributes and arguments to
+      :meth:`__init__` were added.
+
+
+.. exception:: DuplicateOptionError
+
+   Exception raised by strict parsers if a single option appears twice during
+   reading from a single file, string or dictionary. This catches misspellings
+   and case sensitivity-related errors, e.g. a dictionary may have two keys
+   representing the same case-insensitive configuration key.
 
 
 .. exception:: NoOptionError
 
-   Exception raised when a specified option is not found in the specified  section.
+   Exception raised when a specified option is not found in the specified
+   section.
 
 
 .. exception:: InterpolationError
@@ -233,6 +251,9 @@
 
    Exception raised when errors occur attempting to parse a file.
 
+   .. versionchanged:: 3.2
+      The ``filename`` attribute and :meth:`__init__` argument were renamed to
+      ``source`` for consistency.
 
 .. data:: MAX_INTERPOLATION_DEPTH
 
@@ -315,15 +336,41 @@
       default encoding for :func:`open`.
 
 
-.. method:: RawConfigParser.readfp(fp, filename=None)
+.. method:: RawConfigParser.read_file(f, source=None)
 
-   Read and parse configuration data from the file or file-like object in *fp*
+   Read and parse configuration data from the file or file-like object in *f*
    (only the :meth:`readline` method is used).  The file-like object must
    operate in text mode, i.e. return strings from :meth:`readline`.
 
-   If *filename* is omitted and *fp* has a :attr:`name` attribute, that is used
-   for *filename*; the default is ``<???>``.
+   Optional argument *source* specifies the name of the file being read. It not
+   given and *f* has a :attr:`name` attribute, that is used for *source*; the
+   default is ``<???>``.
+
+   .. versionadded:: 3.2
+      Renamed from :meth:`readfp` (with the ``filename`` attribute renamed to
+      ``source`` for consistency with other ``read_*`` methods).
+
+
+.. method:: RawConfigParser.read_string(string, source='<string>')
 
+   Parse configuration data from a given string.
+
+   Optional argument *source* specifies a context-specific name of the string
+   passed. If not given, ``<string>`` is used.
+
+   .. versionadded:: 3.2
+
+.. method:: RawConfigParser.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.
+
+   Optional argument *source* specifies a context-specific name of the
+   dictionary passed.  If not given, ``<dict>`` is used.
+
+   .. versionadded:: 3.2
 
 .. method:: RawConfigParser.get(section, option)
 
@@ -408,6 +455,10 @@
    Note that when reading configuration files, whitespace around the
    option names are stripped before :meth:`optionxform` is called.
 
+.. method:: RawConfigParser.readfp(fp, filename=None)
+
+   .. deprecated:: 3.2
+      Please use :meth:`read_file` instead.
 
 .. _configparser-objects:
 

Modified: python/branches/py3k/Lib/configparser.py
==============================================================================
--- python/branches/py3k/Lib/configparser.py	(original)
+++ python/branches/py3k/Lib/configparser.py	Mon Aug  9 14:52:45 2010
@@ -26,10 +26,10 @@
 
     __init__(defaults=None, dict_type=_default_dict,
              delimiters=('=', ':'), comment_prefixes=('#', ';'),
-             empty_lines_in_values=True, allow_no_value=False):
+             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
-        must be appropriate for %()s string interpolation.  Note that `__name__'
+        must be appropriate for %()s string interpolation. Note that `__name__'
         is always an intrinsic default; its value is the section's name.
 
         When `dict_type' is given, it will be used to create the dictionary
@@ -42,6 +42,10 @@
         When `comment_prefixes' is given, it will be used as the set of
         substrings that prefix comments in a line.
 
+        When `strict` is True, the parser won't allow for any section or option
+        duplicates while reading from a single source (file, string or
+        dictionary). Default is False.
+
         When `empty_lines_in_values' is False (default: True), each empty line
         marks the end of an option. Otherwise, internal empty lines of
         a multiline option are kept as part of the value.
@@ -66,10 +70,19 @@
         name.  A single filename is also allowed.  Non-existing files
         are ignored.  Return list of successfully read files.
 
-    readfp(fp, filename=None)
+    read_file(f, filename=None)
         Read and parse one configuration file, given as a file object.
-        The filename defaults to fp.name; it is only used in error
-        messages (if fp has no `name' attribute, the string `<???>' is used).
+        The filename defaults to f.name; it is only used in error
+        messages (if f has no `name' attribute, the string `<???>' is used).
+
+    read_string(string)
+        Read configuration from a given string.
+
+    read_dict(dictionary)
+        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.
 
     get(section, option, raw=False, vars=None)
         Return a string value for the named option.  All % interpolations are
@@ -114,11 +127,13 @@
     # fallback for setup.py which hasn't yet built _collections
     _default_dict = dict
 
+import io
 import re
 import sys
+import warnings
 
-__all__ = ["NoSectionError", "DuplicateSectionError", "NoOptionError",
-           "InterpolationError", "InterpolationDepthError",
+__all__ = ["NoSectionError", "DuplicateOptionError", "DuplicateSectionError",
+           "NoOptionError", "InterpolationError", "InterpolationDepthError",
            "InterpolationSyntaxError", "ParsingError",
            "MissingSectionHeaderError",
            "ConfigParser", "SafeConfigParser", "RawConfigParser",
@@ -147,8 +162,8 @@
         self.__message = value
 
     # BaseException.message has been deprecated since Python 2.6.  To prevent
-    # DeprecationWarning from popping up over this pre-existing attribute, use a
-    # new property that takes lookup precedence.
+    # DeprecationWarning from popping up over this pre-existing attribute, use
+    # a new property that takes lookup precedence.
     message = property(_get_message, _set_message)
 
     def __init__(self, msg=''):
@@ -171,12 +186,56 @@
 
 
 class DuplicateSectionError(Error):
-    """Raised when a section is multiply-created."""
+    """Raised when a section is repeated in an input source.
 
-    def __init__(self, section):
-        Error.__init__(self, "Section %r already exists" % section)
+    Possible repetitions that raise this exception are: multiple creation
+    using the API or in strict parsers when a section is found more than once
+    in a single input file, string or dictionary.
+    """
+
+    def __init__(self, section, source=None, lineno=None):
+        msg = [repr(section), " already exists"]
+        if source is not None:
+            message = ["While reading from ", source]
+            if lineno is not None:
+                message.append(" [line {0:2d}]".format(lineno))
+            message.append(": section ")
+            message.extend(msg)
+            msg = message
+        else:
+            msg.insert(0, "Section ")
+        Error.__init__(self, "".join(msg))
         self.section = section
-        self.args = (section, )
+        self.source = source
+        self.lineno = lineno
+        self.args = (section, source, lineno)
+
+
+class DuplicateOptionError(Error):
+    """Raised by strict parsers when an option is repeated in an input source.
+
+    Current implementation raises this exception only when an option is found
+    more than once in a single file, string or dictionary.
+    """
+
+    def __init__(self, section, option, source=None, lineno=None):
+        msg = [repr(option), " in section ", repr(section),
+               " already exists"]
+        if source is not None:
+            message = ["While reading from ", source]
+            if lineno is not None:
+                message.append(" [line {0:2d}]".format(lineno))
+            message.append(": option ")
+            message.extend(msg)
+            msg = message
+        else:
+            msg.insert(0, "Option ")
+        Error.__init__(self, "".join(msg))
+        self.section = section
+        self.option = option
+        self.source = source
+        self.lineno = lineno
+        self.args = (section, option, source, lineno)
 
 
 class NoOptionError(Error):
@@ -216,8 +275,12 @@
 
 
 class InterpolationSyntaxError(InterpolationError):
-    """Raised when the source text into which substitutions are made
-    does not conform to the required syntax."""
+    """Raised when the source text contains invalid syntax.
+
+    Current implementation raises this exception only for SafeConfigParser
+    instances when the source text into which substitutions are made
+    does not conform to the required syntax.
+    """
 
 
 class InterpolationDepthError(InterpolationError):
@@ -236,11 +299,40 @@
 class ParsingError(Error):
     """Raised when a configuration file does not follow legal syntax."""
 
-    def __init__(self, filename):
-        Error.__init__(self, 'File contains parsing errors: %s' % filename)
-        self.filename = filename
+    def __init__(self, source=None, filename=None):
+        # Exactly one of `source'/`filename' arguments has to be given.
+        # `filename' kept for compatibility.
+        if filename and source:
+            raise ValueError("Cannot specify both `filename' and `source'. "
+                             "Use `source'.")
+        elif not filename and not source:
+            raise ValueError("Required argument `source' not given.")
+        elif filename:
+            source = filename
+        Error.__init__(self, 'Source contains parsing errors: %s' % source)
+        self.source = source
         self.errors = []
-        self.args = (filename, )
+        self.args = (source, )
+
+    @property
+    def filename(self):
+        """Deprecated, use `source'."""
+        warnings.warn(
+            "This 'filename' attribute will be removed in future versions.  "
+            "Use 'source' instead.",
+            PendingDeprecationWarning, stacklevel=2
+        )
+        return self.source
+
+    @filename.setter
+    def filename(self, value):
+        """Deprecated, user `source'."""
+        warnings.warn(
+            "The 'filename' attribute will be removed in future versions.  "
+            "Use 'source' instead.",
+            PendingDeprecationWarning, stacklevel=2
+        )
+        self.source = value
 
     def append(self, lineno, line):
         self.errors.append((lineno, line))
@@ -255,7 +347,7 @@
             self,
             'File contains no section headers.\nfile: %s, line: %d\n%r' %
             (filename, lineno, line))
-        self.filename = filename
+        self.source = filename
         self.lineno = lineno
         self.line = line
         self.args = (filename, lineno, line)
@@ -302,8 +394,9 @@
     _COMPATIBLE = object()
 
     def __init__(self, defaults=None, dict_type=_default_dict,
-                 delimiters=('=', ':'), comment_prefixes=_COMPATIBLE,
-                 empty_lines_in_values=True, allow_no_value=False):
+                 allow_no_value=False, *, delimiters=('=', ':'),
+                 comment_prefixes=_COMPATIBLE, strict=False,
+                 empty_lines_in_values=True):
         self._dict = dict_type
         self._sections = self._dict()
         self._defaults = self._dict()
@@ -314,12 +407,12 @@
         if delimiters == ('=', ':'):
             self._optcre = self.OPTCRE_NV if allow_no_value else self.OPTCRE
         else:
-            delim = "|".join(re.escape(d) for d in delimiters)
+            d = "|".join(re.escape(d) for d in delimiters)
             if allow_no_value:
-                self._optcre = re.compile(self._OPT_NV_TMPL.format(delim=delim),
+                self._optcre = re.compile(self._OPT_NV_TMPL.format(delim=d),
                                           re.VERBOSE)
             else:
-                self._optcre = re.compile(self._OPT_TMPL.format(delim=delim),
+                self._optcre = re.compile(self._OPT_TMPL.format(delim=d),
                                           re.VERBOSE)
         if comment_prefixes is self._COMPATIBLE:
             self._startonly_comment_prefixes = ('#',)
@@ -327,6 +420,7 @@
         else:
             self._startonly_comment_prefixes = ()
             self._comment_prefixes = tuple(comment_prefixes or ())
+        self._strict = strict
         self._empty_lines_in_values = empty_lines_in_values
 
     def defaults(self):
@@ -394,20 +488,59 @@
             read_ok.append(filename)
         return read_ok
 
-    def readfp(self, fp, filename=None):
+    def read_file(self, f, source=None):
         """Like read() but the argument must be a file-like object.
 
-        The `fp' argument must have a `readline' method.  Optional
-        second argument is the `filename', which if not given, is
-        taken from fp.name.  If fp has no `name' attribute, `<???>' is
-        used.
+        The `f' argument must have a `readline' method.  Optional second
+        argument is the `source' specifying the name of the file being read. If
+        not given, it is taken from f.name. If `f' has no `name' attribute,
+        `<???>' is used.
         """
-        if filename is None:
+        if source is None:
             try:
-                filename = fp.name
+                srouce = f.name
             except AttributeError:
-                filename = '<???>'
-        self._read(fp, filename)
+                source = '<???>'
+        self._read(f, source)
+
+    def read_string(self, string, source='<string>'):
+        """Read configuration from a given string."""
+        sfile = io.StringIO(string)
+        self.read_file(sfile, source)
+
+    def read_dict(self, dictionary, source='<dict>'):
+        """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.
+
+        Optional second argument is the `source' specifying the name of the
+        dictionary being read.
+        """
+        elements_added = set()
+        for section, keys in dictionary.items():
+            try:
+                self.add_section(section)
+            except DuplicateSectionError:
+                if self._strict and section in elements_added:
+                    raise
+                elements_added.add(section)
+            for key, value in keys.items():
+                key = self.optionxform(key)
+                if self._strict and (section, key) in elements_added:
+                    raise DuplicateOptionError(section, key, source)
+                elements_added.add((section, key))
+                self.set(section, key, value)
+
+    def readfp(self, fp, filename=None):
+        """Deprecated, use read_file instead."""
+        warnings.warn(
+            "This method will be removed in future versions.  "
+            "Use 'parser.read_file()' instead.",
+            PendingDeprecationWarning, stacklevel=2
+        )
+        self.read_file(fp, source=filename)
 
     def get(self, section, option):
         opt = self.optionxform(option)
@@ -461,7 +594,6 @@
 
     def has_option(self, section, option):
         """Check for the existence of a given option in a given section."""
-
         if not section or section == DEFAULTSECT:
             option = self.optionxform(option)
             return option in self._defaults
@@ -474,7 +606,6 @@
 
     def set(self, section, option, value=None):
         """Set an option."""
-
         if not section or section == DEFAULTSECT:
             sectdict = self._defaults
         else:
@@ -538,21 +669,23 @@
     def _read(self, fp, fpname):
         """Parse a sectioned configuration file.
 
-        Each section in a configuration file contains a header, indicated by a
-        name in square brackets (`[]'), plus key/value options, indicated by
+        Each section in a configuration file contains a header, indicated by
+        a name in square brackets (`[]'), plus key/value options, indicated by
         `name' and `value' delimited with a specific substring (`=' or `:' by
         default).
 
-        Values can span multiple lines, as long as they are indented deeper than
-        the first line of the value. Depending on the parser's mode, blank lines
-        may be treated as parts of multiline values or ignored.
+        Values can span multiple lines, as long as they are indented deeper
+        than the first line of the value. Depending on the parser's mode, blank
+        lines may be treated as parts of multiline values or ignored.
 
         Configuration files may include comments, prefixed by specific
-        characters (`#' and `;' by default). Comments may appear on their own in
-        an otherwise empty line or may be entered in lines holding values or
+        characters (`#' and `;' by default). Comments may appear on their own
+        in an otherwise empty line or may be entered in lines holding values or
         section names.
         """
+        elements_added = set()
         cursect = None                        # None, or a dictionary
+        sectname = None
         optname = None
         lineno = 0
         indent_level = 0
@@ -598,13 +731,18 @@
                 if mo:
                     sectname = mo.group('header')
                     if sectname in self._sections:
+                        if self._strict and sectname in elements_added:
+                            raise DuplicateSectionError(sectname, fpname,
+                                                        lineno)
                         cursect = self._sections[sectname]
+                        elements_added.add(sectname)
                     elif sectname == DEFAULTSECT:
                         cursect = self._defaults
                     else:
                         cursect = self._dict()
                         cursect['__name__'] = sectname
                         self._sections[sectname] = cursect
+                        elements_added.add(sectname)
                     # So sections can't start with a continuation line
                     optname = None
                 # no section header in the file?
@@ -618,6 +756,11 @@
                         if not optname:
                             e = self._handle_error(e, fpname, lineno, line)
                         optname = self.optionxform(optname.rstrip())
+                        if (self._strict and
+                            (sectname, optname) in elements_added):
+                            raise DuplicateOptionError(sectname, optname,
+                                                       fpname, lineno)
+                        elements_added.add((sectname, optname))
                         # This check is fine because the OPTCRE cannot
                         # match if it would set optval to None
                         if optval is not None:
@@ -692,8 +835,7 @@
             return self._interpolate(section, option, value, d)
 
     def items(self, section, raw=False, vars=None):
-        """Return a list of tuples with (name, value) for each option
-        in the section.
+        """Return a list of (name, value) tuples for each option in a section.
 
         All % interpolations are expanded in the return values, based on the
         defaults passed into the constructor, unless the optional argument
@@ -799,7 +941,8 @@
             else:
                 raise InterpolationSyntaxError(
                     option, section,
-                    "'%%' must be followed by '%%' or '(', found: %r" % (rest,))
+                    "'%%' must be followed by '%%' or '(', "
+                    "found: %r" % (rest,))
 
     def set(self, section, option, value=None):
         """Set an option.  Extend ConfigParser.set: check for string values."""
@@ -811,13 +954,11 @@
         if self._optcre is self.OPTCRE or value:
             if not isinstance(value, str):
                 raise TypeError("option values must be strings")
-        # check for bad percent signs:
-        # first, replace all "good" interpolations
-        tmp_value = value.replace('%%', '')
-        tmp_value = self._interpvar_re.sub('', tmp_value)
-        # then, check if there's a lone percent sign left
-        percent_index = tmp_value.find('%')
-        if percent_index != -1:
-            raise ValueError("invalid interpolation syntax in %r at "
-                             "position %d" % (value, percent_index))
+        # check for bad percent signs
+        if value:
+            tmp_value = value.replace('%%', '') # escaped percent signs
+            tmp_value = self._interpvar_re.sub('', tmp_value) # valid syntax
+            if '%' in tmp_value:
+                raise ValueError("invalid interpolation syntax in %r at "
+                                "position %d" % (value, tmp_value.find('%')))
         ConfigParser.set(self, section, option, value)

Modified: python/branches/py3k/Lib/test/test_cfgparser.py
==============================================================================
--- python/branches/py3k/Lib/test/test_cfgparser.py	(original)
+++ python/branches/py3k/Lib/test/test_cfgparser.py	Mon Aug  9 14:52:45 2010
@@ -30,62 +30,28 @@
     comment_prefixes = (';', '#')
     empty_lines_in_values = True
     dict_type = configparser._default_dict
+    strict = False
 
     def newconfig(self, defaults=None):
         arguments = dict(
+            defaults=defaults,
             allow_no_value=self.allow_no_value,
             delimiters=self.delimiters,
             comment_prefixes=self.comment_prefixes,
             empty_lines_in_values=self.empty_lines_in_values,
             dict_type=self.dict_type,
+            strict=self.strict,
         )
-        if defaults is None:
-            self.cf = self.config_class(**arguments)
-        else:
-            self.cf = self.config_class(defaults,
-                                        **arguments)
-        return self.cf
+        return self.config_class(**arguments)
 
     def fromstring(self, string, defaults=None):
         cf = self.newconfig(defaults)
-        sio = io.StringIO(string)
-        cf.readfp(sio)
+        cf.read_string(string)
         return cf
 
 class BasicTestCase(CfgParserTestCaseClass):
 
-    def test_basic(self):
-        config_string = """\
-[Foo Bar]
-foo{0[0]}bar
-[Spacey Bar]
-foo {0[0]} bar
-[Spacey Bar From The Beginning]
-  foo {0[0]} bar
-  baz {0[0]} qwe
-[Commented Bar]
-foo{0[1]} bar {1[1]} comment
-baz{0[0]}qwe {1[0]}another one
-[Long Line]
-foo{0[1]} this line is much, much longer than my editor
-   likes it.
-[Section\\with$weird%characters[\t]
-[Internationalized Stuff]
-foo[bg]{0[1]} Bulgarian
-foo{0[0]}Default
-foo[en]{0[0]}English
-foo[de]{0[0]}Deutsch
-[Spaces]
-key with spaces {0[1]} value
-another with spaces {0[0]} splat!
-""".format(self.delimiters, self.comment_prefixes)
-        if self.allow_no_value:
-            config_string += (
-                "[NoValue]\n"
-                "option-without-value\n"
-                )
-
-        cf = self.fromstring(config_string)
+    def basic_test(self, cf):
         L = cf.sections()
         L.sort()
         E = ['Commented Bar',
@@ -137,6 +103,125 @@
         eq(cf.get('Long Line', 'foo'),
            'this line is much, much longer than my editor\nlikes it.')
 
+    def test_basic(self):
+        config_string = """\
+[Foo Bar]
+foo{0[0]}bar
+[Spacey Bar]
+foo {0[0]} bar
+[Spacey Bar From The Beginning]
+  foo {0[0]} bar
+  baz {0[0]} qwe
+[Commented Bar]
+foo{0[1]} bar {1[1]} comment
+baz{0[0]}qwe {1[0]}another one
+[Long Line]
+foo{0[1]} this line is much, much longer than my editor
+   likes it.
+[Section\\with$weird%characters[\t]
+[Internationalized Stuff]
+foo[bg]{0[1]} Bulgarian
+foo{0[0]}Default
+foo[en]{0[0]}English
+foo[de]{0[0]}Deutsch
+[Spaces]
+key with spaces {0[1]} value
+another with spaces {0[0]} splat!
+""".format(self.delimiters, self.comment_prefixes)
+        if self.allow_no_value:
+            config_string += (
+                "[NoValue]\n"
+                "option-without-value\n"
+                )
+        cf = self.fromstring(config_string)
+        self.basic_test(cf)
+        if self.strict:
+            with self.assertRaises(configparser.DuplicateOptionError):
+                cf.read_string(textwrap.dedent("""\
+                    [Duplicate Options Here]
+                    option {0[0]} with a value
+                    option {0[1]} with another value
+                """.format(self.delimiters)))
+            with self.assertRaises(configparser.DuplicateSectionError):
+                cf.read_string(textwrap.dedent("""\
+                    [And Now For Something]
+                    completely different {0[0]} True
+                    [And Now For Something]
+                    the larch {0[1]} 1
+                """.format(self.delimiters)))
+        else:
+            cf.read_string(textwrap.dedent("""\
+                [Duplicate Options Here]
+                option {0[0]} with a value
+                option {0[1]} with another value
+            """.format(self.delimiters)))
+
+            cf.read_string(textwrap.dedent("""\
+                [And Now For Something]
+                completely different {0[0]} True
+                [And Now For Something]
+                the larch {0[1]} 1
+            """.format(self.delimiters)))
+
+    def test_basic_from_dict(self):
+        config = {
+            "Foo Bar": {
+                "foo": "bar",
+            },
+            "Spacey Bar": {
+                "foo": "bar",
+            },
+            "Spacey Bar From The Beginning": {
+                "foo": "bar",
+                "baz": "qwe",
+            },
+            "Commented Bar": {
+                "foo": "bar",
+                "baz": "qwe",
+            },
+            "Long Line": {
+                "foo": "this line is much, much longer than my editor\nlikes "
+                       "it.",
+            },
+            "Section\\with$weird%characters[\t": {
+            },
+            "Internationalized Stuff": {
+                "foo[bg]": "Bulgarian",
+                "foo": "Default",
+                "foo[en]": "English",
+                "foo[de]": "Deutsch",
+            },
+            "Spaces": {
+                "key with spaces": "value",
+                "another with spaces": "splat!",
+            }
+        }
+        if self.allow_no_value:
+            config.update({
+                "NoValue": {
+                    "option-without-value": None,
+                }
+            })
+        cf = self.newconfig()
+        cf.read_dict(config)
+        self.basic_test(cf)
+        if self.strict:
+            with self.assertRaises(configparser.DuplicateOptionError):
+                cf.read_dict({
+                    "Duplicate Options Here": {
+                        'option': 'with a value',
+                        'OPTION': 'with another value',
+                    },
+                })
+        else:
+            cf.read_dict({
+                "Duplicate Options Here": {
+                    'option': 'with a value',
+                    'OPTION': 'with another value',
+                },
+            })
+
+
     def test_case_sensitivity(self):
         cf = self.newconfig()
         cf.add_section("A")
@@ -185,25 +270,25 @@
             "could not locate option, expecting case-insensitive defaults")
 
     def test_parse_errors(self):
-        self.newconfig()
-        self.parse_error(configparser.ParsingError,
+        cf = self.newconfig()
+        self.parse_error(cf, configparser.ParsingError,
                          "[Foo]\n"
                          "{}val-without-opt-name\n".format(self.delimiters[0]))
-        self.parse_error(configparser.ParsingError,
+        self.parse_error(cf, configparser.ParsingError,
                          "[Foo]\n"
                          "{}val-without-opt-name\n".format(self.delimiters[1]))
-        e = self.parse_error(configparser.MissingSectionHeaderError,
+        e = self.parse_error(cf, configparser.MissingSectionHeaderError,
                              "No Section!\n")
         self.assertEqual(e.args, ('<???>', 1, "No Section!\n"))
         if not self.allow_no_value:
-            e = self.parse_error(configparser.ParsingError,
+            e = self.parse_error(cf, configparser.ParsingError,
                                 "[Foo]\n  wrong-indent\n")
             self.assertEqual(e.args, ('<???>',))
 
-    def parse_error(self, exc, src):
+    def parse_error(self, cf, exc, src):
         sio = io.StringIO(src)
         with self.assertRaises(exc) as cm:
-            self.cf.readfp(sio)
+            cf.read_file(sio)
         return cm.exception
 
     def test_query_errors(self):
@@ -217,15 +302,15 @@
             cf.options("Foo")
         with self.assertRaises(configparser.NoSectionError):
             cf.set("foo", "bar", "value")
-        e = self.get_error(configparser.NoSectionError, "foo", "bar")
+        e = self.get_error(cf, configparser.NoSectionError, "foo", "bar")
         self.assertEqual(e.args, ("foo",))
         cf.add_section("foo")
-        e = self.get_error(configparser.NoOptionError, "foo", "bar")
+        e = self.get_error(cf, configparser.NoOptionError, "foo", "bar")
         self.assertEqual(e.args, ("bar", "foo"))
 
-    def get_error(self, exc, section, option):
+    def get_error(self, cf, exc, section, option):
         try:
-            self.cf.get(section, option)
+            cf.get(section, option)
         except exc as e:
             return e
         else:
@@ -262,7 +347,31 @@
         cf.add_section("Foo")
         with self.assertRaises(configparser.DuplicateSectionError) as cm:
             cf.add_section("Foo")
-        self.assertEqual(cm.exception.args, ("Foo",))
+        e = cm.exception
+        self.assertEqual(str(e), "Section 'Foo' already exists")
+        self.assertEqual(e.args, ("Foo", None, None))
+
+        if self.strict:
+            with self.assertRaises(configparser.DuplicateSectionError) as cm:
+                cf.read_string(textwrap.dedent("""\
+                    [Foo]
+                    will this be added{equals}True
+                    [Bar]
+                    what about this{equals}True
+                    [Foo]
+                    oops{equals}this won't
+                """.format(equals=self.delimiters[0])), source='<foo-bar>')
+            e = cm.exception
+            self.assertEqual(str(e), "While reading from <foo-bar> [line  5]: "
+                                     "section 'Foo' already exists")
+            self.assertEqual(e.args, ("Foo", '<foo-bar>', 5))
+
+            with self.assertRaises(configparser.DuplicateOptionError) as cm:
+                cf.read_dict({'Bar': {'opt': 'val', 'OPT': 'is really `opt`'}})
+            e = cm.exception
+            self.assertEqual(str(e), "While reading from <dict>: option 'opt' "
+                                     "in section 'Bar' already exists")
+            self.assertEqual(e.args, ("Bar", "opt", "<dict>", None))
 
     def test_write(self):
         config_string = (
@@ -392,6 +501,11 @@
         self.assertEqual(L, expected)
 
 
+class StrictTestCase(BasicTestCase):
+    config_class = configparser.RawConfigParser
+    strict = True
+
+
 class ConfigParserTestCase(BasicTestCase):
     config_class = configparser.ConfigParser
 
@@ -409,7 +523,7 @@
            "something with lots of interpolation (9 steps)")
         eq(cf.get("Foo", "bar10"),
            "something with lots of interpolation (10 steps)")
-        e = self.get_error(configparser.InterpolationDepthError, "Foo", "bar11")
+        e = self.get_error(cf, configparser.InterpolationDepthError, "Foo", "bar11")
         self.assertEqual(e.args, ("bar11", "Foo", rawval[self.config_class]))
 
     def test_interpolation_missing_value(self):
@@ -417,8 +531,8 @@
             configparser.ConfigParser: '%(reference)s',
             configparser.SafeConfigParser: '',
         }
-        self.get_interpolation_config()
-        e = self.get_error(configparser.InterpolationMissingOptionError,
+        cf = self.get_interpolation_config()
+        e = self.get_error(cf, configparser.InterpolationMissingOptionError,
                            "Interpolation Error", "name")
         self.assertEqual(e.reference, "reference")
         self.assertEqual(e.section, "Interpolation Error")
@@ -482,7 +596,7 @@
         # during performance updates in Python 3.2
         cf_from_file = self.newconfig()
         with open(support.TESTFN) as f:
-            cf_from_file.readfp(f)
+            cf_from_file.read_file(f)
         self.assertEqual(cf_from_file.get('section8', 'lovely_spam4'),
                          self.wonderful_spam.replace('\t\n', '\n'))
 
@@ -645,15 +759,15 @@
     dict_type = SortedDict
 
     def test_sorted(self):
-        self.fromstring("[b]\n"
-                        "o4=1\n"
-                        "o3=2\n"
-                        "o2=3\n"
-                        "o1=4\n"
-                        "[a]\n"
-                        "k=v\n")
+        cf = self.fromstring("[b]\n"
+                             "o4=1\n"
+                             "o3=2\n"
+                             "o2=3\n"
+                             "o1=4\n"
+                             "[a]\n"
+                             "k=v\n")
         output = io.StringIO()
-        self.cf.write(output)
+        cf.write(output)
         self.assertEquals(output.getvalue(),
                           "[a]\n"
                           "k = v\n\n"
@@ -697,6 +811,7 @@
         SafeConfigParserTestCaseNoValue,
         SafeConfigParserTestCaseTrickyFile,
         SortedTestCase,
+        StrictTestCase,
         CompatibleTestCase,
         )
 

Modified: python/branches/py3k/Misc/NEWS
==============================================================================
--- python/branches/py3k/Misc/NEWS	(original)
+++ python/branches/py3k/Misc/NEWS	Mon Aug  9 14:52:45 2010
@@ -67,6 +67,9 @@
 Library
 -------
 
+- Issue #9452: Add read_file, read_string, and read_dict to the configparser 
+  API; new source attribute to exceptions.
+
 - Issue #6231: Fix xml.etree.ElementInclude to include the tail of the
   current node.
 


More information about the Python-checkins mailing list