diff options
author | Łukasz Langa <lukasz@langa.pl> | 2010-11-10 18:57:39 (GMT) |
---|---|---|
committer | Łukasz Langa <lukasz@langa.pl> | 2010-11-10 18:57:39 (GMT) |
commit | 26d513cf2ff31ba7c4f8862d5d28cb4cfa71de67 (patch) | |
tree | 058c9a6c41ea34dfd04472c5328463e063bbdcd9 | |
parent | 47f637ce17022464307b093cee6f367e2dc7f024 (diff) | |
download | cpython-26d513cf2ff31ba7c4f8862d5d28cb4cfa71de67.zip cpython-26d513cf2ff31ba7c4f8862d5d28cb4cfa71de67.tar.gz cpython-26d513cf2ff31ba7c4f8862d5d28cb4cfa71de67.tar.bz2 |
Issue #5412: extend configparser to support mapping access
-rw-r--r-- | Doc/library/configparser.rst | 1047 | ||||
-rw-r--r-- | Lib/configparser.py | 241 | ||||
-rw-r--r-- | Lib/logging/config.py | 70 | ||||
-rw-r--r-- | Lib/test/test_cfgparser.py | 161 |
4 files changed, 1086 insertions, 433 deletions
diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst index d80f94e..f661866 100644 --- a/Doc/library/configparser.rst +++ b/Doc/library/configparser.rst @@ -7,7 +7,9 @@ .. moduleauthor:: Ken Manheimer <klm@zope.com> .. moduleauthor:: Barry Warsaw <bwarsaw@python.org> .. moduleauthor:: Eric S. Raymond <esr@thyrsus.com> +.. moduleauthor:: Łukasz Langa <lukasz@langa.pl> .. sectionauthor:: Christopher G. Petrilli <petrilli@amber.org> +.. sectionauthor:: Łukasz Langa <lukasz@langa.pl> .. index:: pair: .ini; file @@ -26,26 +28,219 @@ customized by end users easily. This library does *not* interpret or write the value-type prefixes used in the Windows Registry extended version of INI syntax. +.. seealso:: + + Module :mod:`shlex` + Support for a creating Unix shell-like mini-languages which can be used + as an alternate format for application configuration files. + +Quick Start +----------- + +.. highlightlang:: none + +Let's take a very basic configuration file that looks like this:: + + [DEFAULT] + ServerAliveInterval = 45 + Compression = yes + CompressionLevel = 9 + ForwardX11 = yes + + [bitbucket.org] + User = hg + + [topsecret.server.com] + Port = 50022 + ForwardX11 = no + +The supported file structure of INI files is described `in the following section +<#supported-ini-file-structure>`_, fow now all there's to know is that the file +consists of sections, each of which contains keys with values. +:mod:`configparser` classes can read and write such files. Let's start by +creating the above configuration file programatically. + +.. highlightlang:: python +.. doctest:: + + >>> import configparser + >>> config = configparser.RawConfigParser() + >>> config['DEFAULT'] = {'ServerAliveInterval': '45', + ... 'Compression': 'yes', + ... 'CompressionLevel': '9'} + >>> config['bitbucket.org'] = {} + >>> config['bitbucket.org']['User'] = 'hg' + >>> config['topsecret.server.com'] = {} + >>> topsecret = config['topsecret.server.com'] + >>> topsecret['Port'] = '50022' # mutates the parser + >>> topsecret['ForwardX11'] = 'no' # same here + >>> config['DEFAULT']['ForwardX11'] = 'yes' + >>> with open('example.ini', 'w') as configfile: + ... config.write(configfile) + ... + +As you can see, we can treat a config parser just like a dictionary. There are +a few differences, `outlined later on <#mapping-protocol-access>`_, but the +behaviour is very close to what you'd expect from a dictionary. + +Now that we've created and saved a configuration file, let's try reading it +back and exploring the data it holds. + +.. highlightlang:: python +.. doctest:: + + >>> import configparser + >>> config = configparser.RawConfigParser() + >>> config.sections() + [] + >>> config.read('example.ini') + ['example.ini'] + >>> config.sections() + ['bitbucket.org', 'topsecret.server.com'] + >>> 'bitbucket.org' in config + True + >>> 'bytebong.com' in config + False + >>> config['bitbucket.org']['User'] + 'hg' + >>> config['DEFAULT']['Compression'] + 'yes' + >>> topsecret = config['topsecret.server.com'] + >>> topsecret['ForwardX11'] + 'no' + >>> topsecret['Port'] + '50022' + >>> for key in config['bitbucket.org']: print(key) + ... + user + compressionlevel + serveraliveinterval + compression + forwardx11 + >>> config['bitbucket.org']['ForwardX11'] + 'yes' + +As we can see above, the API is pretty straight forward. The only bit of magic +involves the ``DEFAULT`` section which provides default values for all other +sections [customizable]_. Another thing to note is that keys in sections are +case-insensitive so they're stored in lowercase [customizable]_. + +Supported Datatypes +------------------- + +Config parsers do not guess datatypes of values in configuration files, always +storing them internally as strings. This means that if you need other datatypes, +you should convert on your own: + +.. highlightlang:: python +.. doctest:: + + >>> int(topsecret['Port']) + 50022 + >>> float(topsecret['CompressionLevel']) + 9.0 + +Converting to the boolean type is not that simple, though. Wrapping the return +value around ``bool()`` would do us no good since ``bool('False')`` is still +``True``. This is why config parsers also provide :meth:`getboolean`. This handy +method is also case insensitive and correctly recognizes boolean values from +``'yes'``/``'no'``, ``'on'``/``'off'`` and ``'1'``/``'0'`` [customizable]_. An +example of getting the boolean value: + +.. highlightlang:: python +.. doctest:: + + >>> topsecret.getboolean('ForwardX11') + False + >>> config['bitbucket.org'].getboolean('ForwardX11') + True + >>> config.getboolean('bitbucket.org', 'Compression') + True + +Apart from :meth:`getboolean`, config parsers also provide equivalent +:meth:`getint` and :meth:`getfloat` methods but these are far less useful +because explicit casting is enough for these types. + +Fallback Values +--------------- + +As with a regular dictionary, you can use a section's :meth:`get` method to +provide fallback values: + +.. highlightlang:: python +.. doctest:: + + >>> topsecret.get('Port') + '50022' + >>> topsecret.get('CompressionLevel') + '9' + >>> topsecret.get('Cipher') + >>> topsecret.get('Cipher', '3des-cbc') + '3des-cbc' + +Please note that default values have precedence over fallback values. For +instance, in our example the ``CompressionLevel`` key was specified only in the +``DEFAULT`` section. If we try to get it from the section +``topsecret.server.com``, we will always get the default, even if we specify +a fallback: + +.. highlightlang:: python +.. doctest:: + + >>> topsecret.get('CompressionLevel', '3') + '9' + +One more thing to be aware of is that the parser-level :meth:`get` method +provides a custom, more complex interface, maintained for backwards +compatibility. When using this method, a fallback value can be provided via the +``fallback`` keyword-only argument: + +.. highlightlang:: python +.. doctest:: + + >>> config.get('bitbucket.org', 'monster', + ... fallback='No such things as monsters') + 'No such things as monsters' + +The same ``fallback`` argument can be used with the :meth:`getint`, +:meth:`getfloat` and :meth:`getboolean` methods, for example: + +.. highlightlang:: python +.. doctest:: + + >>> 'BatchMode' in topsecret + False + >>> topsecret.getboolean('BatchMode', fallback=True) + True + >>> config['DEFAULT']['BatchMode'] = 'no' + >>> topsecret.getboolean('BatchMode', fallback=True) + False + +Supported INI File Structure +---------------------------- + A configuration file consists of sections, each led by a ``[section]`` header, 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 -omitted, 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 -as parts of multiline values or ignored. +default [customizable]_). By default, section names are case sensitive but keys +are not [customizable]_. Leading und trailing whitespace is removed from keys and from values. +Values can be omitted, 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 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 section names. In the -latter case, they need to be preceded by a whitespace character to be recognized -as a comment. (For backwards compatibility, by default only ``;`` starts an -inline comment, while ``#`` does not.) +and ``;`` by default [customizable]_). Comments may appear on their own in an +otherwise empty line, or may be entered in lines holding values or section +names. In the latter case, they need to be preceded by a whitespace character +to be recognized as a comment. (For backwards compatibility, by default only +``;`` starts an inline comment, while ``#`` does not [customizable]_.) 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. -Additional defaults can be provided on initialization. +other values in the same section, or values in a special ``DEFAULT`` section +[customizable]_. Additional defaults can be provided on initialization. + +.. highlightlang:: none For example:: @@ -80,7 +275,6 @@ For example:: 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 @@ -92,188 +286,438 @@ 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. -Default values can be specified by passing them as a dictionary when -constructing the :class:`SafeConfigParser`. - -Sections are normally stored in an :class:`collections.OrderedDict` which -maintains the order of all keys. An alternative dictionary type can be passed -to the :meth:`__init__` method. For example, if a dictionary type is passed -that sorts its keys, the sections will be sorted on write-back, as will be the -keys within each section. - - -.. 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 - will be used to create the dictionary objects for the list of sections, for - the options within a section, and for the default values. - - When *delimiters* is given, it will be used as the set of substrings that - divide keys from values. When *comment_prefixes* is given, it will be used - as the set of substrings that prefix comments in a line, both for the whole - line and inline comments. For backwards compatibility, the default value for - *comment_prefixes* is a special value that indicates that ``;`` and ``#`` can - start whole line comments while only ``;`` can start inline comments. - - 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. - - .. 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. - - -.. 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 sane 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 (e.g. 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. - - -.. 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. Note that *__name__* is an intrinsic default; its value is - the section name, and will override any value provided in *defaults*. - - 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. +Mapping Protocol Access +----------------------- - .. versionchanged:: 3.1 - The default *dict_type* is :class:`collections.OrderedDict`. +.. versionadded:: 3.2 +.. highlightlang:: python + +Mapping protocol access is a generic name for functionality that enables using +custom objects as if they were dictionaries. In case of :mod:`configparser`, +the mapping interface implementation is using the +``parser['section']['option']`` notation. + +``parser['section']`` in particular returns a proxy for the section's data in +the parser. This means that the values are not copied but they are taken from +the original parser on demand. What's even more important is that when values +are changed on a section proxy, they are actually mutated in the original +parser. + +:mod:`configparser` objects behave as close to actual dictionaries as possible. +The mapping interface is complete and adheres to the ``MutableMapping`` ABC. +However, there are a few differences that should be taken into account: + +* by default, all keys in sections are accessible in a case-insensitive manner + [customizable]_. E.g. ``for option in parser["section"]`` yields only + ``optionxform``'ed option key names. This means lowercased keys by default. + At the same time, for a section that holds the key ``"a"``, both expressions + return ``True``:: + + "a" in parser["section"] + "A" in parser["section"] + +* all sections include ``DEFAULTSECT`` values as well which means that + ``.clear()`` on a section may not leave the section visibly empty. This is + because default values cannot be deleted from the section (because technically + they are not there). If they are overriden in the section, deleting causes the + default value to be visible again. Trying to delete a default value causes + a ``KeyError``. + +* trying to delete the ``DEFAULTSECT`` throws ``ValueError`` + +* there are two parser-level methods in the legacy API that hide + the dictionary interface and are incompatible: + + * ``parser.get(section, option, **kwargs)`` - the second argument is **not** + a fallback value + + * ``parser.items(section)`` - this returns a list of ``(option, value)`` + pairs for a specified ``section``. + +The mapping protocol is implemented on top of the existing legacy API so that +subclassing the original interface makes the mappings work as expected as well. +One difference is the explicit lack of support for the `__name__` special key. +This is because the existing behaviour of `__name__` is very inconsistent and +supporting it would only lead to problems. Details `here +<http://mail.python.org/pipermail/python-dev/2010-July/102556.html>`_. + +Customizing Parser Behaviour +---------------------------- + +There are nearly as many INI format variants as there are applications using it. +:mod:`configparser` goes a long way to provide support for the largest sensible +set of INI styles available. The default functionality is mainly dictated by +historical background and it's very likely that you will want to customize some +of the features. + +The most natural way to change the way a specific config parser works is to use +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. + + Hint: if you want to specify default values for a specific section, use the + :meth:`read_dict` before you read the actual file. + +* *dict_type*, default value: :class:`collections.OrderedDict` + + This option has a major impact on how the mapping protocol will behave and how + the written configuration files will look like. With the default ordered + dictionary, every section is stored in the order they were added to the + parser. Same goes for options within sections. + + An alternative dictionary type can be used for example to sort sections and + options on write-back. You can also use a regular dictionary for performance + reasons. + + Please note: there are ways to add a set of key-value pairs in a single + operation. When you use a regular dictionary in those operations, the order of + the keys may be random. For example: + + .. highlightlang:: python + .. doctest:: + + >>> parser = configparser.RawConfigParser() + >>> parser.read_dict({'section1': {'key1': 'value1', + ... 'key2': 'value2', + ... 'key3': 'value3'}, + ... 'section2': {'keyA': 'valueA', + ... 'keyB': 'valueB', + ... 'keyC': 'valueC'}, + ... 'section3': {'foo': 'x', + ... 'bar': 'y', + ... 'baz': 'z'} + ... }) + >>> parser.sections() + ['section3', 'section2', 'section1'] + >>> [option for option in parser['section3']] + ['baz', 'foo', 'bar'] + + In these operations you need to use an ordered dictionary as well: + + .. highlightlang:: python + .. doctest:: + + >>> from collections import OrderedDict + >>> parser = configparser.RawConfigParser() + >>> parser.read_dict( + ... OrderedDict(( + ... ('s1', + ... OrderedDict(( + ... ('1', '2'), + ... ('3', '4'), + ... ('5', '6'), + ... )) + ... ), + ... ('s2', + ... OrderedDict(( + ... ('a', 'b'), + ... ('c', 'd'), + ... ('e', 'f'), + ... )) + ... ), + ... )) + ... ) + >>> parser.sections() + ['s1', 's2'] + >>> [option for option in parser['s1']] + ['1', '3', '5'] + >>> [option for option in parser['s2'].values()] + ['b', 'd', 'f'] + +* *allow_no_value*, default value: ``False`` + + Some configuration files are known to include settings without values, but + which otherwise conform to the syntax supported by :mod:`configparser`. The + *allow_no_value* parameter to the :meth:`__init__` method can be used to + indicate that such values should be accepted: + + .. highlightlang:: python + .. doctest:: + + >>> import configparser + + >>> sample_config = """ + ... [mysqld] + ... user = mysql + ... pid-file = /var/run/mysqld/mysqld.pid + ... skip-external-locking + ... old_passwords = 1 + ... skip-bdb + ... skip-innodb # we don't need ACID today + ... """ + >>> config = configparser.RawConfigParser(allow_no_value=True) + >>> config.read_string(sample_config) + + >>> # Settings with values are treated as before: + >>> config["mysqld"]["user"] + 'mysql' + + >>> # Settings without values provide None: + >>> config["mysqld"]["skip-bdb"] + + >>> # Settings which aren't specified still raise an error: + >>> config["mysqld"]["does-not-exist"] + Traceback (most recent call last): + ... + KeyError: 'does-not-exist' - .. versionchanged:: 3.2 - *allow_no_value*, *delimiters*, *comment_prefixes*, - *strict* and *empty_lines_in_values* were added. +* *delimiters*, default value: ``('=', ':')`` + Delimiters are substrings that delimit keys from values within a section. The + first occurence of a delimiting substring on a line is considered a delimiter. + This means values (but not keus) can contain substrings that are in the + *delimiters*. + + See also the *space_around_delimiters* argument to + :meth:`RawConfigParser.write`. + +* *comment_prefixes*, default value: ``_COMPATIBLE`` (``'#'`` valid on empty + lines, ``';'`` valid also on non-empty lines) + + Comment prefixes are substrings that indicate the start of a valid comment + within a config file. The peculiar default value allows for comments starting + with ``'#'`` or ``';'`` but only the latter can be used in a non-empty line. + This is obviously dictated by backwards compatibiliy. A more predictable + approach would be to specify prefixes as ``('#', ';')`` which will allow for + both prefixes to be used in non-empty lines. + + Please note that config parsers don't support escaping of comment prefixes so + leaving characters out of *comment_prefixes* is a way of ensuring they can be + used as parts of keys or values. + +* *strict*, default value: ``False`` + + If set to ``True``, the parser will not allow for any section or option + duplicates while reading from a single source (using :meth:`read_file`, + :meth:`read_string` or :meth:`read_dict`). The default is ``False`` only + because of backwards compatibility reasons. It's recommended to use strict + parsers in new applications. + +* *empty_lines_in_values*, default value: ``True`` + + .. highlightlang:: none + + In config parsers, values can be multiline as long as they're indented deeper + than the key that holds them. By default parsers also let empty lines to be + parts of values. At the same time, keys can be arbitrarily indented themselves + to improve readability. In consequence, when configuration files get big and + complex, it's easy for the user to lose track of the file structure. Take for + instance:: + + [Section] + key = multiline + value with a gotcha + + this = is still a part of the multiline value of 'key' + + + This can be especially problematic for the user to see if she's using + a proportional font to edit the file. That's why when your application does + not need values with empty lines, you should consider disallowing them. This + will make empty lines split keys every time. In the example above, it would + produce two keys, ``key`` and ``this``. + +.. highlightlang:: python + +More advanced customization may be achieved by overriding default values of the +following parser members: + +* `RawConfigParser.BOOLEAN_STATES` + + By default when using :meth:`getboolean`, config parsers consider the + following values ``True``: ``'1'``, ``'yes'``, ``'true'``, ``'on'`` and the + following values ``False``: ``'0'``, ``'no'``, ``'false'``, ``'off'``. You can + override this by specifying a custom dictionary of strings and their boolean + outcomes. For example: + + .. highlightlang:: python + .. doctest:: + + >>> custom = configparser.RawConfigParser() + >>> custom['section1'] = {'funky': 'nope'} + >>> custom['section1'].getboolean('funky') + Traceback (most recent call last): + ... + ValueError: Not a boolean: nope + >>> custom.BOOLEAN_STATES = {'sure': True, 'nope': False} + >>> custom['section1'].getboolean('funky') + False + + Other typical boolean pairs include ``accept``/``reject`` or + ``enabled``/``disabled``. + +* :meth:`RawConfigParser.optionxform` + + This is a method that transforms option names on every read or set operation. + By default it converts the name to lowercase. This also means that when + a configuration file gets written, all keys will be lowercase. If you find + that behaviour unsuitable, you can override this method. For example: + + .. highlightlang:: python + .. doctest:: + + >>> config = """ + ... [Section1] + ... Key = Value + ... + ... [Section2] + ... AnotherKey = Value + ... """ + >>> typical = configparser.RawConfigParser() + >>> typical.read_string(config) + >>> list(typical['Section1'].keys()) + ['key'] + >>> list(typical['Section2'].keys()) + ['anotherkey'] + >>> custom = configparser.RawConfigParser() + >>> custom.optionxform = lambda option: option + >>> custom.read_string(config) + >>> list(custom['Section1'].keys()) + ['Key'] + >>> list(custom['Section2'].keys()) + ['AnotherKey'] + +Legacy API Examples +------------------- + +Mainly because of backwards compatibility concerns, :mod:`configparser` +provides also a legacy API with explicit ``get``/``set`` methods. While there +are valid use cases for the methods outlined below, mapping protocol access +is preferred for new projects. The legacy API is at times more advanced, +low-level and downright counterintuitive. -.. exception:: Error +An example of writing to a configuration file:: - Base class for all other configparser exceptions. + import configparser + config = configparser.RawConfigParser() -.. exception:: NoSectionError + # Please note that using RawConfigParser's and the raw mode of + # ConfigParser's respective set functions, you can assign non-string values + # to keys internally, but will receive an error when attempting to write to + # a file or when you get it in non-raw mode. Setting values using the + # mapping protocol or SafeConfigParser's set() does not allow such + # assignments to take place. + config.add_section('Section1') + config.set('Section1', 'int', '15') + config.set('Section1', 'bool', 'true') + config.set('Section1', 'float', '3.1415') + config.set('Section1', 'baz', 'fun') + config.set('Section1', 'bar', 'Python') + config.set('Section1', 'foo', '%(bar)s is %(baz)s!') - Exception raised when a specified section is not found. + # Writing our configuration file to 'example.cfg' + with open('example.cfg', 'w') as configfile: + config.write(configfile) +An example of reading the configuration file again:: -.. exception:: DuplicateSectionError + import configparser - Exception raised if :meth:`add_section` is called with the name of a section - that is already present or in strict parsers when a section if found more - than once in a single input file, string or dictionary. + config = configparser.RawConfigParser() + config.read('example.cfg') - .. versionadded:: 3.2 - Optional ``source`` and ``lineno`` attributes and arguments to - :meth:`__init__` were added. + # getfloat() raises an exception if the value is not a float + # getint() and getboolean() also do this for their respective types + float = config.getfloat('Section1', 'float') + int = config.getint('Section1', 'int') + print(float + int) + # Notice that the next output does not interpolate '%(bar)s' or '%(baz)s'. + # This is because we are using a RawConfigParser(). + if config.getboolean('Section1', 'bool'): + print(config.get('Section1', 'foo')) -.. exception:: DuplicateOptionError +To get interpolation, you will need to use a :class:`SafeConfigParser` or, if +you absolutely have to, a :class:`ConfigParser`:: - 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. + import configparser + cfg = configparser.SafeConfigParser() + cfg.read('example.cfg') -.. exception:: NoOptionError + # Set the optional `raw` argument of get() to True if you wish to disable + # interpolation in a single get operation. + print(cfg.get('Section1', 'foo', raw=False)) # -> "Python is fun!" + print(cfg.get('Section1', 'foo', raw=True)) # -> "%(bar)s is %(baz)s!" - Exception raised when a specified option is not found in the specified - section. + # The optional `vars` argument is a dict with members that will take + # precedence in interpolation. + print(cfg.get('Section1', 'foo', vars={'bar': 'Documentation', + 'baz': 'evil'})) + # The optional `fallback` argument can be used to provide a fallback value + print(cfg.get('Section1', 'foo')) + # -> "Python is fun!" -.. exception:: InterpolationError + print(cfg.get('Section1', 'foo', fallback='Monty is not.')) + # -> "Python is fun!" - Base class for exceptions raised when problems occur performing string - interpolation. + print(cfg.get('Section1', 'monster', fallback='No such things as monsters.')) + # -> "No such things as monsters." + # A bare print(cfg.get('Section1', 'monster')) would raise NoOptionError + # but we can also use: -.. exception:: InterpolationDepthError + print(cfg.get('Section1', 'monster', fallback=None)) + # -> None - Exception raised when string interpolation cannot be completed because the - number of iterations exceeds :const:`MAX_INTERPOLATION_DEPTH`. Subclass of - :exc:`InterpolationError`. +Defaults are available in all three types of ConfigParsers. They are used in +interpolation if an option used is not defined elsewhere. :: -.. exception:: InterpolationMissingOptionError + import configparser - Exception raised when an option referenced from a value does not exist. Subclass - of :exc:`InterpolationError`. + # New instance with 'bar' and 'baz' defaulting to 'Life' and 'hard' each + config = configparser.SafeConfigParser({'bar': 'Life', 'baz': 'hard'}) + config.read('example.cfg') + print(config.get('Section1', 'foo')) # -> "Python is fun!" + config.remove_option('Section1', 'bar') + config.remove_option('Section1', 'baz') + print(config.get('Section1', 'foo')) # -> "Life is hard!" -.. exception:: InterpolationSyntaxError +.. _rawconfigparser-objects: - Exception raised when the source text into which substitutions are made does not - conform to the required syntax. Subclass of :exc:`InterpolationError`. +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) -.. exception:: MissingSectionHeaderError + The basic configuration object. 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. - Exception raised when attempting to parse a file which has no section headers. + When *delimiters* is given, it will be used as the set of substrings that + divide keys from values. When *comment_prefixes* is given, it will be used + as the set of substrings that prefix comments in a line, both for the whole + line and inline comments. For backwards compatibility, the default value for + *comment_prefixes* is a special value that indicates that ``;`` and ``#`` can + start whole line comments while only ``;`` can start inline comments. + 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``. -.. exception:: ParsingError + This class does not support the magical interpolation behavior. - Exception raised when errors occur attempting to parse a file. + .. versionchanged:: 3.1 + The default *dict_type* is :class:`collections.OrderedDict`. .. versionchanged:: 3.2 - The ``filename`` attribute and :meth:`__init__` argument were renamed to - ``source`` for consistency. - -.. 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. - - -.. seealso:: - - Module :mod:`shlex` - Support for a creating Unix shell-like mini-languages which can be used as an - alternate format for application configuration files. - - -.. _rawconfigparser-objects: - -RawConfigParser Objects ------------------------ - -:class:`RawConfigParser` instances have the following methods: + *allow_no_value*, *delimiters*, *comment_prefixes*, *strict* and + *empty_lines_in_values* were added. .. method:: RawConfigParser.defaults() @@ -373,29 +817,34 @@ RawConfigParser Objects .. versionadded:: 3.2 -.. method:: RawConfigParser.get(section, option, [vars, default]) +.. method:: RawConfigParser.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 - *default* is provided, it is used as a fallback value. ``None`` can be - provided as a *default* value. + *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:: RawConfigParser.getint(section, option, [vars, default]) +.. method:: RawConfigParser.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 *default*. + an integer. See :meth:`get` for explanation of *vars* and *fallback*. -.. method:: RawConfigParser.getfloat(section, option, [vars, default]) +.. method:: RawConfigParser.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 - *default*. + *fallback*. -.. method:: RawConfigParser.getboolean(section, option, [vars, default]) +.. method:: RawConfigParser.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 @@ -403,30 +852,39 @@ RawConfigParser Objects 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 *default*. + :exc:`ValueError`. See :meth:`get` for explanation of *vars* and *fallback*. .. method:: RawConfigParser.items(section) - Return a list of ``(name, value)`` pairs for each option in the given *section*. + Return a list of ``(name, value)`` pairs for each option in the given + *section*. .. method:: RawConfigParser.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. + :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. + +.. warning:: + + 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:: RawConfigParser.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`` (the default), delimiters between keys and values are surrounded - by spaces. + 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`` (the default), delimiters between + keys and values are surrounded by spaces. .. method:: RawConfigParser.remove_option(section, option) @@ -474,39 +932,72 @@ RawConfigParser 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. Whenever you -can, consider using :class:`SafeConfigParser` which adds validation and escaping -for the interpolation. +: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) -.. method:: ConfigParser.get(section, option, raw=False, [vars, default]) + 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. Note that *__name__* is an intrinsic default; its value is + the section name, and will override any value provided in *defaults*. + + 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:: ConfigParser.get(section, option, raw=False, [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 - *default* is provided, it is used as a fallback value. ``None`` can be - provided as a *default* value. + *fallback* is provided, it is used as a fallback value. ``None`` can be + provided as a *fallback* value. All the ``'%'`` interpolations are expanded in the return values, unless the *raw* argument is true. Values for interpolation keys are looked up in the same manner as the option. + .. versionchanged:: 3.2 + Arguments *raw*, *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:: ConfigParser.getint(section, option, raw=False, [vars, default]) +.. method:: ConfigParser.getint(section, option, raw=False, [vars, fallback]) A convenience method which coerces the *option* in the specified *section* to - an integer. See :meth:`get` for explanation of *raw*, *vars* and *default*. + an integer. See :meth:`get` for explanation of *raw*, *vars* and *fallback*. -.. method:: ConfigParser.getfloat(section, option, raw=False, [vars, default]) +.. method:: ConfigParser.getfloat(section, option, raw=False, [vars, fallback]) A convenience method which coerces the *option* in the specified *section* to a floating point number. See :meth:`get` for explanation of *raw*, *vars* - and *default*. + and *fallback*. -.. method:: ConfigParser.getboolean(section, option, raw=False, [vars, default]) +.. method:: ConfigParser.getboolean(section, option, raw=False, [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 @@ -515,7 +1006,7 @@ for the interpolation. 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 *raw*, *vars* and - *default*. + *fallback*. .. method:: ConfigParser.items(section, raw=False, vars=None) @@ -525,15 +1016,39 @@ for the interpolation. 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 sane 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 (e.g. 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:: SafeConfigParser.set(section, option, value) If the given section exists, set the given option to the specified value; @@ -541,126 +1056,82 @@ The :class:`SafeConfigParser` class implements the same extended interface as not, :exc:`TypeError` is raised. -Examples --------- +Exceptions +---------- -An example of writing to a configuration file:: +.. exception:: Error - import configparser + Base class for all other configparser exceptions. - config = configparser.RawConfigParser() - # When adding sections or items, add them in the reverse order of - # how you want them to be displayed in the actual file. - # In addition, please note that using RawConfigParser's and the raw - # mode of ConfigParser's respective set functions, you can assign - # non-string values to keys internally, but will receive an error - # when attempting to write to a file or when you get it in non-raw - # mode. SafeConfigParser does not allow such assignments to take place. - config.add_section('Section1') - config.set('Section1', 'int', '15') - config.set('Section1', 'bool', 'true') - config.set('Section1', 'float', '3.1415') - config.set('Section1', 'baz', 'fun') - config.set('Section1', 'bar', 'Python') - config.set('Section1', 'foo', '%(bar)s is %(baz)s!') +.. exception:: NoSectionError - # Writing our configuration file to 'example.cfg' - with open('example.cfg', 'w') as configfile: - config.write(configfile) + Exception raised when a specified section is not found. -An example of reading the configuration file again:: - import configparser +.. exception:: DuplicateSectionError - config = configparser.RawConfigParser() - config.read('example.cfg') + Exception raised if :meth:`add_section` is called with the name of a section + that is already present or in strict parsers when a section if found more + than once in a single input file, string or dictionary. - # getfloat() raises an exception if the value is not a float - # getint() and getboolean() also do this for their respective types - float = config.getfloat('Section1', 'float') - int = config.getint('Section1', 'int') - print(float + int) + .. versionadded:: 3.2 + Optional ``source`` and ``lineno`` attributes and arguments to + :meth:`__init__` were added. - # Notice that the next output does not interpolate '%(bar)s' or '%(baz)s'. - # This is because we are using a RawConfigParser(). - if config.getboolean('Section1', 'bool'): - print(config.get('Section1', 'foo')) -To get interpolation, you will need to use a :class:`ConfigParser` or -:class:`SafeConfigParser`:: +.. exception:: DuplicateOptionError - import configparser + 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. - config = configparser.ConfigParser() - config.read('example.cfg') - # Set the third, optional argument of get to 1 if you wish to use raw mode. - print(config.get('Section1', 'foo', 0)) # -> "Python is fun!" - print(config.get('Section1', 'foo', 1)) # -> "%(bar)s is %(baz)s!" +.. exception:: NoOptionError - # The optional fourth argument is a dict with members that will take - # precedence in interpolation. - print(config.get('Section1', 'foo', 0, {'bar': 'Documentation', - 'baz': 'evil'})) + Exception raised when a specified option is not found in the specified + section. -Defaults are available in all three types of ConfigParsers. They are used in -interpolation if an option used is not defined elsewhere. :: - import configparser +.. exception:: InterpolationError - # New instance with 'bar' and 'baz' defaulting to 'Life' and 'hard' each - config = configparser.SafeConfigParser({'bar': 'Life', 'baz': 'hard'}) - config.read('example.cfg') + Base class for exceptions raised when problems occur performing string + interpolation. - print(config.get('Section1', 'foo')) # -> "Python is fun!" - config.remove_option('Section1', 'bar') - config.remove_option('Section1', 'baz') - print(config.get('Section1', 'foo')) # -> "Life is hard!" -The function ``opt_move`` below can be used to move options between sections:: +.. exception:: InterpolationDepthError - def opt_move(config, section1, section2, option): - try: - config.set(section2, option, config.get(section1, option, 1)) - except configparser.NoSectionError: - # Create non-existent section - config.add_section(section2) - opt_move(config, section1, section2, option) - else: - config.remove_option(section1, option) + Exception raised when string interpolation cannot be completed because the + number of iterations exceeds :const:`MAX_INTERPOLATION_DEPTH`. Subclass of + :exc:`InterpolationError`. -Some configuration files are known to include settings without values, but which -otherwise conform to the syntax supported by :mod:`configparser`. The -*allow_no_value* parameter to the :meth:`__init__` method can be used to -indicate that such values should be accepted: -.. doctest:: +.. exception:: InterpolationMissingOptionError + + Exception raised when an option referenced from a value does not exist. Subclass + of :exc:`InterpolationError`. + + +.. exception:: InterpolationSyntaxError + + Exception raised when the source text into which substitutions are made does not + conform to the required syntax. Subclass of :exc:`InterpolationError`. + + +.. exception:: MissingSectionHeaderError + + Exception raised when attempting to parse a file which has no section headers. + + +.. exception:: ParsingError + + 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. - >>> import configparser - >>> import io - - >>> sample_config = """ - ... [mysqld] - ... user = mysql - ... pid-file = /var/run/mysqld/mysqld.pid - ... skip-external-locking - ... old_passwords = 1 - ... skip-bdb - ... skip-innodb # we don't need ACID today - ... """ - >>> config = configparser.RawConfigParser(allow_no_value=True) - >>> config.read_file(io.BytesIO(sample_config)) - - >>> # Settings with values are treated as before: - >>> config.get("mysqld", "user") - 'mysql' - - >>> # Settings without values provide None: - >>> config.get("mysqld", "skip-bdb") - - >>> # Settings which aren't specified still raise an error: - >>> config.get("mysqld", "does-not-exist") - Traceback (most recent call last): - ... - configparser.NoOptionError: No option 'does-not-exist' in section: 'mysqld' +.. [customizable] Config parsers allow for very heavy customization. If you're + interested in changing the behaviour outlined by the footnote + reference, consult the `Customizing Parser Behaviour`_ section. diff --git a/Lib/configparser.py b/Lib/configparser.py index 03d6713..80f6aed 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -85,7 +85,7 @@ ConfigParser -- responsible for parsing a list of and their keys will be added in order. Values are automatically converted to strings. - get(section, option, raw=False, vars=None, default=_UNSET) + get(section, option, raw=False, vars=None, fallback=_UNSET) Return a string value for the named option. All % interpolations are expanded in the return values, based on the defaults passed into the constructor and the DEFAULT section. Additional substitutions may be @@ -93,13 +93,13 @@ ConfigParser -- responsible for parsing a list of contents override any pre-existing defaults. If `option' is a key in `vars', the value from `vars' is used. - getint(section, options, raw=False, vars=None, default=_UNSET) + getint(section, options, raw=False, vars=None, fallback=_UNSET) Like get(), but convert value to an integer. - getfloat(section, options, raw=False, vars=None, default=_UNSET) + getfloat(section, options, raw=False, vars=None, fallback=_UNSET) Like get(), but convert value to a float. - getboolean(section, options, raw=False, vars=None, default=_UNSET) + getboolean(section, options, raw=False, vars=None, fallback=_UNSET) Like get(), but convert value to a boolean (currently case insensitively defined as 0, false, no, off for False, and 1, true, yes, on for True). Returns False or True. @@ -123,13 +123,10 @@ ConfigParser -- responsible for parsing a list of between keys and values are surrounded by spaces. """ -try: - from collections import OrderedDict as _default_dict -except ImportError: - # fallback for setup.py which hasn't yet built _collections - _default_dict = dict - +from collections import MutableMapping, OrderedDict as _default_dict +import functools import io +import itertools import re import sys import warnings @@ -366,7 +363,7 @@ _COMPATIBLE = object() _UNSET = object() -class RawConfigParser: +class RawConfigParser(MutableMapping): """ConfigParser that does not do interpolation.""" # Regular expressions for parsing section headers and options @@ -413,6 +410,8 @@ class RawConfigParser: self._dict = dict_type self._sections = self._dict() self._defaults = self._dict() + self._views = self._dict() + self._views[DEFAULTSECT] = SectionProxy(self, DEFAULTSECT) if defaults: for key, value in defaults.items(): self._defaults[self.optionxform(key)] = value @@ -434,6 +433,7 @@ class RawConfigParser: self._startonly_comment_prefixes = () self._comment_prefixes = tuple(comment_prefixes or ()) self._strict = strict + self._allow_no_value = allow_no_value self._empty_lines_in_values = empty_lines_in_values def defaults(self): @@ -451,12 +451,13 @@ class RawConfigParser: already exists. Raise ValueError if name is DEFAULT or any of it's case-insensitive variants. """ - if section.lower() == "default": + if section.upper() == DEFAULTSECT: raise ValueError('Invalid section name: %s' % section) if section in self._sections: raise DuplicateSectionError(section) self._sections[section] = self._dict() + self._views[section] = SectionProxy(self, section) def has_section(self, section): """Indicate whether the named section is present in the configuration. @@ -534,7 +535,7 @@ class RawConfigParser: for section, keys in dictionary.items(): try: self.add_section(section) - except DuplicateSectionError: + except (DuplicateSectionError, ValueError): if self._strict and section in elements_added: raise elements_added.add(section) @@ -556,29 +557,31 @@ class RawConfigParser: ) self.read_file(fp, source=filename) - def get(self, section, option, vars=None, default=_UNSET): + def get(self, section, option, *, 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 in `vars' (if provided), `section', and in `DEFAULTSECT' in that order. - If the key is not found and `default' is provided, it is used as - a fallback value. `None' can be provided as a `default' value. + 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. """ try: d = self._unify_values(section, vars) except NoSectionError: - if default is _UNSET: + if fallback is _UNSET: raise else: - return default + return fallback option = self.optionxform(option) try: return d[option] except KeyError: - if default is _UNSET: + if fallback is _UNSET: raise NoOptionError(option, section) else: - return default + return fallback def items(self, section): try: @@ -593,35 +596,36 @@ class RawConfigParser: del d["__name__"] return d.items() - def _get(self, section, conv, option, *args, **kwargs): - return conv(self.get(section, option, *args, **kwargs)) + def _get(self, section, conv, option, **kwargs): + return conv(self.get(section, option, **kwargs)) - def getint(self, section, option, vars=None, default=_UNSET): + def getint(self, section, option, *, vars=None, fallback=_UNSET): try: - return self._get(section, int, option, vars) + return self._get(section, int, option, vars=vars) except (NoSectionError, NoOptionError): - if default is _UNSET: + if fallback is _UNSET: raise else: - return default + return fallback - def getfloat(self, section, option, vars=None, default=_UNSET): + def getfloat(self, section, option, *, vars=None, fallback=_UNSET): try: - return self._get(section, float, option, vars) + return self._get(section, float, option, vars=vars) except (NoSectionError, NoOptionError): - if default is _UNSET: + if fallback is _UNSET: raise else: - return default + return fallback - def getboolean(self, section, option, vars=None, default=_UNSET): + def getboolean(self, section, option, *, vars=None, fallback=_UNSET): try: - return self._get(section, self._convert_to_boolean, option, vars) + return self._get(section, self._convert_to_boolean, option, + vars=vars) except (NoSectionError, NoOptionError): - if default is _UNSET: + if fallback is _UNSET: raise else: - return default + return fallback def optionxform(self, optionstr): return optionstr.lower() @@ -671,7 +675,7 @@ class RawConfigParser: for key, value in section_items: if key == "__name__": continue - if (value is not None) or (self._optcre == self.OPTCRE): + if value is not None or not self._allow_no_value: value = delimiter + str(value).replace('\n', '\n\t') else: value = "" @@ -698,8 +702,40 @@ class RawConfigParser: existed = section in self._sections if existed: del self._sections[section] + del self._views[section] return existed + def __getitem__(self, key): + if key != DEFAULTSECT and not self.has_section(key): + raise KeyError(key) + return self._views[key] + + def __setitem__(self, key, value): + # To conform with the mapping protocol, overwrites existing values in + # the section. + + # XXX this is not atomic if read_dict fails at any point. Then again, + # no update method in configparser is atomic in this implementation. + self.remove_section(key) + self.read_dict({key: value}) + + def __delitem__(self, key): + if key == DEFAULTSECT: + 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 == DEFAULTSECT 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((DEFAULTSECT,), self._sections.keys()) + def _read(self, fp, fpname): """Parse a sectioned configuration file. @@ -776,6 +812,7 @@ class RawConfigParser: cursect = self._dict() cursect['__name__'] = sectname self._sections[sectname] = cursect + self._views[sectname] = SectionProxy(self, sectname) elements_added.add(sectname) # So sections can't start with a continuation line optname = None @@ -818,8 +855,8 @@ class RawConfigParser: self._join_multiline_values() def _join_multiline_values(self): - all_sections = [self._defaults] - all_sections.extend(self._sections.values()) + all_sections = itertools.chain((self._defaults,), + self._sections.values()) for options in all_sections: for name, val in options.items(): if isinstance(val, list): @@ -857,73 +894,95 @@ class RawConfigParser: raise ValueError('Not a boolean: %s' % value) return self.BOOLEAN_STATES[value.lower()] + def _validate_value_type(self, value): + """Raises a TypeError for non-string values. + + The only legal non-string value if we allow valueless + options is None, so we need to check if the value is a + string if: + - we do not allow valueless options, or + - we allow valueless options but the value is not None + + For compatibility reasons this method is not used in classic set() + for RawConfigParsers and ConfigParsers. It is invoked in every + case for mapping protocol access and in SafeConfigParser.set(). + """ + if not self._allow_no_value or value: + if not isinstance(value, str): + raise TypeError("option values must be strings") + + class ConfigParser(RawConfigParser): """ConfigParser implementing interpolation.""" - def get(self, section, option, raw=False, vars=None, default=_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 in `vars' (if provided), `section', and in `DEFAULTSECT' in that order. - If the key is not found and `default' is provided, it is used as - a fallback value. `None' can be provided as a `default' value. + 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 default is _UNSET: + if fallback is _UNSET: raise else: - return default + return fallback option = self.optionxform(option) try: value = d[option] except KeyError: - if default is _UNSET: + if fallback is _UNSET: raise NoOptionError(option, section) else: - return default + 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, default=_UNSET): + def getint(self, section, option, *, raw=False, vars=None, + fallback=_UNSET): try: - return self._get(section, int, option, raw, vars) + return self._get(section, int, option, raw=raw, vars=vars) except (NoSectionError, NoOptionError): - if default is _UNSET: + if fallback is _UNSET: raise else: - return default + return fallback - def getfloat(self, section, option, raw=False, vars=None, default=_UNSET): + def getfloat(self, section, option, *, raw=False, vars=None, + fallback=_UNSET): try: - return self._get(section, float, option, raw, vars) + return self._get(section, float, option, raw=raw, vars=vars) except (NoSectionError, NoOptionError): - if default is _UNSET: + if fallback is _UNSET: raise else: - return default + return fallback - def getboolean(self, section, option, raw=False, vars=None, - default=_UNSET): + def getboolean(self, section, option, *, raw=False, vars=None, + fallback=_UNSET): try: - return self._get(section, self._convert_to_boolean, option, raw, - vars) + return self._get(section, self._convert_to_boolean, option, + raw=raw, vars=vars) except (NoSectionError, NoOptionError): - if default is _UNSET: + if fallback is _UNSET: raise else: - return default + return fallback def items(self, section, raw=False, vars=None): """Return a list of (name, value) tuples for each option in a section. @@ -1037,14 +1096,7 @@ class SafeConfigParser(ConfigParser): def set(self, section, option, value=None): """Set an option. Extend ConfigParser.set: check for string values.""" - # The only legal non-string value if we allow valueless - # options is None, so we need to check if the value is a - # string if: - # - we do not allow valueless options, or - # - we allow valueless options but the value is not None - if self._optcre is self.OPTCRE or value: - if not isinstance(value, str): - raise TypeError("option values must be strings") + self._validate_value_type(value) # check for bad percent signs if value: tmp_value = value.replace('%%', '') # escaped percent signs @@ -1053,3 +1105,60 @@ class SafeConfigParser(ConfigParser): raise ValueError("invalid interpolation syntax in %r at " "position %d" % (value, tmp_value.find('%'))) ConfigParser.set(self, section, option, value) + + +class SectionProxy(MutableMapping): + """A proxy for a single section from a parser.""" + + _noname = ("__name__ special key access and modification " + "not supported through the mapping interface.") + + def __init__(self, parser, section_name): + """Creates a view on a section named `section_name` in `parser`.""" + self._parser = parser + self._section = section_name + self.getint = functools.partial(self._parser.getint, + self._section) + self.getfloat = functools.partial(self._parser.getfloat, + self._section) + self.getboolean = functools.partial(self._parser.getboolean, + self._section) + + def __repr__(self): + return '<Section: {}>'.format(self._section) + + def __getitem__(self, key): + if key == '__name__': + raise ValueError(self._noname) + if not self._parser.has_option(self._section, key): + raise KeyError(key) + return self._parser.get(self._section, key) + + def __setitem__(self, key, value): + if key == '__name__': + raise ValueError(self._noname) + self._parser._validate_value_type(value) + return self._parser.set(self._section, key, value) + + def __delitem__(self, key): + if key == '__name__': + raise ValueError(self._noname) + if not self._parser.has_option(self._section, key): + raise KeyError(key) + return self._parser.remove_option(self._section, key) + + def __contains__(self, key): + if key == '__name__': + return False + return self._parser.has_option(self._section, key) + + def __len__(self): + # __name__ is properly hidden by .options() + # XXX weak performance + return len(self._parser.options(self._section)) + + def __iter__(self): + # __name__ is properly hidden by .options() + # XXX weak performance + # XXX does not break when underlying container state changed + return self._parser.options(self._section).__iter__() diff --git a/Lib/logging/config.py b/Lib/logging/config.py index 12c1c13..5afdf9f 100644 --- a/Lib/logging/config.py +++ b/Lib/logging/config.py @@ -103,7 +103,7 @@ def _encoded(s): def _create_formatters(cp): """Create and return formatters""" - flist = cp.get("formatters", "keys") + flist = cp["formatters"]["keys"] if not len(flist): return {} flist = flist.split(",") @@ -111,20 +111,12 @@ def _create_formatters(cp): formatters = {} for form in flist: sectname = "formatter_%s" % form - opts = cp.options(sectname) - if "format" in opts: - fs = cp.get(sectname, "format", 1) - else: - fs = None - if "datefmt" in opts: - dfs = cp.get(sectname, "datefmt", 1) - else: - dfs = None + fs = cp.get(sectname, "format", raw=True, fallback=None) + dfs = cp.get(sectname, "datefmt", raw=True, fallback=None) c = logging.Formatter - if "class" in opts: - class_name = cp.get(sectname, "class") - if class_name: - c = _resolve(class_name) + class_name = cp[sectname].get("class") + if class_name: + c = _resolve(class_name) f = c(fs, dfs) formatters[form] = f return formatters @@ -132,7 +124,7 @@ def _create_formatters(cp): def _install_handlers(cp, formatters): """Install and return handlers""" - hlist = cp.get("handlers", "keys") + hlist = cp["handlers"]["keys"] if not len(hlist): return {} hlist = hlist.split(",") @@ -140,30 +132,23 @@ def _install_handlers(cp, formatters): handlers = {} fixups = [] #for inter-handler references for hand in hlist: - sectname = "handler_%s" % hand - klass = cp.get(sectname, "class") - opts = cp.options(sectname) - if "formatter" in opts: - fmt = cp.get(sectname, "formatter") - else: - fmt = "" + section = cp["handler_%s" % hand] + klass = section["class"] + fmt = section.get("formatter", "") try: klass = eval(klass, vars(logging)) except (AttributeError, NameError): klass = _resolve(klass) - args = cp.get(sectname, "args") + args = section["args"] args = eval(args, vars(logging)) h = klass(*args) - if "level" in opts: - level = cp.get(sectname, "level") + if "level" in section: + level = section["level"] h.setLevel(logging._levelNames[level]) if len(fmt): h.setFormatter(formatters[fmt]) if issubclass(klass, logging.handlers.MemoryHandler): - if "target" in opts: - target = cp.get(sectname,"target") - else: - target = "" + target = section.get("target", "") if len(target): #the target handler may not be loaded yet, so keep for later... fixups.append((h, target)) handlers[hand] = h @@ -197,20 +182,19 @@ def _install_loggers(cp, handlers, disable_existing): """Create and install loggers""" # configure the root first - llist = cp.get("loggers", "keys") + llist = cp["loggers"]["keys"] llist = llist.split(",") llist = list(map(lambda x: x.strip(), llist)) llist.remove("root") - sectname = "logger_root" + section = cp["logger_root"] root = logging.root log = root - opts = cp.options(sectname) - if "level" in opts: - level = cp.get(sectname, "level") + if "level" in section: + level = section["level"] log.setLevel(logging._levelNames[level]) for h in root.handlers[:]: root.removeHandler(h) - hlist = cp.get(sectname, "handlers") + hlist = section["handlers"] if len(hlist): hlist = hlist.split(",") hlist = _strip_spaces(hlist) @@ -237,13 +221,9 @@ def _install_loggers(cp, handlers, disable_existing): child_loggers = [] #now set up the new ones... for log in llist: - sectname = "logger_%s" % log - qn = cp.get(sectname, "qualname") - opts = cp.options(sectname) - if "propagate" in opts: - propagate = cp.getint(sectname, "propagate") - else: - propagate = 1 + section = cp["logger_%s" % log] + qn = section["qualname"] + propagate = section.getint("propagate", fallback=1) logger = logging.getLogger(qn) if qn in existing: i = existing.index(qn) @@ -255,14 +235,14 @@ def _install_loggers(cp, handlers, disable_existing): child_loggers.append(existing[i]) i = i + 1 existing.remove(qn) - if "level" in opts: - level = cp.get(sectname, "level") + if "level" in section: + level = section["level"] logger.setLevel(logging._levelNames[level]) for h in logger.handlers[:]: logger.removeHandler(h) logger.propagate = propagate logger.disabled = 0 - hlist = cp.get(sectname, "handlers") + hlist = section["handlers"] if len(hlist): hlist = hlist.split(",") hlist = _strip_spaces(hlist) diff --git a/Lib/test/test_cfgparser.py b/Lib/test/test_cfgparser.py index f7e240f..11aa267 100644 --- a/Lib/test/test_cfgparser.py +++ b/Lib/test/test_cfgparser.py @@ -52,8 +52,6 @@ class CfgParserTestCaseClass(unittest.TestCase): class BasicTestCase(CfgParserTestCaseClass): def basic_test(self, cf): - L = cf.sections() - L.sort() E = ['Commented Bar', 'Foo Bar', 'Internationalized Stuff', @@ -64,20 +62,34 @@ class BasicTestCase(CfgParserTestCaseClass): 'Spacey Bar From The Beginning', 'Types', ] + if self.allow_no_value: E.append('NoValue') E.sort() + + # API access + L = cf.sections() + L.sort() eq = self.assertEqual eq(L, E) + # mapping access + L = [section for section in cf] + L.sort() + E.append(configparser.DEFAULTSECT) + E.sort() + eq(L, E) + # The use of spaces in the section names serves as a # regression test for SourceForge bug #583248: # http://www.python.org/sf/583248 - eq(cf.get('Foo Bar', 'foo'), 'bar') - eq(cf.get('Spacey Bar', 'foo'), 'bar') - eq(cf.get('Spacey Bar From The Beginning', 'foo'), 'bar') + + # API access + eq(cf.get('Foo Bar', 'foo'), 'bar1') + eq(cf.get('Spacey Bar', 'foo'), 'bar2') + eq(cf.get('Spacey Bar From The Beginning', 'foo'), 'bar3') eq(cf.get('Spacey Bar From The Beginning', 'baz'), 'qwe') - eq(cf.get('Commented Bar', 'foo'), 'bar') + eq(cf.get('Commented Bar', 'foo'), 'bar4') eq(cf.get('Commented Bar', 'baz'), 'qwe') eq(cf.get('Spaces', 'key with spaces'), 'value') eq(cf.get('Spaces', 'another with spaces'), 'splat!') @@ -89,40 +101,69 @@ class BasicTestCase(CfgParserTestCaseClass): if self.allow_no_value: eq(cf.get('NoValue', 'option-without-value'), None) - # test vars= and default= - eq(cf.get('Foo Bar', 'foo', default='baz'), 'bar') + # test vars= and fallback= + eq(cf.get('Foo Bar', 'foo', fallback='baz'), 'bar1') eq(cf.get('Foo Bar', 'foo', vars={'foo': 'baz'}), 'baz') with self.assertRaises(configparser.NoSectionError): cf.get('No Such Foo Bar', 'foo') with self.assertRaises(configparser.NoOptionError): cf.get('Foo Bar', 'no-such-foo') - eq(cf.get('No Such Foo Bar', 'foo', default='baz'), 'baz') - eq(cf.get('Foo Bar', 'no-such-foo', default='baz'), 'baz') - eq(cf.get('Spacey Bar', 'foo', default=None), 'bar') - eq(cf.get('No Such Spacey Bar', 'foo', default=None), None) - eq(cf.getint('Types', 'int', default=18), 42) - eq(cf.getint('Types', 'no-such-int', default=18), 18) - eq(cf.getint('Types', 'no-such-int', default="18"), "18") # sic! + eq(cf.get('No Such Foo Bar', 'foo', fallback='baz'), 'baz') + eq(cf.get('Foo Bar', 'no-such-foo', fallback='baz'), 'baz') + eq(cf.get('Spacey Bar', 'foo', fallback=None), 'bar2') + eq(cf.get('No Such Spacey Bar', 'foo', fallback=None), None) + eq(cf.getint('Types', 'int', fallback=18), 42) + eq(cf.getint('Types', 'no-such-int', fallback=18), 18) + eq(cf.getint('Types', 'no-such-int', fallback="18"), "18") # sic! self.assertAlmostEqual(cf.getfloat('Types', 'float', - default=0.0), 0.44) + fallback=0.0), 0.44) self.assertAlmostEqual(cf.getfloat('Types', 'no-such-float', - default=0.0), 0.0) - eq(cf.getfloat('Types', 'no-such-float', default="0.0"), "0.0") # sic! - eq(cf.getboolean('Types', 'boolean', default=True), False) - eq(cf.getboolean('Types', 'no-such-boolean', default="yes"), + fallback=0.0), 0.0) + eq(cf.getfloat('Types', 'no-such-float', fallback="0.0"), "0.0") # sic! + eq(cf.getboolean('Types', 'boolean', fallback=True), False) + eq(cf.getboolean('Types', 'no-such-boolean', fallback="yes"), "yes") # sic! - eq(cf.getboolean('Types', 'no-such-boolean', default=True), True) - eq(cf.getboolean('No Such Types', 'boolean', default=True), True) + eq(cf.getboolean('Types', 'no-such-boolean', fallback=True), True) + eq(cf.getboolean('No Such Types', 'boolean', fallback=True), True) if self.allow_no_value: - eq(cf.get('NoValue', 'option-without-value', default=False), None) + eq(cf.get('NoValue', 'option-without-value', fallback=False), None) eq(cf.get('NoValue', 'no-such-option-without-value', - default=False), False) + fallback=False), False) + + # mapping access + eq(cf['Foo Bar']['foo'], 'bar1') + eq(cf['Spacey Bar']['foo'], 'bar2') + eq(cf['Spacey Bar From The Beginning']['foo'], 'bar3') + eq(cf['Spacey Bar From The Beginning']['baz'], 'qwe') + eq(cf['Commented Bar']['foo'], 'bar4') + eq(cf['Commented Bar']['baz'], 'qwe') + eq(cf['Spaces']['key with spaces'], 'value') + eq(cf['Spaces']['another with spaces'], 'splat!') + eq(cf['Long Line']['foo'], + 'this line is much, much longer than my editor\nlikes it.') + if self.allow_no_value: + eq(cf['NoValue']['option-without-value'], None) + # API access self.assertNotIn('__name__', cf.options("Foo Bar"), '__name__ "option" should not be exposed by the API!') + # mapping access + self.assertNotIn('__name__', cf['Foo Bar'], + '__name__ "option" should not be exposed by ' + 'mapping protocol access') + self.assertFalse('__name__' in cf['Foo Bar']) + with self.assertRaises(ValueError): + cf['Foo Bar']['__name__'] + with self.assertRaises(ValueError): + del cf['Foo Bar']['__name__'] + with self.assertRaises(ValueError): + cf['Foo Bar']['__name__'] = "can't write to this special name" + # Make sure the right things happen for remove_option(); # added to include check for SourceForge bug #123324: + + # API acceess self.assertTrue(cf.remove_option('Foo Bar', 'foo'), "remove_option() failed to report existence of option") self.assertFalse(cf.has_option('Foo Bar', 'foo'), @@ -138,17 +179,25 @@ class BasicTestCase(CfgParserTestCaseClass): eq(cf.get('Long Line', 'foo'), 'this line is much, much longer than my editor\nlikes it.') + # mapping access + del cf['Spacey Bar']['foo'] + self.assertFalse('foo' in cf['Spacey Bar']) + with self.assertRaises(KeyError): + del cf['Spacey Bar']['foo'] + with self.assertRaises(KeyError): + del cf['No Such Section']['foo'] + def test_basic(self): config_string = """\ [Foo Bar] -foo{0[0]}bar +foo{0[0]}bar1 [Spacey Bar] -foo {0[0]} bar +foo {0[0]} bar2 [Spacey Bar From The Beginning] - foo {0[0]} bar + foo {0[0]} bar3 baz {0[0]} qwe [Commented Bar] -foo{0[1]} bar {1[1]} comment +foo{0[1]} bar4 {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 @@ -205,17 +254,17 @@ boolean {0[0]} NO def test_basic_from_dict(self): config = { "Foo Bar": { - "foo": "bar", + "foo": "bar1", }, "Spacey Bar": { - "foo": "bar", + "foo": "bar2", }, "Spacey Bar From The Beginning": { - "foo": "bar", + "foo": "bar3", "baz": "qwe", }, "Commented Bar": { - "foo": "bar", + "foo": "bar4", "baz": "qwe", }, "Long Line": { @@ -270,14 +319,18 @@ boolean {0[0]} NO cf = self.newconfig() cf.add_section("A") cf.add_section("a") + cf.add_section("B") L = cf.sections() L.sort() eq = self.assertEqual - eq(L, ["A", "a"]) + eq(L, ["A", "B", "a"]) cf.set("a", "B", "value") eq(cf.options("a"), ["b"]) eq(cf.get("a", "b"), "value", "could not locate option, expecting case-insensitive option names") + with self.assertRaises(configparser.NoSectionError): + # section names are case-sensitive + cf.set("b", "A", "value") self.assertTrue(cf.has_option("a", "b")) cf.set("A", "A-B", "A-B value") for opt in ("a-b", "A-b", "a-B", "A-B"): @@ -291,7 +344,7 @@ boolean {0[0]} NO # SF bug #432369: cf = self.fromstring( - "[MySection]\nOption{} first line\n\tsecond line\n".format( + "[MySection]\nOption{} first line \n\tsecond line \n".format( self.delimiters[0])) eq(cf.options("MySection"), ["option"]) eq(cf.get("MySection", "Option"), "first line\nsecond line") @@ -303,6 +356,46 @@ boolean {0[0]} NO self.assertTrue(cf.has_option("section", "Key")) + def test_case_sensitivity_mapping_access(self): + cf = self.newconfig() + cf["A"] = {} + cf["a"] = {"B": "value"} + cf["B"] = {} + L = [section for section in cf] + L.sort() + eq = self.assertEqual + elem_eq = self.assertItemsEqual + eq(L, ["A", "B", configparser.DEFAULTSECT, "a"]) + eq(cf["a"].keys(), {"b"}) + eq(cf["a"]["b"], "value", + "could not locate option, expecting case-insensitive option names") + with self.assertRaises(KeyError): + # section names are case-sensitive + cf["b"]["A"] = "value" + self.assertTrue("b" in cf["a"]) + cf["A"]["A-B"] = "A-B value" + for opt in ("a-b", "A-b", "a-B", "A-B"): + self.assertTrue( + opt in cf["A"], + "has_option() returned false for option which should exist") + eq(cf["A"].keys(), {"a-b"}) + eq(cf["a"].keys(), {"b"}) + del cf["a"]["B"] + elem_eq(cf["a"].keys(), {}) + + # SF bug #432369: + cf = self.fromstring( + "[MySection]\nOption{} first line \n\tsecond line \n".format( + self.delimiters[0])) + eq(cf["MySection"].keys(), {"option"}) + eq(cf["MySection"]["Option"], "first line\nsecond line") + + # SF bug #561822: + cf = self.fromstring("[section]\n" + "nekey{}nevalue\n".format(self.delimiters[0]), + defaults={"key":"value"}) + self.assertTrue("Key" in cf["section"]) + def test_default_case_sensitivity(self): cf = self.newconfig({"foo": "Bar"}) self.assertEqual( |