From b25a791802a1915097e02bfba04e27a41ae55ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Fri, 17 Dec 2010 01:32:29 +0000 Subject: =?UTF-8?q?configparser=20API=20cleanup:=20default=20values=20now?= =?UTF-8?q?=20sensible,=20slightly=20incompatible.=20Backwards=20compatibl?= =?UTF-8?q?e=20alternative=20values=20possible=20as=20documented.=20Done?= =?UTF-8?q?=20by=20=C5=81ukasz=20Langa,=20approved=20by=20Raymond=20and=20?= =?UTF-8?q?Fred.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doc/library/configparser.rst | 127 ++++++++++++++++++++++++++++++------------- Lib/configparser.py | 42 +++++++------- Lib/test/test_cfgparser.py | 12 +++- 3 files changed, 117 insertions(+), 64 deletions(-) diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst index 421015a..9472b88 100644 --- a/Doc/library/configparser.rst +++ b/Doc/library/configparser.rst @@ -230,21 +230,18 @@ may be treated as parts of multiline values or ignored. Configuration files may include comments, prefixed by specific characters (``#`` and ``;`` by default [1]_). Comments may appear on -their own on an otherwise empty line, or may be entered on 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 [1]_. +their own on an otherwise empty line, possibly indented. [1]_ For example: .. code-block:: ini [Simple Values] - key: value - spaces in keys: allowed - spaces in values: allowed as well - you can also use = to delimit keys from values + key=value + spaces in keys=allowed + spaces in values=allowed as well + spaces around the delimiter = obviously + you can also use : to delimit keys from values [All Values Are Strings] values like this: 1000000 @@ -261,12 +258,14 @@ For example: key_without_value empty string value here = - [You can use comments] ; after a useful line - ; in an empty line - after: a_value ; here's another comment - inside: a ;comment - multiline ;comment - value! ;comment + [You can use comments] + # like this + ; or this + + # By default only in an empty line. + # Inline comments can be harmful because they prevent users + # from using the delimiting characters as parts of values. + # That being said, this can be customized. [Sections Can Be Indented] can_values_be_as_well = True @@ -509,7 +508,8 @@ the :meth:`__init__` options: ... skip-external-locking ... old_passwords = 1 ... skip-bdb - ... skip-innodb # we don't need ACID today + ... # we don't need ACID today + ... skip-innodb ... """ >>> config = configparser.ConfigParser(allow_no_value=True) >>> config.read_string(sample_config) @@ -536,28 +536,78 @@ the :meth:`__init__` options: See also the *space_around_delimiters* argument to :meth:`ConfigParser.write`. -* *comment_prefixes*, default value: ``_COMPATIBLE`` (``'#'`` valid on empty - lines, ``';'`` valid also on non-empty lines) +* *comment_prefixes*, default value: ``('#', ';')`` - Comment prefixes are strings 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. +* *inline_comment_prefixes*, default value: ``None`` - 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. + Comment prefixes are strings that indicate the start of a valid comment within + a config file. *comment_prefixes* are used only on otherwise empty lines + (optionally indented) whereas *inline_comment_prefixes* can be used after + every valid value (e.g. section names, options and empty lines as well). By + default inline comments are disabled and ``'#'`` and ``';'`` are used as + prefixes for whole line comments. -* *strict*, default value: ``False`` + .. versionchanged:: 3.2 + In previous versions of :mod:`configparser` behaviour matched + ``comment_prefixes=('#',';')`` and ``inline_comment_prefixes=(';',)``. - If set to ``True``, the parser will not allow for any section or option + Please note that config parsers don't support escaping of comment prefixes so + using *inline_comment_prefixes* may prevent users from specifying option + values with characters used as comment prefixes. When in doubt, avoid setting + *inline_comment_prefixes*. In any circumstances, the only way of storing + comment prefix characters at the beginning of a line in multiline values is to + interpolate the prefix, for example:: + + >>> from configparser import ConfigParser, ExtendedInterpolation + >>> parser = ConfigParser(interpolation=ExtendedInterpolation()) + >>> # the default BasicInterpolation could be used as well + >>> parser.read_string(""" + ... [DEFAULT] + ... hash = # + ... + ... [hashes] + ... shebang = + ... ${hash}!/usr/bin/env python + ... ${hash} -*- coding: utf-8 -*- + ... + ... extensions = + ... enabled_extension + ... another_extension + ... #disabled_by_comment + ... yet_another_extension + ... + ... interpolation not necessary = if # is not at line start + ... even in multiline values = line #1 + ... line #2 + ... line #3 + ... """) + >>> print(parser['hashes']['shebang']) + + #!/usr/bin/env python + # -*- coding: utf-8 -*- + >>> print(parser['hashes']['extensions']) + + enabled_extension + another_extension + yet_another_extension + >>> print(parser['hashes']['interpolation not necessary']) + if # is not at line start + >>> print(parser['hashes']['even in multiline values']) + line #1 + line #2 + line #3 + +* *strict*, default value: ``True`` + + When set to ``True``, the parser will not allow for any section or option duplicates while reading from a single source (using :meth:`read_file`, - :meth:`read_string` or :meth:`read_dict`). The default is ``False`` only - because of backwards compatibility reasons. It is recommended to use strict + :meth:`read_string` or :meth:`read_dict`). It is recommended to use strict parsers in new applications. + .. versionchanged:: 3.2 + In previous versions of :mod:`configparser` behaviour matched + ``strict=False``. + * *empty_lines_in_values*, default value: ``True`` In config parsers, values can span multiple lines as long as they are @@ -575,7 +625,6 @@ the :meth:`__init__` options: 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 is why when your application does not need values with empty lines, you should consider disallowing them. This @@ -603,8 +652,7 @@ the :meth:`__init__` options: 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. + :class:`RawConfigParser` has a default value of ``None``. More advanced customization may be achieved by overriding default values of @@ -769,7 +817,7 @@ interpolation if an option used is not defined elsewhere. :: 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=BasicInterpolation()) +.. class:: ConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT, interpolation=BasicInterpolation()) The main configuration parser. When *defaults* is given, it is initialized into the dictionary of intrinsic defaults. When *dict_type* is given, it @@ -778,12 +826,15 @@ ConfigParser Objects When *delimiters* is given, it is used as the set of substrings that divide keys from values. When *comment_prefixes* is given, it will be used - as the set of substrings that prefix comments in a line, both for the whole + as the set of substrings that prefix comments in otherwise empty lines. + Comments can be indented. When *inline_comment_prefixes* is given, it will be + used as the set of substrings that prefix comments in non-empty lines. + 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 + When *strict* is ``True`` (the default), 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`` @@ -1043,7 +1094,7 @@ ConfigParser 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) +.. class:: RawConfigParser(defaults=None, dict_type=collections.OrderedDict, allow_no_value=False, delimiters=('=', ':'), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=configaparser.DEFAULTSECT, interpolation=None) Legacy variant of the :class:`ConfigParser` with interpolation disabled by default and unsafe ``add_section`` and ``set`` methods. diff --git a/Lib/configparser.py b/Lib/configparser.py index aba444c..0e41d2f 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -15,8 +15,9 @@ ConfigParser -- responsible for parsing a list of methods: __init__(defaults=None, dict_type=_default_dict, allow_no_value=False, - delimiters=('=', ':'), comment_prefixes=_COMPATIBLE, - strict=False, empty_lines_in_values=True): + delimiters=('=', ':'), comment_prefixes=('#', ';'), + inline_comment_prefixes=None, strict=True, + empty_lines_in_values=True): Create the parser. When `defaults' is given, it is initialized into the dictionary or intrinsic defaults. The keys must be strings, the values must be appropriate for %()s string interpolation. @@ -29,11 +30,15 @@ ConfigParser -- responsible for parsing a list of 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. + substrings that prefix comments in empty lines. Comments can be + indented. + + When `inline_comment_prefixes' is given, it will be used as the set of + substrings that prefix comments in non-empty lines. When `strict` is True, the parser won't allow for any section or option duplicates while reading from a single source (file, string or - dictionary). Default is False. + dictionary). Default is True. When `empty_lines_in_values' is False (default: True), each empty line marks the end of an option. Otherwise, internal empty lines of @@ -340,11 +345,6 @@ class MissingSectionHeaderError(ParsingError): self.args = (filename, lineno, line) -# Used in parsers to denote selecting a backwards-compatible inline comment -# character behavior (; and # are comments at the start of a line, but ; only -# inline) -_COMPATIBLE = object() - # Used in parser getters to indicate the default behaviour when a specific # option is not found it to raise an exception. Created to enable `None' as # a valid fallback value. @@ -592,8 +592,8 @@ class RawConfigParser(MutableMapping): def __init__(self, defaults=None, dict_type=_default_dict, allow_no_value=False, *, delimiters=('=', ':'), - comment_prefixes=_COMPATIBLE, strict=False, - empty_lines_in_values=True, + comment_prefixes=('#', ';'), inline_comment_prefixes=None, + strict=True, empty_lines_in_values=True, default_section=DEFAULTSECT, interpolation=_UNSET): @@ -616,12 +616,8 @@ class RawConfigParser(MutableMapping): else: self._optcre = re.compile(self._OPT_TMPL.format(delim=d), re.VERBOSE) - if comment_prefixes is _COMPATIBLE: - self._startonly_comment_prefixes = ('#',) - self._comment_prefixes = (';',) - else: - self._startonly_comment_prefixes = () - self._comment_prefixes = tuple(comment_prefixes or ()) + self._comment_prefixes = tuple(comment_prefixes or ()) + self._inline_comment_prefixes = tuple(inline_comment_prefixes or ()) self._strict = strict self._allow_no_value = allow_no_value self._empty_lines_in_values = empty_lines_in_values @@ -989,18 +985,18 @@ class RawConfigParser(MutableMapping): indent_level = 0 e = None # None, or an exception for lineno, line in enumerate(fp, start=1): - # strip full line comments comment_start = None - for prefix in self._startonly_comment_prefixes: - if line.strip().startswith(prefix): - comment_start = 0 - break # strip inline comments - for prefix in self._comment_prefixes: + for prefix in self._inline_comment_prefixes: index = line.find(prefix) if index == 0 or (index > 0 and line[index-1].isspace()): comment_start = index break + # strip full line comments + for prefix in self._comment_prefixes: + if line.strip().startswith(prefix): + comment_start = 0 + break value = line[:comment_start].strip() if not value: if self._empty_lines_in_values: diff --git a/Lib/test/test_cfgparser.py b/Lib/test/test_cfgparser.py index 08a313a..4b2d2df 100644 --- a/Lib/test/test_cfgparser.py +++ b/Lib/test/test_cfgparser.py @@ -29,6 +29,7 @@ class CfgParserTestCaseClass(unittest.TestCase): allow_no_value = False delimiters = ('=', ':') comment_prefixes = (';', '#') + inline_comment_prefixes = (';', '#') empty_lines_in_values = True dict_type = configparser._default_dict strict = False @@ -41,6 +42,7 @@ class CfgParserTestCaseClass(unittest.TestCase): allow_no_value=self.allow_no_value, delimiters=self.delimiters, comment_prefixes=self.comment_prefixes, + inline_comment_prefixes=self.inline_comment_prefixes, empty_lines_in_values=self.empty_lines_in_values, dict_type=self.dict_type, strict=self.strict, @@ -812,6 +814,7 @@ class ConfigParserTestCaseLegacyInterpolation(ConfigParserTestCase): class ConfigParserTestCaseNonStandardDelimiters(ConfigParserTestCase): delimiters = (':=', '$') comment_prefixes = ('//', '"') + inline_comment_prefixes = ('//', '"') class ConfigParserTestCaseNonStandardDefaultSection(ConfigParserTestCase): default_section = 'general' @@ -888,10 +891,12 @@ class RawConfigParserTestCase(BasicTestCase): class RawConfigParserTestCaseNonStandardDelimiters(RawConfigParserTestCase): delimiters = (':=', '$') comment_prefixes = ('//', '"') + inline_comment_prefixes = ('//', '"') -class RawConfigParserTestSambaConf(BasicTestCase): +class RawConfigParserTestSambaConf(CfgParserTestCaseClass): config_class = configparser.RawConfigParser - comment_prefixes = ('#', ';', '//', '----') + comment_prefixes = ('#', ';', '----') + inline_comment_prefixes = ('//',) empty_lines_in_values = False def test_reading(self): @@ -1074,7 +1079,8 @@ class SortedTestCase(RawConfigParserTestCase): class CompatibleTestCase(CfgParserTestCaseClass): config_class = configparser.RawConfigParser - comment_prefixes = configparser._COMPATIBLE + comment_prefixes = '#;' + inline_comment_prefixes = ';' def test_comment_handling(self): config_string = textwrap.dedent("""\ -- cgit v0.12