| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -274,6 +274,11 @@ may be treated as parts of multiline values or ignored. | |||
| 274 | 274 | By default, a valid section name can be any string that does not contain '\\n'. | |
| 275 | 275 | To change this, see :attr:`ConfigParser.SECTCRE`. | |
| 276 | 276 | ||
| 277 | + The first section name may be omitted if the parser is configured to allow an | ||
| 278 | + unnamed top level section with ``allow_unnamed_section=True``. In this case, | ||
| 279 | + the keys/values may be retrieved by :const:`UNNAMED_SECTION` as in | ||
| 280 | + ``config[UNNAMED_SECTION]``. | ||
| 281 | + | ||
| 277 | 282 | Configuration files may include comments, prefixed by specific | |
| 278 | 283 | characters (``#`` and ``;`` by default [1]_). Comments may appear on | |
| 279 | 284 | their own on an otherwise empty line, possibly indented. [1]_ | |
@@ -325,6 +330,27 @@ For example: | |||
| 325 | 330 | # Did I mention we can indent comments, too? | |
| 326 | 331 | ||
| 327 | 332 | ||
| 333 | + .. _unnamed-sections: | ||
| 334 | + | ||
| 335 | + Unnamed Sections | ||
| 336 | + ---------------- | ||
| 337 | + | ||
| 338 | + The name of the first section (or unique) may be omitted and values | ||
| 339 | + retrieved by the :const:`UNNAMED_SECTION` attribute. | ||
| 340 | + | ||
| 341 | + .. doctest:: | ||
| 342 | + | ||
| 343 | + >>> config = """ | ||
| 344 | + ... option = value | ||
| 345 | + ... | ||
| 346 | + ... [ Section 2 ] | ||
| 347 | + ... another = val | ||
| 348 | + ... """ | ||
| 349 | + >>> unnamed = configparser.ConfigParser(allow_unnamed_section=True) | ||
| 350 | + >>> unnamed.read_string(config) | ||
| 351 | + >>> unnamed.get(configparser.UNNAMED_SECTION, 'option') | ||
| 352 | + 'value' | ||
| 353 | + | ||
| 328 | 354 | Interpolation of values | |
| 329 | 355 | ----------------------- | |
| 330 | 356 | ||
@@ -1216,6 +1242,11 @@ ConfigParser Objects | |||
| 1216 | 1242 | names is stripped before :meth:`optionxform` is called. | |
| 1217 | 1243 | ||
| 1218 | 1244 | ||
| 1245 | + .. data:: UNNAMED_SECTION | ||
| 1246 | + | ||
| 1247 | + A special object representing a section name used to reference the unnamed section (see :ref:`unnamed-sections`). | ||
| 1248 | + | ||
| 1249 | + | ||
| 1219 | 1250 | .. data:: MAX_INTERPOLATION_DEPTH | |
| 1220 | 1251 | ||
| 1221 | 1252 | The maximum depth for recursive interpolation for :meth:`~configparser.ConfigParser.get` when the *raw* | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -214,6 +214,12 @@ Other Language Changes | |||
| 214 | 214 | ||
| 215 | 215 | (Contributed by William Woodruff in :gh:`112389`.) | |
| 216 | 216 | ||
| 217 | + * The :class:`configparser.ConfigParser` now accepts unnamed sections before named | ||
| 218 | + ones if configured to do so. | ||
| 219 | + | ||
| 220 | + (Contributed by Pedro Sousa Lacerda in :gh:`66449`) | ||
| 221 | + | ||
| 222 | + | ||
| 217 | 223 | New Modules | |
| 218 | 224 | =========== | |
| 219 | 225 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -18,8 +18,8 @@ | |||
| 18 | 18 | delimiters=('=', ':'), comment_prefixes=('#', ';'), | |
| 19 | 19 | inline_comment_prefixes=None, strict=True, | |
| 20 | 20 | empty_lines_in_values=True, default_section='DEFAULT', | |
| 21 | - interpolation=<unset>, converters=<unset>): | ||
| 22 | - | ||
| 21 | + interpolation=<unset>, converters=<unset>, | ||
| 22 | + allow_unnamed_section=False): | ||
| 23 | 23 | Create the parser. When `defaults` is given, it is initialized into the | |
| 24 | 24 | dictionary or intrinsic defaults. The keys must be strings, the values | |
| 25 | 25 | must be appropriate for %()s string interpolation. | |
@@ -68,6 +68,10 @@ | |||
| 68 | 68 | converter gets its corresponding get*() method on the parser object and | |
| 69 | 69 | section proxies. | |
| 70 | 70 | ||
| 71 | + When `allow_unnamed_section` is True (default: False), options | ||
| 72 | + without section are accepted: the section for these is | ||
| 73 | + ``configparser.UNNAMED_SECTION``. | ||
| 74 | + | ||
| 71 | 75 | sections() | |
| 72 | 76 | Return all the configuration section names, sans DEFAULT. | |
| 73 | 77 | ||
@@ -156,7 +160,7 @@ | |||
| 156 | 160 | "ConfigParser", "RawConfigParser", | |
| 157 | 161 | "Interpolation", "BasicInterpolation", "ExtendedInterpolation", | |
| 158 | 162 | "SectionProxy", "ConverterMapping", | |
| 159 | - "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH") | ||
| 163 | + "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION") | ||
| 160 | 164 | ||
| 161 | 165 | _default_dict = dict | |
| 162 | 166 | DEFAULTSECT = "DEFAULT" | |
@@ -336,6 +340,15 @@ def __init__(self, filename, lineno, line): | |||
| 336 | 340 | self.line = line | |
| 337 | 341 | self.args = (filename, lineno, line) | |
| 338 | 342 | ||
| 343 | + class _UnnamedSection: | ||
| 344 | + | ||
| 345 | + def __repr__(self): | ||
| 346 | + return "<UNNAMED_SECTION>" | ||
| 347 | + | ||
| 348 | + | ||
| 349 | + UNNAMED_SECTION = _UnnamedSection() | ||
| 350 | + | ||
| 351 | + | ||
| 339 | 352 | # Used in parser getters to indicate the default behaviour when a specific | |
| 340 | 353 | # option is not found it to raise an exception. Created to enable `None` as | |
| 341 | 354 | # a valid fallback value. | |
@@ -550,7 +563,8 @@ def __init__(self, defaults=None, dict_type=_default_dict, | |||
| 550 | 563 | comment_prefixes=('#', ';'), inline_comment_prefixes=None, | |
| 551 | 564 | strict=True, empty_lines_in_values=True, | |
| 552 | 565 | default_section=DEFAULTSECT, | |
| 553 | - interpolation=_UNSET, converters=_UNSET): | ||
| 566 | + interpolation=_UNSET, converters=_UNSET, | ||
| 567 | + allow_unnamed_section=False,): | ||
| 554 | 568 | ||
| 555 | 569 | self._dict = dict_type | |
| 556 | 570 | self._sections = self._dict() | |
@@ -589,6 +603,7 @@ def __init__(self, defaults=None, dict_type=_default_dict, | |||
| 589 | 603 | self._converters.update(converters) | |
| 590 | 604 | if defaults: | |
| 591 | 605 | self._read_defaults(defaults) | |
| 606 | + self._allow_unnamed_section = allow_unnamed_section | ||
| 592 | 607 | ||
| 593 | 608 | def defaults(self): | |
| 594 | 609 | return self._defaults | |
@@ -862,13 +877,19 @@ def write(self, fp, space_around_delimiters=True): | |||
| 862 | 877 | if self._defaults: | |
| 863 | 878 | self._write_section(fp, self.default_section, | |
| 864 | 879 | self._defaults.items(), d) | |
| 880 | + if UNNAMED_SECTION in self._sections: | ||
| 881 | + self._write_section(fp, UNNAMED_SECTION, self._sections[UNNAMED_SECTION].items(), d, unnamed=True) | ||
| 882 | + | ||
| 865 | 883 | for section in self._sections: | |
| 884 | + if section is UNNAMED_SECTION: | ||
| 885 | + continue | ||
| 866 | 886 | self._write_section(fp, section, | |
| 867 | 887 | self._sections[section].items(), d) | |
| 868 | 888 | ||
| 869 | - def _write_section(self, fp, section_name, section_items, delimiter): | ||
| 870 | - """Write a single section to the specified `fp`.""" | ||
| 871 | - fp.write("[{}]\n".format(section_name)) | ||
| 889 | + def _write_section(self, fp, section_name, section_items, delimiter, unnamed=False): | ||
| 890 | + """Write a single section to the specified `fp'.""" | ||
| 891 | + if not unnamed: | ||
| 892 | + fp.write("[{}]\n".format(section_name)) | ||
| 872 | 893 | for key, value in section_items: | |
| 873 | 894 | value = self._interpolation.before_write(self, section_name, key, | |
| 874 | 895 | value) | |
@@ -961,6 +982,7 @@ def _read(self, fp, fpname): | |||
| 961 | 982 | lineno = 0 | |
| 962 | 983 | indent_level = 0 | |
| 963 | 984 | e = None # None, or an exception | |
| 985 | + | ||
| 964 | 986 | try: | |
| 965 | 987 | for lineno, line in enumerate(fp, start=1): | |
| 966 | 988 | comment_start = sys.maxsize | |
@@ -1007,6 +1029,13 @@ def _read(self, fp, fpname): | |||
| 1007 | 1029 | cursect[optname].append(value) | |
| 1008 | 1030 | # a section header or option header? | |
| 1009 | 1031 | else: | |
| 1032 | + if self._allow_unnamed_section and cursect is None: | ||
| 1033 | + sectname = UNNAMED_SECTION | ||
| 1034 | + cursect = self._dict() | ||
| 1035 | + self._sections[sectname] = cursect | ||
| 1036 | + self._proxies[sectname] = SectionProxy(self, sectname) | ||
| 1037 | + elements_added.add(sectname) | ||
| 1038 | + | ||
| 1010 | 1039 | indent_level = cur_indent_level | |
| 1011 | 1040 | # is it a section header? | |
| 1012 | 1041 | mo = self.SECTCRE.match(value) | |
@@ -1027,36 +1056,61 @@ def _read(self, fp, fpname): | |||
| 1027 | 1056 | elements_added.add(sectname) | |
| 1028 | 1057 | # So sections can't start with a continuation line | |
| 1029 | 1058 | optname = None | |
| 1030 | - # no section header in the file? | ||
| 1059 | + # no section header? | ||
| 1031 | 1060 | elif cursect is None: | |
| 1032 | 1061 | raise MissingSectionHeaderError(fpname, lineno, line) | |
| 1033 | - # an option line? | ||
| 1062 | + # an option line? | ||
| 1034 | 1063 | else: | |
| 1035 | - mo = self._optcre.match(value) | ||
| 1064 | + indent_level = cur_indent_level | ||
| 1065 | + # is it a section header? | ||
| 1066 | + mo = self.SECTCRE.match(value) | ||
| 1036 | 1067 | if mo: | |
| 1037 | - optname, vi, optval = mo.group('option', 'vi', 'value') | ||
| 1038 | - if not optname: | ||
| 1039 | - e = self._handle_error(e, fpname, lineno, line) | ||
| 1040 | - optname = self.optionxform(optname.rstrip()) | ||
| 1041 | - if (self._strict and | ||
| 1042 | - (sectname, optname) in elements_added): | ||
| 1043 | - raise DuplicateOptionError(sectname, optname, | ||
| 1044 | - fpname, lineno) | ||
| 1045 | - elements_added.add((sectname, optname)) | ||
| 1046 | - # This check is fine because the OPTCRE cannot | ||
| 1047 | - # match if it would set optval to None | ||
| 1048 | - if optval is not None: | ||
| 1049 | - optval = optval.strip() | ||
| 1050 | - cursect[optname] = [optval] | ||
| 1068 | + sectname = mo.group('header') | ||
| 1069 | + if sectname in self._sections: | ||
| 1070 | + if self._strict and sectname in elements_added: | ||
| 1071 | + raise DuplicateSectionError(sectname, fpname, | ||
| 1072 | + lineno) | ||
| 1073 | + cursect = self._sections[sectname] | ||
| 1074 | + elements_added.add(sectname) | ||
| 1075 | + elif sectname == self.default_section: | ||
| 1076 | + cursect = self._defaults | ||
| 1051 | 1077 | else: | |
| 1052 | - # valueless option handling | ||
| 1053 | - cursect[optname] = None | ||
| 1078 | + cursect = self._dict() | ||
| 1079 | + self._sections[sectname] = cursect | ||
| 1080 | + self._proxies[sectname] = SectionProxy(self, sectname) | ||
| 1081 | + elements_added.add(sectname) | ||
| 1082 | + # So sections can't start with a continuation line | ||
| 1083 | + optname = None | ||
| 1084 | + # no section header in the file? | ||
| 1085 | + elif cursect is None: | ||
| 1086 | + raise MissingSectionHeaderError(fpname, lineno, line) | ||
| 1087 | + # an option line? | ||
| 1054 | 1088 | else: | |
| 1055 | - # a non-fatal parsing error occurred. set up the | ||
| 1056 | - # exception but keep going. the exception will be | ||
| 1057 | - # raised at the end of the file and will contain a | ||
| 1058 | - # list of all bogus lines | ||
| 1059 | - e = self._handle_error(e, fpname, lineno, line) | ||
| 1089 | + mo = self._optcre.match(value) | ||
| 1090 | + if mo: | ||
| 1091 | + optname, vi, optval = mo.group('option', 'vi', 'value') | ||
| 1092 | + if not optname: | ||
| 1093 | + e = self._handle_error(e, fpname, lineno, line) | ||
| 1094 | + optname = self.optionxform(optname.rstrip()) | ||
| 1095 | + if (self._strict and | ||
| 1096 | + (sectname, optname) in elements_added): | ||
| 1097 | + raise DuplicateOptionError(sectname, optname, | ||
| 1098 | + fpname, lineno) | ||
| 1099 | + elements_added.add((sectname, optname)) | ||
| 1100 | + # This check is fine because the OPTCRE cannot | ||
| 1101 | + # match if it would set optval to None | ||
| 1102 | + if optval is not None: | ||
| 1103 | + optval = optval.strip() | ||
| 1104 | + cursect[optname] = [optval] | ||
| 1105 | + else: | ||
| 1106 | + # valueless option handling | ||
| 1107 | + cursect[optname] = None | ||
| 1108 | + else: | ||
| 1109 | + # a non-fatal parsing error occurred. set up the | ||
| 1110 | + # exception but keep going. the exception will be | ||
| 1111 | + # raised at the end of the file and will contain a | ||
| 1112 | + # list of all bogus lines | ||
| 1113 | + e = self._handle_error(e, fpname, lineno, line) | ||
| 1060 | 1114 | finally: | |
| 1061 | 1115 | self._join_multiline_values() | |
| 1062 | 1116 | # if any parsing errors occurred, raise an exception | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2115,6 +2115,54 @@ def test_instance_assignment(self): | |||
| 2115 | 2115 | self.assertEqual(cfg['two'].getlen('one'), 5) | |
| 2116 | 2116 | ||
| 2117 | 2117 | ||
| 2118 | + class SectionlessTestCase(unittest.TestCase): | ||
| 2119 | + | ||
| 2120 | + def fromstring(self, string): | ||
| 2121 | + cfg = configparser.ConfigParser(allow_unnamed_section=True) | ||
| 2122 | + cfg.read_string(string) | ||
| 2123 | + return cfg | ||
| 2124 | + | ||
| 2125 | + def test_no_first_section(self): | ||
| 2126 | + cfg1 = self.fromstring(""" | ||
| 2127 | + a = 1 | ||
| 2128 | + b = 2 | ||
| 2129 | + [sect1] | ||
| 2130 | + c = 3 | ||
| 2131 | + """) | ||
| 2132 | + | ||
| 2133 | + self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), set(cfg1.sections())) | ||
| 2134 | + self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a']) | ||
| 2135 | + self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b']) | ||
| 2136 | + self.assertEqual('3', cfg1['sect1']['c']) | ||
| 2137 | + | ||
| 2138 | + output = io.StringIO() | ||
| 2139 | + cfg1.write(output) | ||
| 2140 | + cfg2 = self.fromstring(output.getvalue()) | ||
| 2141 | + | ||
| 2142 | + #self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), set(cfg2.sections())) | ||
| 2143 | + self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a']) | ||
| 2144 | + self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b']) | ||
| 2145 | + self.assertEqual('3', cfg2['sect1']['c']) | ||
| 2146 | + | ||
| 2147 | + def test_no_section(self): | ||
| 2148 | + cfg1 = self.fromstring(""" | ||
| 2149 | + a = 1 | ||
| 2150 | + b = 2 | ||
| 2151 | + """) | ||
| 2152 | + | ||
| 2153 | + self.assertEqual([configparser.UNNAMED_SECTION], cfg1.sections()) | ||
| 2154 | + self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a']) | ||
| 2155 | + self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b']) | ||
| 2156 | + | ||
| 2157 | + output = io.StringIO() | ||
| 2158 | + cfg1.write(output) | ||
| 2159 | + cfg2 = self.fromstring(output.getvalue()) | ||
| 2160 | + | ||
| 2161 | + self.assertEqual([configparser.UNNAMED_SECTION], cfg2.sections()) | ||
| 2162 | + self.assertEqual('1', cfg2[configparser.UNNAMED_SECTION]['a']) | ||
| 2163 | + self.assertEqual('2', cfg2[configparser.UNNAMED_SECTION]['b']) | ||
| 2164 | + | ||
| 2165 | + | ||
| 2118 | 2166 | class MiscTestCase(unittest.TestCase): | |
| 2119 | 2167 | def test__all__(self): | |
| 2120 | 2168 | support.check__all__(self, configparser, not_exported={"Error"}) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,2 @@ | |||
| 1 | + :class:`configparser.ConfigParser` now accepts unnamed sections before named | ||
| 2 | + ones, if configured to do so. | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments