Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit54f7e14

Browse files
pslacerdajaraco
andauthored
gh-66449: configparser: Add support for unnamed sections (#117273)
Co-authored-by: Jason R. Coombs <jaraco@jaraco.com>
1 parentd9cfe7e commit54f7e14

File tree

5 files changed

+172
-31
lines changed

5 files changed

+172
-31
lines changed

‎Doc/library/configparser.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,11 @@ may be treated as parts of multiline values or ignored.
274274
By default, a valid section name can be any string that does not contain '\\n'.
275275
To change this, see:attr:`ConfigParser.SECTCRE`.
276276

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+
277282
Configuration files may include comments, prefixed by specific
278283
characters (``#`` and ``;`` by default [1]_). Comments may appear on
279284
their own on an otherwise empty line, possibly indented. [1]_
@@ -325,6 +330,27 @@ For example:
325330
# Did I mention we can indent comments, too?
326331
327332
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+
...[ Section2 ]
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+
328354
Interpolation of values
329355
-----------------------
330356

@@ -1216,6 +1242,11 @@ ConfigParser Objects
12161242
names is stripped before:meth:`optionxform` is called.
12171243

12181244

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+
12191250
..data::MAX_INTERPOLATION_DEPTH
12201251

12211252
The maximum depth for recursive interpolation for:meth:`~configparser.ConfigParser.get` when the *raw*

‎Doc/whatsnew/3.13.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ Other Language Changes
214214

215215
(Contributed by William Woodruff in:gh:`112389`.)
216216

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+
217223
New Modules
218224
===========
219225

‎Lib/configparser.py

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
delimiters=('=', ':'), comment_prefixes=('#', ';'),
1919
inline_comment_prefixes=None, strict=True,
2020
empty_lines_in_values=True, default_section='DEFAULT',
21-
interpolation=<unset>, converters=<unset>):
22-
21+
interpolation=<unset>, converters=<unset>,
22+
allow_unnamed_section=False):
2323
Create the parser. When `defaults` is given, it is initialized into the
2424
dictionary or intrinsic defaults. The keys must be strings, the values
2525
must be appropriate for %()s string interpolation.
@@ -68,6 +68,10 @@
6868
converter gets its corresponding get*() method on the parser object and
6969
section proxies.
7070
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+
7175
sections()
7276
Return all the configuration section names, sans DEFAULT.
7377
@@ -156,7 +160,7 @@
156160
"ConfigParser","RawConfigParser",
157161
"Interpolation","BasicInterpolation","ExtendedInterpolation",
158162
"SectionProxy","ConverterMapping",
159-
"DEFAULTSECT","MAX_INTERPOLATION_DEPTH")
163+
"DEFAULTSECT","MAX_INTERPOLATION_DEPTH","UNNAMED_SECTION")
160164

161165
_default_dict=dict
162166
DEFAULTSECT="DEFAULT"
@@ -336,6 +340,15 @@ def __init__(self, filename, lineno, line):
336340
self.line=line
337341
self.args= (filename,lineno,line)
338342

343+
class_UnnamedSection:
344+
345+
def__repr__(self):
346+
return"<UNNAMED_SECTION>"
347+
348+
349+
UNNAMED_SECTION=_UnnamedSection()
350+
351+
339352
# Used in parser getters to indicate the default behaviour when a specific
340353
# option is not found it to raise an exception. Created to enable `None` as
341354
# a valid fallback value.
@@ -550,7 +563,8 @@ def __init__(self, defaults=None, dict_type=_default_dict,
550563
comment_prefixes=('#',';'),inline_comment_prefixes=None,
551564
strict=True,empty_lines_in_values=True,
552565
default_section=DEFAULTSECT,
553-
interpolation=_UNSET,converters=_UNSET):
566+
interpolation=_UNSET,converters=_UNSET,
567+
allow_unnamed_section=False,):
554568

