From b6a6f5f886ed869612e16cd1e29a1190996dc78d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 3 Dec 2010 16:28:00 +0000 Subject: [PATCH] Issue 10499: Modular interpolation in configparser --- Doc/library/configparser.rst | 561 +++++++++++++++++++---------------- Doc/library/fileformats.rst | 2 +- Lib/configparser.py | 540 ++++++++++++++++++--------------- Lib/test/test_cfgparser.py | 77 ++++- Misc/NEWS | 8 + 5 files changed, 691 insertions(+), 497 deletions(-) diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst index df7a1de15c4..6822176093d 100644 --- a/Doc/library/configparser.rst +++ b/Doc/library/configparser.rst @@ -17,11 +17,10 @@ single: ini file single: Windows ini file -This module provides the classes :class:`RawConfigParser` and -:class:`SafeConfigParser`. They implement a basic configuration -language which provides a structure similar to what's found in Microsoft -Windows INI files. You can use this to write Python programs which can be -customized by end users easily. +This module provides the :class:`SafeConfigParser` class which implements +a basic configuration language which provides a structure similar to what's +found in Microsoft Windows INI files. You can use this to write Python +programs which can be customized by end users easily. .. note:: @@ -34,6 +33,10 @@ customized by end users easily. Support for a creating Unix shell-like mini-languages which can be used as an alternate format for application configuration files. + Module :mod:`json` + The json module implements a subset of JavaScript syntax which can also + be used for this purpose. + Quick Start ----------- @@ -43,17 +46,17 @@ Let's take a very basic configuration file that looks like this: .. code-block:: ini [DEFAULT] - ServerAliveInterval = 45 - Compression = yes - CompressionLevel = 9 - ForwardX11 = yes + ServerAliveInterval = 45 + Compression = yes + CompressionLevel = 9 + ForwardX11 = yes [bitbucket.org] - User = hg + User = hg [topsecret.server.com] - Port = 50022 - ForwardX11 = no + Port = 50022 + ForwardX11 = no The structure of INI files is described `in the following section <#supported-ini-file-structure>`_. Essentially, the file @@ -64,7 +67,7 @@ creating the above configuration file programatically. .. doctest:: >>> import configparser - >>> config = configparser.RawConfigParser() + >>> config = configparser.SafeConfigParser() >>> config['DEFAULT'] = {'ServerAliveInterval': '45', ... 'Compression': 'yes', ... 'CompressionLevel': '9'} @@ -89,7 +92,7 @@ back and explore the data it holds. .. doctest:: >>> import configparser - >>> config = configparser.RawConfigParser() + >>> config = configparser.SafeConfigParser() >>> config.sections() [] >>> config.read('example.ini') @@ -233,23 +236,26 @@ by a whitespace character to be recognized as a comment. For backwards compatibility, by default only ``;`` starts an inline comment, while ``#`` does not [1]_. -On top of the core functionality, :class:`SafeConfigParser` supports -interpolation. This means values can contain format strings which refer to -other values in the same section, or values in a special ``DEFAULT`` section -[1]_. Additional defaults can be provided on initialization. - For example: .. code-block:: ini - [Paths] - home_dir: /Users - my_dir: %(home_dir)s/lumberjack - my_pictures: %(my_dir)s/Pictures + [Simple Values] + key: value + spaces in keys: allowed + spaces in values: allowed as well + you can also use = to delimit keys from values + + [All Values Are Strings] + values like this: 1000000 + or this: 3.14159265359 + are they treated as numbers? : no + integers, floats and booleans are held as: strings + can use the API to get converted values directly: true [Multiline Values] chorus: I'm a lumberjack, and I'm okay - I sleep all night and I work all day + I sleep all night and I work all day [No Values] key_without_value @@ -262,28 +268,92 @@ For example: multiline ;comment value! ;comment - [Sections Can Be Indented] - can_values_be_as_well = True - does_that_mean_anything_special = False - purpose = formatting for readability - multiline_values = are - handled just fine as - long as they are indented - deeper than the first line - of a value - # Did I mention we can indent comments, too? + [Sections Can Be Indented] + can_values_be_as_well = True + does_that_mean_anything_special = False + purpose = formatting for readability + multiline_values = are + handled just fine as + long as they are indented + deeper than the first line + of a value + # Did I mention we can indent comments, too? -In the example above, :class:`SafeConfigParser` would resolve ``%(home_dir)s`` -to the value of ``home_dir`` (``/Users`` in this case). ``%(my_dir)s`` in -effect would resolve to ``/Users/lumberjack``. All interpolations are done on -demand so keys used in the chain of references do not have to be specified in -any specific order in the configuration file. -:class:`RawConfigParser` would simply return ``%(my_dir)s/Pictures`` as the -value of ``my_pictures`` and ``%(home_dir)s/lumberjack`` as the value of -``my_dir``. Other features presented in the example are handled in the same -manner by both parsers. +Interpolation of values +----------------------- +On top of the core functionality, :class:`SafeConfigParser` supports +interpolation. This means values can be preprocessed before returning them +from ``get()`` calls. + +.. class:: BasicInterpolation() + + The default implementation used by :class:`SafeConfigParser`. It enables + values to contain format strings which refer to other values in the same + section, or values in the special default section [1]_. Additional default + values can be provided on initialization. + + For example: + + .. code-block:: ini + + [Paths] + home_dir: /Users + my_dir: %(home_dir)s/lumberjack + my_pictures: %(my_dir)s/Pictures + + + In the example above, :class:`SafeConfigParser` with *interpolation* set to + ``BasicInterpolation()`` would resolve ``%(home_dir)s`` to the value of + ``home_dir`` (``/Users`` in this case). ``%(my_dir)s`` in effect would + resolve to ``/Users/lumberjack``. All interpolations are done on demand so + keys used in the chain of references do not have to be specified in any + specific order in the configuration file. + + With ``interpolation`` set to ``None``, the parser would simply return + ``%(my_dir)s/Pictures`` as the value of ``my_pictures`` and + ``%(home_dir)s/lumberjack`` as the value of ``my_dir``. + +.. class:: ExtendedInterpolation() + + An alternative handler for interpolation which implements a more advanced + 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). + + For example, the configuration specified above with basic interpolation, + would look like this with extended interpolation: + + .. code-block:: ini + + [Paths] + home_dir: /Users + my_dir: ${home_dir}/lumberjack + my_pictures: ${my_dir}/Pictures + + Values from other sections can be fetched as well: + + .. code-block:: ini + + [Common] + home_dir: /Users + library_dir: /Library + system_dir: /System + macports_dir: /opt/local + + [Frameworks] + Python: 3.2 + path: ${Common:system_dir}/Library/Frameworks/ + + [Arthur] + nickname: Two Sheds + last_name: Jackson + my_dir: ${Common:home_dir}/twosheds + my_pictures: ${my_dir}/Pictures + python_dir: ${Frameworks:path}/Python/Versions/${Frameworks:Python} Mapping Protocol Access ----------------------- @@ -350,9 +420,9 @@ the :meth:`__init__` options: * *defaults*, default value: ``None`` This option accepts a dictionary of key-value pairs which will be initially - put in the ``DEFAULTSECT``. This makes for an elegant way to support concise - configuration files that don't specify values which are the same as the - documented default. + put in the ``DEFAULT`` section. This makes for an elegant way to support + concise configuration files that don't specify values which are the same as + the documented default. Hint: if you want to specify default values for a specific section, use :meth:`read_dict` before you read the actual file. @@ -374,7 +444,7 @@ the :meth:`__init__` options: .. doctest:: - >>> parser = configparser.RawConfigParser() + >>> parser = configparser.SafeConfigParser() >>> parser.read_dict({'section1': {'key1': 'value1', ... 'key2': 'value2', ... 'key3': 'value3'}, @@ -395,7 +465,7 @@ the :meth:`__init__` options: .. doctest:: >>> from collections import OrderedDict - >>> parser = configparser.RawConfigParser() + >>> parser = configparser.SafeConfigParser() >>> parser.read_dict( ... OrderedDict(( ... ('s1', @@ -441,7 +511,7 @@ the :meth:`__init__` options: ... skip-bdb ... skip-innodb # we don't need ACID today ... """ - >>> config = configparser.RawConfigParser(allow_no_value=True) + >>> config = configparser.SafeConfigParser(allow_no_value=True) >>> config.read_string(sample_config) >>> # Settings with values are treated as before: @@ -464,7 +534,7 @@ the :meth:`__init__` options: This means values (but not keys) can contain the delimiters. See also the *space_around_delimiters* argument to - :meth:`RawConfigParser.write`. + :meth:`SafeConfigParser.write`. * *comment_prefixes*, default value: ``_COMPATIBLE`` (``'#'`` valid on empty lines, ``';'`` valid also on non-empty lines) @@ -512,6 +582,31 @@ the :meth:`__init__` options: will make empty lines split keys every time. In the example above, it would produce two keys, ``key`` and ``this``. +* *default_section*, default value: ``configparser.DEFAULTSECT`` (that is: + ``"DEFAULT"``) + + 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 + 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 + ``parser_instance.default_section`` attribute and may be modified at runtime + (i.e. to convert files from one format to another). + +* *interpolation*, default value: ``configparser.BasicInterpolation`` + + 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 + `dedicated documentation section <#interpolation-of-values>`_. + + .. note:: :class:`RawConfigParser` is using ``None`` by default and + :class:`ConfigParser` is using ``configparser.BrokenInterpolation``. + More advanced customization may be achieved by overriding default values of these parser attributes. The defaults are defined on the classes, so they @@ -527,7 +622,7 @@ may be overriden by subclasses or by attribute assignment. .. doctest:: - >>> custom = configparser.RawConfigParser() + >>> custom = configparser.SafeConfigParser() >>> custom['section1'] = {'funky': 'nope'} >>> custom['section1'].getboolean('funky') Traceback (most recent call last): @@ -557,7 +652,7 @@ may be overriden by subclasses or by attribute assignment. ... [Section2] ... AnotherKey = Value ... """ - >>> typical = configparser.RawConfigParser() + >>> typical = configparser.SafeConfigParser() >>> typical.read_string(config) >>> list(typical['Section1'].keys()) ['key'] @@ -623,8 +718,7 @@ An example of reading the configuration file again:: if config.getboolean('Section1', 'bool'): print(config.get('Section1', 'foo')) -To get interpolation, use :class:`SafeConfigParser` or, if -you absolutely have to, a :class:`ConfigParser`:: +To get interpolation, use :class:`SafeConfigParser`:: import configparser @@ -672,14 +766,14 @@ used in interpolation if an option used is not defined elsewhere. :: print(config.get('Section1', 'foo')) # -> "Life is hard!" -.. _rawconfigparser-objects: +.. _safeconfigparser-objects: -RawConfigParser Objects ------------------------ +SafeConfigParser Objects +------------------------ -.. class:: RawConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, strict=False, empty_lines_in_values=True) +.. class:: SafeConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, strict=False, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT, interpolation=BasicInterpolation()) - The basic configuration parser. When *defaults* is given, it is initialized + The main configuration parser. When *defaults* is given, it is initialized into the dictionary of intrinsic defaults. When *dict_type* is given, it will be used to create the dictionary objects for the list of sections, for the options within a section, and for the default values. @@ -698,16 +792,33 @@ RawConfigParser Objects (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``. + values are accepted; the value held for these is ``None`` and they are + serialized without the trailing delimiter. - This class does not support the magical interpolation behavior. + 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 + 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 + `dedicated documentation section <#interpolation-of-values>`_. + + All option names used in interpolation will be passed through the + :meth:`optionxform` method just like any other option name reference. For + example, using the default implementation of :meth:`optionxform` (which + converts option names to lower case), the values ``foo %(bar)s`` and ``foo + %(BAR)s`` are equivalent. .. versionchanged:: 3.1 The default *dict_type* is :class:`collections.OrderedDict`. .. versionchanged:: 3.2 - *allow_no_value*, *delimiters*, *comment_prefixes*, *strict* and - *empty_lines_in_values* were added. + *allow_no_value*, *delimiters*, *comment_prefixes*, *strict*, + *empty_lines_in_values*, *default_section* and *interpolation* were + added. .. method:: defaults() @@ -717,22 +828,21 @@ RawConfigParser Objects .. method:: sections() - Return a list of the sections available; ``DEFAULT`` is not included in - the list. + Return a list of the sections available; the *default section* is not + included in the list. .. method:: add_section(section) Add a section named *section* to the instance. If a section by the given - name already exists, :exc:`DuplicateSectionError` is raised. If the name - ``DEFAULT`` (or any of it's case-insensitive variants) is passed, - :exc:`ValueError` is raised. + name already exists, :exc:`DuplicateSectionError` is raised. If the + *default section* name is passed, :exc:`ValueError` is raised. .. method:: has_section(section) - Indicates whether the named section is present in the configuration. The - ``DEFAULT`` section is not acknowledged. + Indicates whether the named *section* is present in the configuration. + The *default section* is not acknowledged. .. method:: options(section) @@ -742,7 +852,7 @@ RawConfigParser Objects .. method:: has_option(section, option) - If the given section exists, and contains the given option, return + If the given *section* exists, and contains the given *option*, return :const:`True`; otherwise return :const:`False`. @@ -750,19 +860,20 @@ RawConfigParser Objects Attempt to read and parse a list of filenames, returning a list of filenames which were successfully parsed. If *filenames* is a string, it - is treated as a single filename. If a file named in *filenames* cannot be - opened, that file will be ignored. This is designed so that you can - specify a list of potential configuration file locations (for example, the - current directory, the user's home directory, and some system-wide - directory), and all existing configuration files in the list will be read. - If none of the named files exist, the :class:`ConfigParser` instance will - contain an empty dataset. An application which requires initial values to - be loaded from a file should load the required file or files using - :meth:`read_file` before calling :meth:`read` for any optional files:: + is treated as a single filename. If a file named in *filenames* cannot + be opened, that file will be ignored. This is designed so that you can + specify a list of potential configuration file locations (for example, + the current directory, the user's home directory, and some system-wide + directory), and all existing configuration files in the list will be + read. If none of the named files exist, the :class:`ConfigParser` + instance will contain an empty dataset. An application which requires + initial values to be loaded from a file should load the required file or + files using :meth:`read_file` before calling :meth:`read` for any + optional files:: import configparser, os - config = configparser.ConfigParser() + config = configparser.SafeConfigParser() config.read_file(open('defaults.cfg')) config.read(['site.cfg', os.path.expanduser('~/.myapp.cfg')], encoding='cp1250') @@ -810,154 +921,6 @@ RawConfigParser Objects .. versionadded:: 3.2 - .. method:: get(section, option, [vars, fallback]) - - 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 *fallback* is provided, it is used as a fallback value. ``None`` can - be provided as a *fallback* value. - - .. versionchanged:: 3.2 - Arguments *vars* and *fallback* are keyword only to protect users from - trying to use the third argument as the *fallback* fallback (especially - when using the mapping protocol). - - - .. method:: getint(section, option, [vars, fallback]) - - A convenience method which coerces the *option* in the specified *section* - to an integer. See :meth:`get` for explanation of *vars* and *fallback*. - - - .. method:: getfloat(section, option, [vars, fallback]) - - A convenience method which coerces the *option* in the specified *section* - to a floating point number. See :meth:`get` for explanation of *vars* and - *fallback*. - - - .. method:: getboolean(section, option, [vars, fallback]) - - 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`. See :meth:`get` for explanation of *vars* and - *fallback*. - - - .. method:: items(section) - - Return a list of *name*, *value* pairs for each option in the given - *section*. - - - .. method:: set(section, option, value) - - If the given section exists, set the given option to the specified value; - otherwise raise :exc:`NoSectionError`. While it is possible to use - :class:`RawConfigParser` (or :class:`ConfigParser` with *raw* parameters - set to true) for *internal* storage of non-string values, full - functionality (including interpolation and output to files) can only be - achieved using string values. - - .. note:: - - This method lets users assign non-string values to keys internally. - This behaviour is unsupported and will cause errors when attempting to - write to a file or get it in non-raw mode. **Use the mapping protocol - API** which does not allow such assignments to take place. - - - .. method:: write(fileobject, space_around_delimiters=True) - - Write a representation of the configuration to the specified :term:`file - object`, which must be opened in text mode (accepting strings). This - representation can be parsed by a future :meth:`read` call. If - *space_around_delimiters* is true, delimiters between - keys and values are surrounded by spaces. - - - .. method:: remove_option(section, option) - - Remove the specified *option* from the specified *section*. If the - section does not exist, raise :exc:`NoSectionError`. If the option - existed to be removed, return :const:`True`; otherwise return - :const:`False`. - - - .. method:: remove_section(section) - - Remove the specified *section* from the configuration. If the section in - fact existed, return ``True``. Otherwise return ``False``. - - - .. method:: optionxform(option) - - Transforms the option name *option* as found in an input file or as passed - in by client code to the form that should be used in the internal - structures. The default implementation returns a lower-case version of - *option*; subclasses may override this or client code can set an attribute - of this name on instances to affect this behavior. - - You don't need to subclass the parser to use this method, you can also - set it on an instance, to a function that takes a string argument and - returns a string. Setting it to ``str``, for example, would make option - names case sensitive:: - - cfgparser = ConfigParser() - cfgparser.optionxform = str - - Note that when reading configuration files, whitespace around the option - names is stripped before :meth:`optionxform` is called. - - - .. method:: readfp(fp, filename=None) - - .. deprecated:: 3.2 - Use :meth:`read_file` instead. - - -.. _configparser-objects: - -ConfigParser Objects --------------------- - -.. warning:: - Whenever you can, consider using :class:`SafeConfigParser` which adds - validation and escaping for the interpolation. - -The :class:`ConfigParser` class extends some methods of the -:class:`RawConfigParser` interface, adding some optional arguments. - -.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, 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 - :meth:`items` methods. - - :class:`SafeConfigParser` is generally recommended over this class if you - need interpolation. - - The values in *defaults* must be appropriate for the ``%()s`` string - interpolation. - - All option names used in interpolation will be passed through the - :meth:`optionxform` method just like any other option name reference. For - example, using the default implementation of :meth:`optionxform` (which - converts option names to lower case), the values ``foo %(bar)s`` and ``foo - %(BAR)s`` are equivalent. - - .. versionchanged:: 3.1 - The default *dict_type* is :class:`collections.OrderedDict`. - - .. versionchanged:: 3.2 - *allow_no_value*, *delimiters*, *comment_prefixes*, - *strict* and *empty_lines_in_values* were added. - .. method:: get(section, option, raw=False, [vars, fallback]) @@ -1010,40 +973,6 @@ The :class:`ConfigParser` class extends some methods of the :meth:`get` method. -.. data:: MAX_INTERPOLATION_DEPTH - - The maximum depth for recursive interpolation for :meth:`get` when the *raw* - parameter is false. This is relevant only for the :class:`ConfigParser` class. - - -.. _safeconfigparser-objects: - -SafeConfigParser Objects ------------------------- - -.. class:: SafeConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, strict=False, empty_lines_in_values=True) - - Derived class of :class:`ConfigParser` that implements a variant of the - magical interpolation feature. This implementation is more predictable as - it validates the interpolation syntax used within a configuration file. - This class also enables escaping the interpolation character (a key can have - ``%`` as part of the value by specifying ``%%`` in the file). - - Applications that don't require interpolation should use - :class:`RawConfigParser`, otherwise :class:`SafeConfigParser` is the best - option. - - .. versionchanged:: 3.1 - The default *dict_type* is :class:`collections.OrderedDict`. - - .. versionchanged:: 3.2 - *allow_no_value*, *delimiters*, *comment_prefixes*, *strict* and - *empty_lines_in_values* were added. - - - The :class:`SafeConfigParser` class implements the same extended interface - as :class:`ConfigParser`, with the following addition: - .. method:: set(section, option, value) If the given section exists, set the given option to the specified value; @@ -1051,6 +980,112 @@ SafeConfigParser Objects :exc:`TypeError` is raised. + .. method:: write(fileobject, space_around_delimiters=True) + + Write a representation of the configuration to the specified :term:`file + object`, which must be opened in text mode (accepting strings). This + representation can be parsed by a future :meth:`read` call. If + *space_around_delimiters* is true, delimiters between + keys and values are surrounded by spaces. + + + .. method:: remove_option(section, option) + + Remove the specified *option* from the specified *section*. If the + section does not exist, raise :exc:`NoSectionError`. If the option + existed to be removed, return :const:`True`; otherwise return + :const:`False`. + + + .. method:: remove_section(section) + + Remove the specified *section* from the configuration. If the section in + fact existed, return ``True``. Otherwise return ``False``. + + + .. method:: optionxform(option) + + Transforms the option name *option* as found in an input file or as passed + in by client code to the form that should be used in the internal + structures. The default implementation returns a lower-case version of + *option*; subclasses may override this or client code can set an attribute + of this name on instances to affect this behavior. + + You don't need to subclass the parser to use this method, you can also + set it on an instance, to a function that takes a string argument and + returns a string. Setting it to ``str``, for example, would make option + names case sensitive:: + + cfgparser = ConfigParser() + cfgparser.optionxform = str + + Note that when reading configuration files, whitespace around the option + names is stripped before :meth:`optionxform` is called. + + + .. method:: readfp(fp, filename=None) + + .. deprecated:: 3.2 + Use :meth:`read_file` instead. + + +.. data:: MAX_INTERPOLATION_DEPTH + + The maximum depth for recursive interpolation for :meth:`get` when the *raw* + parameter is false. This is relevant only when the default *interpolation* + is used. + + +.. _rawconfigparser-objects: + +RawConfigParser Objects +----------------------- + +.. class:: RawConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, strict=False, empty_lines_in_values=True, default_section=configaparser.DEFAULTSECT, interpolation=None) + + Legacy variant of the :class:`SafeConfigParser` with interpolation disabled + by default and an unsafe ``set`` method. + + .. note:: + Consider using :class:`SafeConfigParser` instead which checks types of + the values to be stored internally. If you don't want interpolation, you + can use ``SafeConfigParser(interpolation=None)``. + + + .. method:: set(section, option, value) + + If the given section exists, set the given option to the specified value; + otherwise raise :exc:`NoSectionError`. While it is possible to use + :class:`RawConfigParser` (or :class:`ConfigParser` with *raw* parameters + set to true) for *internal* storage of non-string values, full + functionality (including interpolation and output to files) can only be + achieved using string values. + + This method lets users assign non-string values to keys internally. This + behaviour is unsupported and will cause errors when attempting to write + to a file or get it in non-raw mode. **Use the mapping protocol API** + which does not allow such assignments to take place. + + +.. _configparser-objects: + +ConfigParser Objects +-------------------- + +.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, strict=False, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT, interpolation=BrokenInterpolation()) + + .. deprecated:: 3.2 + Whenever you can, consider using :class:`SafeConfigParser`. The + :class:`ConfigParser` provides the same functionality but its + implementation is less predictable. It does not validate the + interpolation syntax used within a configuration file. It also does not + enable escaping the interpolation character (when using + :class:`SafeConfigParser`, a key can have ``%`` as part of the value by + specifying ``%%`` in the file). On top of that, this class doesn't ensure + whether values passed to the parser object are strings which may lead to + inconsistent internal state. + + Exceptions ---------- diff --git a/Doc/library/fileformats.rst b/Doc/library/fileformats.rst index 980d4f5ffb3..e9c2e1fbbdf 100644 --- a/Doc/library/fileformats.rst +++ b/Doc/library/fileformats.rst @@ -5,7 +5,7 @@ File Formats ************ The modules described in this chapter parse various miscellaneous file formats -that aren't markup languages or are related to e-mail. +that aren't markup languages and are not related to e-mail. .. toctree:: diff --git a/Lib/configparser.py b/Lib/configparser.py index f9bb32cda81..56c02ba802b 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -4,23 +4,13 @@ A configuration file consists of sections, lead by a "[section]" header, and followed by "name: value" entries, with continuations and such in the style of RFC 822. -The option values can contain format strings which refer to other values in -the same section, or values in a special [DEFAULT] section. - -For example: - - something: %(dir)s/whatever - -would resolve the "%(dir)s" to the value of dir. All reference -expansions are done late, on demand. - Intrinsic defaults can be specified by passing them into the -ConfigParser constructor as a dictionary. +SafeConfigParser constructor as a dictionary. class: -ConfigParser -- responsible for parsing a list of - configuration files, and managing the parsed database. +SafeConfigParser -- responsible for parsing a list of + configuration files, and managing the parsed database. methods: @@ -316,7 +306,7 @@ class ParsingError(Error): def filename(self): """Deprecated, use `source'.""" warnings.warn( - "This 'filename' attribute will be removed in future versions. " + "The 'filename' attribute will be removed in future versions. " "Use 'source' instead.", DeprecationWarning, stacklevel=2 ) @@ -362,6 +352,204 @@ _COMPATIBLE = object() _UNSET = object() +class Interpolation: + """Dummy interpolation that passes the value through with no changes.""" + + def before_get(self, parser, section, option, value, defaults): + return value + + def before_set(self, parser, section, option, value): + return value + + def before_read(self, parser, section, option, value): + return value + + def before_write(self, parser, section, option, value): + return value + + +class BasicInterpolation(Interpolation): + """Interpolation as implemented in the classic SafeConfigParser. + + The option values can contain format strings which refer to other values in + the same section, or values in the special default section. + + For example: + + something: %(dir)s/whatever + + would resolve the "%(dir)s" to the value of dir. All reference + expansions are done late, on demand. If a user needs to use a bare % in + a configuration file, she can escape it by writing %%. Other other % usage + is considered a user error and raises `InterpolationSyntaxError'.""" + + _KEYCRE = re.compile(r"%\(([^)]+)\)s") + + def before_get(self, parser, section, option, value, defaults): + L = [] + self._interpolate_some(parser, option, L, value, section, defaults, 1) + return ''.join(L) + + def before_set(self, parser, section, option, value): + tmp_value = value.replace('%%', '') # escaped percent signs + tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax + if '%' in tmp_value: + raise ValueError("invalid interpolation syntax in %r at " + "position %d" % (value, tmp_value.find('%'))) + return value + + def _interpolate_some(self, parser, option, accum, rest, section, map, + depth): + if depth > MAX_INTERPOLATION_DEPTH: + raise InterpolationDepthError(option, section, rest) + while rest: + p = rest.find("%") + if p < 0: + accum.append(rest) + return + if p > 0: + accum.append(rest[:p]) + rest = rest[p:] + # p is no longer used + c = rest[1:2] + if c == "%": + accum.append("%") + rest = rest[2:] + elif c == "(": + m = self._KEYCRE.match(rest) + if m is None: + raise InterpolationSyntaxError(option, section, + "bad interpolation variable reference %r" % rest) + var = parser.optionxform(m.group(1)) + rest = rest[m.end():] + try: + v = map[var] + except KeyError: + raise InterpolationMissingOptionError( + option, section, rest, var) + if "%" in v: + self._interpolate_some(parser, option, accum, v, + section, map, depth + 1) + else: + accum.append(v) + else: + raise InterpolationSyntaxError( + option, section, + "'%%' must be followed by '%%' or '(', " + "found: %r" % (rest,)) + + +class ExtendedInterpolation(Interpolation): + """Advanced variant of interpolation, supports the syntax used by + `zc.buildout'. Enables interpolation between sections.""" + + _KEYCRE = re.compile(r"\$\{([^}]+)\}") + + def before_get(self, parser, section, option, value, defaults): + L = [] + self._interpolate_some(parser, option, L, value, section, defaults, 1) + return ''.join(L) + + def before_set(self, parser, section, option, value): + tmp_value = value.replace('$$', '') # escaped dollar signs + tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax + if '$' in tmp_value: + raise ValueError("invalid interpolation syntax in %r at " + "position %d" % (value, tmp_value.find('%'))) + return value + + def _interpolate_some(self, parser, option, accum, rest, section, map, + depth): + if depth > MAX_INTERPOLATION_DEPTH: + raise InterpolationDepthError(option, section, rest) + while rest: + p = rest.find("$") + if p < 0: + accum.append(rest) + return + if p > 0: + accum.append(rest[:p]) + rest = rest[p:] + # p is no longer used + c = rest[1:2] + if c == "$": + accum.append("$") + rest = rest[2:] + elif c == "{": + m = self._KEYCRE.match(rest) + if m is None: + raise InterpolationSyntaxError(option, section, + "bad interpolation variable reference %r" % rest) + path = parser.optionxform(m.group(1)).split(':') + rest = rest[m.end():] + sect = section + opt = option + try: + if len(path) == 1: + opt = path[0] + v = map[opt] + elif len(path) == 2: + sect = path[0] + opt = path[1] + v = parser.get(sect, opt, raw=True) + else: + raise InterpolationSyntaxError( + option, section, + "More than one ':' found: %r" % (rest,)) + except KeyError: + raise InterpolationMissingOptionError( + option, section, rest, var) + if "$" in v: + self._interpolate_some(parser, opt, accum, v, sect, + dict(parser.items(sect, raw=True)), + depth + 1) + else: + accum.append(v) + else: + raise InterpolationSyntaxError( + option, section, + "'$' must be followed by '$' or '{', " + "found: %r" % (rest,)) + + +class BrokenInterpolation(Interpolation): + """Deprecated interpolation as implemented in the classic ConfigParser. + Use BasicInterpolation or ExtendedInterpolation instead.""" + + _KEYCRE = re.compile(r"%\(([^)]*)\)s|.") + + def before_get(self, parser, section, option, value, vars): + rawval = value + depth = MAX_INTERPOLATION_DEPTH + while depth: # Loop through this until it's done + depth -= 1 + if value and "%(" in value: + replace = functools.partial(self._interpolation_replace, + parser=parser) + value = self._KEYCRE.sub(replace, value) + try: + value = value % vars + except KeyError as e: + raise InterpolationMissingOptionError( + option, section, rawval, e.args[0]) + else: + break + if value and "%(" in value: + raise InterpolationDepthError(option, section, rawval) + return value + + def before_set(self, parser, section, option, value): + return value + + @staticmethod + def _interpolation_replace(match, parser): + s = match.group(1) + if s is None: + return match.group() + else: + return "%%(%s)s" % parser.optionxform(s) + + class RawConfigParser(MutableMapping): """ConfigParser that does not do interpolation.""" @@ -388,7 +576,8 @@ class RawConfigParser(MutableMapping): # space/tab (?P.*))?$ # everything up to eol """ - + # Interpolation algorithm to be used if the user does not specify another + _DEFAULT_INTERPOLATION = Interpolation() # Compiled regular expression for matching sections SECTCRE = re.compile(_SECT_TMPL, re.VERBOSE) # Compiled regular expression for matching options with typical separators @@ -406,7 +595,15 @@ class RawConfigParser(MutableMapping): allow_no_value=False, *, delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, strict=False, empty_lines_in_values=True, - default_section=DEFAULTSECT): + default_section=DEFAULTSECT, + interpolation=_UNSET): + + if self.__class__ is RawConfigParser: + warnings.warn( + "The RawConfigParser class will be removed in future versions." + " Use 'SafeConfigParser(interpolation=None)' instead.", + DeprecationWarning, stacklevel=2 + ) self._dict = dict_type self._sections = self._dict() self._defaults = self._dict() @@ -435,7 +632,11 @@ class RawConfigParser(MutableMapping): self._strict = strict self._allow_no_value = allow_no_value self._empty_lines_in_values = empty_lines_in_values - self._default_section=default_section + if interpolation is _UNSET: + self._interpolation = self._DEFAULT_INTERPOLATION + else: + self._interpolation = interpolation + self.default_section=default_section def defaults(self): return self._defaults @@ -451,7 +652,7 @@ class RawConfigParser(MutableMapping): Raise DuplicateSectionError if a section by the specified name already exists. Raise ValueError if name is DEFAULT. """ - if section == self._default_section: + if section == self.default_section: raise ValueError('Invalid section name: %s' % section) if section in self._sections: @@ -555,7 +756,7 @@ class RawConfigParser(MutableMapping): ) self.read_file(fp, source=filename) - def get(self, section, option, *, vars=None, fallback=_UNSET): + def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET): """Get an option value for a given section. If `vars' is provided, it must be a dictionary. The option is looked up @@ -563,7 +764,12 @@ class RawConfigParser(MutableMapping): If the key is not found and `fallback' is provided, it is used as a fallback value. `None' can be provided as a `fallback' value. - Arguments `vars' and `fallback' are keyword only. + If interpolation is enabled and the optional argument `raw' is False, + all interpolations are expanded in the return values. + + Arguments `raw', `vars', and `fallback' are keyword only. + + The section DEFAULT is special. """ try: d = self._unify_values(section, vars) @@ -574,61 +780,90 @@ class RawConfigParser(MutableMapping): return fallback option = self.optionxform(option) try: - return d[option] + value = d[option] except KeyError: if fallback is _UNSET: raise NoOptionError(option, section) else: return fallback - def items(self, section): - try: - d2 = self._sections[section] - except KeyError: - if section != self._default_section: - raise NoSectionError(section) - d2 = self._dict() - d = self._defaults.copy() - d.update(d2) - return d.items() + if raw or value is None: + return value + else: + return self._interpolation.before_get(self, section, option, value, + d) def _get(self, section, conv, option, **kwargs): return conv(self.get(section, option, **kwargs)) - def getint(self, section, option, *, vars=None, fallback=_UNSET): + def getint(self, section, option, *, raw=False, vars=None, + fallback=_UNSET): try: - return self._get(section, int, option, vars=vars) + return self._get(section, int, option, raw=raw, vars=vars) except (NoSectionError, NoOptionError): if fallback is _UNSET: raise else: return fallback - def getfloat(self, section, option, *, vars=None, fallback=_UNSET): + def getfloat(self, section, option, *, raw=False, vars=None, + fallback=_UNSET): try: - return self._get(section, float, option, vars=vars) + return self._get(section, float, option, raw=raw, vars=vars) except (NoSectionError, NoOptionError): if fallback is _UNSET: raise else: return fallback - def getboolean(self, section, option, *, vars=None, fallback=_UNSET): + def getboolean(self, section, option, *, raw=False, vars=None, + fallback=_UNSET): try: return self._get(section, self._convert_to_boolean, option, - vars=vars) + raw=raw, vars=vars) except (NoSectionError, NoOptionError): if fallback is _UNSET: raise else: return fallback + def items(self, section, raw=False, vars=None): + """Return a list of (name, value) tuples for each option in a section. + + All % interpolations are expanded in the return values, based on the + defaults passed into the constructor, unless the optional argument + `raw' is true. Additional substitutions may be provided using the + `vars' argument, which must be a dictionary whose contents overrides + any pre-existing defaults. + + The section DEFAULT is special. + """ + d = self._defaults.copy() + try: + d.update(self._sections[section]) + except KeyError: + if section != self.default_section: + raise NoSectionError(section) + # Update with the entry specific variables + if vars: + for key, value in vars.items(): + d[self.optionxform(key)] = value + options = list(d.keys()) + if raw: + return [(option, d[option]) + for option in options] + else: + return [(option, self._interpolation.before_get(self, section, + option, d[option], + d)) + for option in options] + def optionxform(self, optionstr): return optionstr.lower() def has_option(self, section, option): """Check for the existence of a given option in a given section.""" - if not section or section == self._default_section: + if not section or section == self.default_section: option = self.optionxform(option) return option in self._defaults elif section not in self._sections: @@ -640,7 +875,10 @@ class RawConfigParser(MutableMapping): def set(self, section, option, value=None): """Set an option.""" - if not section or section == self._default_section: + if value: + value = self._interpolation.before_set(self, section, option, + value) + if not section or section == self.default_section: sectdict = self._defaults else: try: @@ -660,7 +898,7 @@ class RawConfigParser(MutableMapping): else: d = self._delimiters[0] if self._defaults: - self._write_section(fp, self._default_section, + self._write_section(fp, self.default_section, self._defaults.items(), d) for section in self._sections: self._write_section(fp, section, @@ -670,6 +908,8 @@ class RawConfigParser(MutableMapping): """Write a single section to the specified `fp'.""" fp.write("[{}]\n".format(section_name)) for key, value in section_items: + value = self._interpolation.before_write(self, section_name, key, + value) if value is not None or not self._allow_no_value: value = delimiter + str(value).replace('\n', '\n\t') else: @@ -679,7 +919,7 @@ class RawConfigParser(MutableMapping): def remove_option(self, section, option): """Remove an option.""" - if not section or section == self._default_section: + if not section or section == self.default_section: sectdict = self._defaults else: try: @@ -701,7 +941,7 @@ class RawConfigParser(MutableMapping): return existed def __getitem__(self, key): - if key != self._default_section and not self.has_section(key): + if key != self.default_section and not self.has_section(key): raise KeyError(key) return self._proxies[key] @@ -715,21 +955,21 @@ class RawConfigParser(MutableMapping): self.read_dict({key: value}) def __delitem__(self, key): - if key == self._default_section: + if key == self.default_section: raise ValueError("Cannot remove the default section.") if not self.has_section(key): raise KeyError(key) self.remove_section(key) def __contains__(self, key): - return key == self._default_section or self.has_section(key) + return key == self.default_section or self.has_section(key) def __len__(self): return len(self._sections) + 1 # the default section def __iter__(self): # XXX does it break when underlying container state changed? - return itertools.chain((self._default_section,), self._sections.keys()) + return itertools.chain((self.default_section,), self._sections.keys()) def _read(self, fp, fpname): """Parse a sectioned configuration file. @@ -801,7 +1041,7 @@ class RawConfigParser(MutableMapping): lineno) cursect = self._sections[sectname] elements_added.add(sectname) - elif sectname == self._default_section: + elif sectname == self.default_section: cursect = self._defaults else: cursect = self._dict() @@ -836,7 +1076,7 @@ class RawConfigParser(MutableMapping): cursect[optname] = [optval] else: # valueless option handling - cursect[optname] = optval + cursect[optname] = None else: # a non-fatal parsing error occurred. set up the # exception but keep going. the exception will be @@ -849,12 +1089,16 @@ class RawConfigParser(MutableMapping): self._join_multiline_values() def _join_multiline_values(self): - all_sections = itertools.chain((self._defaults,), - self._sections.values()) - for options in all_sections: + defaults = self.default_section, self._defaults + all_sections = itertools.chain((defaults,), + self._sections.items()) + for section, options in all_sections: for name, val in options.items(): if isinstance(val, list): - options[name] = '\n'.join(val).rstrip() + val = '\n'.join(val).rstrip() + options[name] = self._interpolation.before_read(self, + section, + name, val) def _handle_error(self, exc, fpname, lineno, line): if not exc: @@ -871,7 +1115,7 @@ class RawConfigParser(MutableMapping): try: d.update(self._sections[section]) except KeyError: - if section != self._default_section: + if section != self.default_section: raise NoSectionError(section) # Update with the entry specific variables if vars: @@ -906,197 +1150,31 @@ class RawConfigParser(MutableMapping): raise TypeError("option values must be strings") - class ConfigParser(RawConfigParser): """ConfigParser implementing interpolation.""" - def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET): - """Get an option value for a given section. + _DEFAULT_INTERPOLATION = BrokenInterpolation() - 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 `fallback' is provided, it is used as - a fallback value. `None' can be provided as a `fallback' value. - - All % interpolations are expanded in the return values, unless the - optional argument `raw' is true. Values for interpolation keys are - looked up in the same manner as the option. - - Arguments `raw', `vars', and `fallback' are keyword only. - - The section DEFAULT is special. - """ - try: - d = self._unify_values(section, vars) - except NoSectionError: - if fallback is _UNSET: - raise - else: - return fallback - option = self.optionxform(option) - try: - value = d[option] - except KeyError: - if fallback is _UNSET: - raise NoOptionError(option, section) - else: - return fallback - - 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, - fallback=_UNSET): - try: - return self._get(section, int, option, raw=raw, vars=vars) - except (NoSectionError, NoOptionError): - if fallback is _UNSET: - raise - else: - return fallback - - 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 - - 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 - - def items(self, section, raw=False, vars=None): - """Return a list of (name, value) tuples for each option in a section. - - All % interpolations are expanded in the return values, based on the - defaults passed into the constructor, unless the optional argument - `raw' is true. Additional substitutions may be provided using the - `vars' argument, which must be a dictionary whose contents overrides - any pre-existing defaults. - - The section DEFAULT is special. - """ - d = self._defaults.copy() - try: - d.update(self._sections[section]) - except KeyError: - if section != self._default_section: - raise NoSectionError(section) - # Update with the entry specific variables - if vars: - for key, value in vars.items(): - d[self.optionxform(key)] = value - options = list(d.keys()) - if raw: - return [(option, d[option]) - for option in options] - else: - return [(option, self._interpolate(section, option, d[option], d)) - for option in options] - - def _interpolate(self, section, option, rawval, vars): - # do the string interpolation - value = rawval - depth = MAX_INTERPOLATION_DEPTH - while depth: # Loop through this until it's done - depth -= 1 - if value and "%(" in value: - value = self._KEYCRE.sub(self._interpolation_replace, value) - try: - value = value % vars - except KeyError as e: - raise InterpolationMissingOptionError( - option, section, rawval, e.args[0]) - else: - break - if value and "%(" in value: - raise InterpolationDepthError(option, section, rawval) - return value - - _KEYCRE = re.compile(r"%\(([^)]*)\)s|.") - - def _interpolation_replace(self, match): - s = match.group(1) - if s is None: - return match.group() - else: - return "%%(%s)s" % self.optionxform(s) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.__class__ is ConfigParser: + warnings.warn( + "The ConfigParser class will be removed in future versions." + " Use SafeConfigParser instead.", + DeprecationWarning, stacklevel=2 + ) class SafeConfigParser(ConfigParser): """ConfigParser implementing sane interpolation.""" - def _interpolate(self, section, option, rawval, vars): - # do the string interpolation - L = [] - self._interpolate_some(option, L, rawval, section, vars, 1) - return ''.join(L) - - _interpvar_re = re.compile(r"%\(([^)]+)\)s") - - def _interpolate_some(self, option, accum, rest, section, map, depth): - if depth > MAX_INTERPOLATION_DEPTH: - raise InterpolationDepthError(option, section, rest) - while rest: - p = rest.find("%") - if p < 0: - accum.append(rest) - return - if p > 0: - accum.append(rest[:p]) - rest = rest[p:] - # p is no longer used - c = rest[1:2] - if c == "%": - accum.append("%") - rest = rest[2:] - elif c == "(": - m = self._interpvar_re.match(rest) - if m is None: - raise InterpolationSyntaxError(option, section, - "bad interpolation variable reference %r" % rest) - var = self.optionxform(m.group(1)) - rest = rest[m.end():] - try: - v = map[var] - except KeyError: - raise InterpolationMissingOptionError( - option, section, rest, var) - if "%" in v: - self._interpolate_some(option, accum, v, - section, map, depth + 1) - else: - accum.append(v) - else: - raise InterpolationSyntaxError( - option, section, - "'%%' must be followed by '%%' or '(', " - "found: %r" % (rest,)) + _DEFAULT_INTERPOLATION = BasicInterpolation() def set(self, section, option, value=None): - """Set an option. Extend ConfigParser.set: check for string values.""" + """Set an option. Extends RawConfigParser.set by validating type and + interpolation syntax on the value.""" self._validate_value_type(value) - # 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) + super().set(section, option, value) class SectionProxy(MutableMapping): diff --git a/Lib/test/test_cfgparser.py b/Lib/test/test_cfgparser.py index 38dd34bc001..1ae720ecbb0 100644 --- a/Lib/test/test_cfgparser.py +++ b/Lib/test/test_cfgparser.py @@ -4,6 +4,7 @@ import io import os import unittest import textwrap +import warnings from test import support @@ -32,6 +33,7 @@ class CfgParserTestCaseClass(unittest.TestCase): dict_type = configparser._default_dict strict = False default_section = configparser.DEFAULTSECT + interpolation = configparser._UNSET def newconfig(self, defaults=None): arguments = dict( @@ -43,8 +45,12 @@ class CfgParserTestCaseClass(unittest.TestCase): dict_type=self.dict_type, strict=self.strict, default_section=self.default_section, + interpolation=self.interpolation, ) - return self.config_class(**arguments) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + instance = self.config_class(**arguments) + return instance def fromstring(self, string, defaults=None): cf = self.newconfig(defaults) @@ -847,6 +853,70 @@ class SafeConfigParserTestCase(ConfigParserTestCase): cf = self.newconfig() self.assertRaises(ValueError, cf.add_section, self.default_section) +class SafeConfigParserTestCaseExtendedInterpolation(BasicTestCase): + config_class = configparser.SafeConfigParser + interpolation = configparser.ExtendedInterpolation() + default_section = 'common' + + def test_extended_interpolation(self): + cf = self.fromstring(textwrap.dedent(""" + [common] + favourite Beatle = Paul + favourite color = green + + [tom] + favourite band = ${favourite color} day + favourite pope = John ${favourite Beatle} II + sequel = ${favourite pope}I + + [ambv] + favourite Beatle = George + son of Edward VII = ${favourite Beatle} V + son of George V = ${son of Edward VII}I + + [stanley] + favourite Beatle = ${ambv:favourite Beatle} + favourite pope = ${tom:favourite pope} + favourite color = black + favourite state of mind = paranoid + favourite movie = soylent ${common:favourite color} + favourite song = ${favourite color} sabbath - ${favourite state of mind} + """).strip()) + + eq = self.assertEqual + eq(cf['common']['favourite Beatle'], 'Paul') + eq(cf['common']['favourite color'], 'green') + eq(cf['tom']['favourite Beatle'], 'Paul') + eq(cf['tom']['favourite color'], 'green') + eq(cf['tom']['favourite band'], 'green day') + eq(cf['tom']['favourite pope'], 'John Paul II') + eq(cf['tom']['sequel'], 'John Paul III') + eq(cf['ambv']['favourite Beatle'], 'George') + eq(cf['ambv']['favourite color'], 'green') + eq(cf['ambv']['son of Edward VII'], 'George V') + eq(cf['ambv']['son of George V'], 'George VI') + eq(cf['stanley']['favourite Beatle'], 'George') + eq(cf['stanley']['favourite color'], 'black') + eq(cf['stanley']['favourite state of mind'], 'paranoid') + eq(cf['stanley']['favourite movie'], 'soylent green') + eq(cf['stanley']['favourite pope'], 'John Paul II') + eq(cf['stanley']['favourite song'], + 'black sabbath - paranoid') + + def test_endless_loop(self): + cf = self.fromstring(textwrap.dedent(""" + [one for you] + ping = ${one for me:pong} + + [one for me] + pong = ${one for you:ping} + """).strip()) + + with self.assertRaises(configparser.InterpolationDepthError): + cf['one for you']['ping'] + + + class SafeConfigParserTestCaseNonStandardDelimiters(SafeConfigParserTestCase): delimiters = (':=', '$') comment_prefixes = ('//', '"') @@ -910,7 +980,9 @@ class Issue7005TestCase(unittest.TestCase): def prepare(self, config_class): # This is the default, but that's the point. - cp = config_class(allow_no_value=False) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=DeprecationWarning) + cp = config_class(allow_no_value=False) cp.add_section("section") cp.set("section", "option", None) sio = io.StringIO() @@ -978,6 +1050,7 @@ def test_main(): RawConfigParserTestCaseNonStandardDelimiters, RawConfigParserTestSambaConf, SafeConfigParserTestCase, + SafeConfigParserTestCaseExtendedInterpolation, SafeConfigParserTestCaseNonStandardDelimiters, SafeConfigParserTestCaseNoValue, SafeConfigParserTestCaseTrickyFile, diff --git a/Misc/NEWS b/Misc/NEWS index 13c29cd9923..0349ef2621a 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -149,6 +149,14 @@ Library - Issue #10467: Fix BytesIO.readinto() after seeking into a position after the end of the file. +- configparser: the ConfigParser class has been deprecated in favor of + SafeConfigParser. Usage of RawConfigParser is now discouraged for new + projects in favor of SafeConfigParser(interpolation=None). + +- Issue #10499: configparser supports pluggable interpolation handlers. New + interpolation handler added (ExtendedInterpolation) which supports the syntax + used by zc.buildout (e.g. interpolation between sections). + - Issue #1682942: configparser supports alternative option/value delimiters. - Issue #5412: configparser supports mapping protocol access.