555569
self._dict=dict_type
556570
self._sections=self._dict()
@@ -589,6 +603,7 @@ def __init__(self, defaults=None, dict_type=_default_dict,
589603
self._converters.update(converters)
590604
ifdefaults:
591605
self._read_defaults(defaults)
606+
self._allow_unnamed_section=allow_unnamed_section
592607

593608
defdefaults(self):
594609
returnself._defaults
@@ -862,13 +877,19 @@ def write(self, fp, space_around_delimiters=True):
862877
ifself._defaults:
863878
self._write_section(fp,self.default_section,
864879
self._defaults.items(),d)
880+
ifUNNAMED_SECTIONinself._sections:
881+
self._write_section(fp,UNNAMED_SECTION,self._sections[UNNAMED_SECTION].items(),d,unnamed=True)
882+
865883
forsectioninself._sections:
884+
ifsectionisUNNAMED_SECTION:
885+
continue
866886
self._write_section(fp,section,
867887
self._sections[section].items(),d)
868888

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+
ifnotunnamed:
892+
fp.write("[{}]\n".format(section_name))
872893
forkey,valueinsection_items:
873894
value=self._interpolation.before_write(self,section_name,key,
874895
value)
@@ -961,6 +982,7 @@ def _read(self, fp, fpname):
961982
lineno=0
962983
indent_level=0
963984
e=None# None, or an exception
985+
964986
try:
965987
forlineno,lineinenumerate(fp,start=1):
966988
comment_start=sys.maxsize
@@ -1007,6 +1029,13 @@ def _read(self, fp, fpname):
10071029
cursect[optname].append(value)
10081030
# a section header or option header?
10091031
else:
1032+
ifself._allow_unnamed_sectionandcursectisNone:
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+
10101039
indent_level=cur_indent_level
10111040
# is it a section header?
10121041
mo=self.SECTCRE.match(value)
@@ -1027,36 +1056,61 @@ def _read(self, fp, fpname):
10271056
elements_added.add(sectname)
10281057
# So sections can't start with a continuation line
10291058
optname=None
1030-
# no section header in the file?
1059+
# no section header?
10311060
elifcursectisNone:
10321061
raiseMissingSectionHeaderError(fpname,lineno,line)
1033-
# an option line?
1062+
# an option line?
10341063
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)
10361067
ifmo:
1037-
optname,vi,optval=mo.group('option','vi','value')
1038-
ifnotoptname:
1039-
e=self._handle_error(e,fpname,lineno,line)
1040-
optname=self.optionxform(optname.rstrip())
1041-
if (self._strictand
1042-
(sectname,optname)inelements_added):
1043-
raiseDuplicateOptionError(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-
ifoptvalisnotNone:
1049-
optval=optval.strip()
1050-
cursect[optname]= [optval]
1068+
sectname=mo.group('header')
1069+
ifsectnameinself._sections:
1070+
ifself._strictandsectnameinelements_added:
1071+
raiseDuplicateSectionError(sectname,fpname,
1072+
lineno)
1073+
cursect=self._sections[sectname]
1074+
elements_added.add(sectname)
1075+
elifsectname==self.default_section:
1076+
cursect=self._defaults
10511077
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+
elifcursectisNone:
1086+
raiseMissingSectionHeaderError(fpname,lineno,line)
1087+
# an option line?
10541088
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+
ifmo:
1091+
optname,vi,optval=mo.group('option','vi','value')
1092+
ifnotoptname:
1093+
e=self._handle_error(e,fpname,lineno,line)
1094+
optname=self.optionxform(optname.rstrip())
1095+
if (self._strictand
1096+
(sectname,optname)inelements_added):
1097+
raiseDuplicateOptionError(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+
ifoptvalisnotNone:
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)
10601114
finally:
10611115
self._join_multiline_values()
10621116
# if any parsing errors occurred, raise an exception

‎Lib/test/test_configparser.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2115,6 +2115,54 @@ def test_instance_assignment(self):
21152115
self.assertEqual(cfg['two'].getlen('one'),5)
21162116

21172117

2118+
classSectionlessTestCase(unittest.TestCase):
2119+
2120+
deffromstring(self,string):
2121+
cfg=configparser.ConfigParser(allow_unnamed_section=True)
2122+
cfg.read_string(string)
2123+
returncfg
2124+
2125+
deftest_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+
deftest_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+
21182166
classMiscTestCase(unittest.TestCase):
21192167
deftest__all__(self):
21202168
support.check__all__(self,configparser,not_exported={"Error"})
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:class:`configparser.ConfigParser` now accepts unnamed sections before named
2+
ones, if configured to do so.

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp