Skip to content

Commit 1b72935

Browse files
jaracoclaude
andauthored
gh-159684: Modernize string formatting and simplify constructs in configparser (#159687)
* gh-159684: Use f-strings in configparser and its tests Convert the remaining printf-style and str.format() call sites in Lib/configparser.py and Lib/test/test_configparser.py to f-strings (Ruff UP030, UP031, UP032), rewrapping the lines that would otherwise exceed 79 columns. No behavior change; the rendered strings are identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * gh-159684: Simplify a few constructs in configparser and its tests Apply the straightforward simplifications Ruff reports for these two modules: redundant list() calls (C408, C414), a literal set (C405), a key check that get() already covers (RUF019), an unpacked value that is never used (RUF059), an unused assertRaises binding (F841), collapsible if statements (SIM102) and nested with statements (SIM117). Also parenthesize an implicit string concatenation inside a list literal (ISC004) so it is not mistaken for a missing comma. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * gh-159684: Convert the last percent-formatting in configparser Ruff leaves these UP031 sites alone because the %d conversions are not provably integral, so convert them by hand, keeping the explicit :d format spec so a non-integer still raises rather than being coerced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * gh-159684: Clear the remaining Ruff findings in configparser Sort the imports (I001) and __all__ (RUF022), and annotate the mutable class attributes as ClassVar (RUF012). The ClassVar annotation would otherwise mean importing typing at runtime, which this module avoids, so defer annotations with __future__ instead and import ClassVar only under TYPE_CHECKING, as tomllib and importlib.metadata do. Deferring annotations also lets ParsingError be referenced unquoted (UP037). ExtendedInterpolation._interpolate_some gets an explicit noqa: C901, tagged with gh-159686, which records why the complexity is inherent to the interpolation grammar and what a refactoring might look like. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * gh-159684: Add a NEWS entry Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 4372120 commit 1b72935

3 files changed

Lines changed: 227 additions & 199 deletions

File tree

‎Lib/configparser.py‎

Lines changed: 80 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -143,27 +143,50 @@
143143
between keys and values are surrounded by spaces.
144144
"""
145145

146-
# Do not import dataclasses; overhead is unacceptable (gh-117703)
146+
# Defer annotations so that typing need not be imported at runtime.
147+
from __future__ import annotations
147148

148-
from collections.abc import Iterable, MutableMapping
149-
from collections import ChainMap as _ChainMap
149+
# Do not import dataclasses; overhead is unacceptable (gh-117703)
150150
import contextlib
151151
import functools
152152
import io
153153
import itertools
154154
import os
155155
import re
156156
import sys
157+
from collections import ChainMap as _ChainMap
158+
from collections.abc import Iterable, MutableMapping
157159

158-
__all__ = ("NoSectionError", "DuplicateOptionError", "DuplicateSectionError",
159-
"NoOptionError", "InterpolationError", "InterpolationDepthError",
160-
"InterpolationMissingOptionError", "InterpolationSyntaxError",
161-
"ParsingError", "MissingSectionHeaderError",
162-
"MultilineContinuationError", "UnnamedSectionDisabledError",
163-
"InvalidWriteError", "ConfigParser", "RawConfigParser",
164-
"Interpolation", "BasicInterpolation", "ExtendedInterpolation",
165-
"SectionProxy", "ConverterMapping",
166-
"DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION")
160+
# Do not import typing either; it is only needed for annotations.
161+
TYPE_CHECKING = False
162+
if TYPE_CHECKING:
163+
from typing import ClassVar
164+
165+
__all__ = (
166+
"DEFAULTSECT",
167+
"MAX_INTERPOLATION_DEPTH",
168+
"UNNAMED_SECTION",
169+
"BasicInterpolation",
170+
"ConfigParser",
171+
"ConverterMapping",
172+
"DuplicateOptionError",
173+
"DuplicateSectionError",
174+
"ExtendedInterpolation",
175+
"Interpolation",
176+
"InterpolationDepthError",
177+
"InterpolationError",
178+
"InterpolationMissingOptionError",
179+
"InterpolationSyntaxError",
180+
"InvalidWriteError",
181+
"MissingSectionHeaderError",
182+
"MultilineContinuationError",
183+
"NoOptionError",
184+
"NoSectionError",
185+
"ParsingError",
186+
"RawConfigParser",
187+
"SectionProxy",
188+
"UnnamedSectionDisabledError",
189+
)
167190

168191
_default_dict = dict
169192
DEFAULTSECT = "DEFAULT"
@@ -190,7 +213,7 @@ class NoSectionError(Error):
190213
"""Raised when no section matches a requested option."""
191214

192215
def __init__(self, section):
193-
Error.__init__(self, 'No section: %r' % (section,))
216+
Error.__init__(self, f'No section: {section!r}')
194217
self.section = section
195218
self.args = (section, )
196219

@@ -208,7 +231,7 @@ def __init__(self, section, source=None, lineno=None):
208231
if source is not None:
209232
message = ["While reading from ", repr(source)]
210233
if lineno is not None:
211-
message.append(" [line {0:2d}]".format(lineno))
234+
message.append(f" [line {lineno:2d}]")
212235
message.append(": section ")
213236
message.extend(msg)
214237
msg = message
@@ -234,7 +257,7 @@ def __init__(self, section, option, source=None, lineno=None):
234257
if source is not None:
235258
message = ["While reading from ", repr(source)]
236259
if lineno is not None:
237-
message.append(" [line {0:2d}]".format(lineno))
260+
message.append(f" [line {lineno:2d}]")
238261
message.append(": option ")
239262
message.extend(msg)
240263
msg = message
@@ -252,8 +275,7 @@ class NoOptionError(Error):
252275
"""A requested option was not found."""
253276

254277
def __init__(self, option, section):
255-
Error.__init__(self, "No option %r in section: %r" %
256-
(option, section))
278+
Error.__init__(self, f"No option {option!r} in section: {section!r}")
257279
self.option = option
258280
self.section = section
259281
self.args = (option, section)
@@ -273,9 +295,9 @@ class InterpolationMissingOptionError(InterpolationError):
273295
"""A string substitution required a setting which was not available."""
274296

275297
def __init__(self, option, section, rawval, reference):
276-
msg = ("Bad value substitution: option {!r} in section {!r} contains "
277-
"an interpolation key {!r} which is not a valid option name. "
278-
"Raw value: {!r}".format(option, section, reference, rawval))
298+
msg = (f"Bad value substitution: option {option!r} in section "
299+
f"{section!r} contains an interpolation key {reference!r} "
300+
f"which is not a valid option name. Raw value: {rawval!r}")
279301
InterpolationError.__init__(self, option, section, msg)
280302
self.reference = reference
281303
self.args = (option, section, rawval, reference)
@@ -293,11 +315,10 @@ class InterpolationDepthError(InterpolationError):
293315
"""Raised when substitutions are nested too deeply."""
294316

295317
def __init__(self, option, section, rawval):
296-
msg = ("Recursion limit exceeded in value substitution: option {!r} "
297-
"in section {!r} contains an interpolation key which "
298-
"cannot be substituted in {} steps. Raw value: {!r}"
299-
"".format(option, section, MAX_INTERPOLATION_DEPTH,
300-
rawval))
318+
msg = (f"Recursion limit exceeded in value substitution: option "
319+
f"{option!r} in section {section!r} contains an interpolation "
320+
f"key which cannot be substituted in "
321+
f"{MAX_INTERPOLATION_DEPTH} steps. Raw value: {rawval!r}")
301322
InterpolationError.__init__(self, option, section, msg)
302323
self.args = (option, section, rawval)
303324

@@ -327,7 +348,7 @@ def combine(self, others):
327348
return self
328349

329350
@staticmethod
330-
def _raise_all(exceptions: Iterable['ParsingError']):
351+
def _raise_all(exceptions: Iterable[ParsingError]):
331352
"""
332353
Combine any number of ParsingErrors into one and raise it.
333354
"""
@@ -343,8 +364,8 @@ class MissingSectionHeaderError(ParsingError):
343364
def __init__(self, filename, lineno, line):
344365
Error.__init__(
345366
self,
346-
'File contains no section headers.\nfile: %r, line: %d\n%r' %
347-
(filename, lineno, line))
367+
'File contains no section headers.\n'
368+
f'file: {filename!r}, line: {lineno:d}\n{line!r}')
348369
self.source = filename
349370
self.lineno = lineno
350371
self.line = line
@@ -357,8 +378,7 @@ def __init__(self, filename, lineno, line):
357378
Error.__init__(
358379
self,
359380
"Key without value continued with an indented line.\n"
360-
"file: %r, line: %d\n%r"
361-
%(filename, lineno, line))
381+
f"file: {filename!r}, line: {lineno:d}\n{line!r}")
362382
self.source = filename
363383
self.lineno = lineno
364384
self.line = line
@@ -437,8 +457,8 @@ def before_set(self, parser, section, option, value):
437457
tmp_value = value.replace('%%', '') # escaped percent signs
438458
tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax
439459
if '%' in tmp_value:
440-
raise ValueError("invalid interpolation syntax in %r at "
441-
"position %d" % (value, tmp_value.find('%')))
460+
raise ValueError(f"invalid interpolation syntax in {value!r} "
461+
f"at position {tmp_value.find('%'):d}")
442462
return value
443463

444464
def _interpolate_some(self, parser, option, accum, rest, section, map,
@@ -463,7 +483,7 @@ def _interpolate_some(self, parser, option, accum, rest, section, map,
463483
m = self._KEYCRE.match(rest)
464484
if m is None:
465485
raise InterpolationSyntaxError(option, section,
466-
"bad interpolation variable reference %r" % rest)
486+
f"bad interpolation variable reference {rest!r}")
467487
var = parser.optionxform(m.group(1))
468488
rest = rest[m.end():]
469489
try:
@@ -479,8 +499,8 @@ def _interpolate_some(self, parser, option, accum, rest, section, map,
479499
else:
480500
raise InterpolationSyntaxError(
481501
option, section,
482-
"'%%' must be followed by '%%' or '(', "
483-
"found: %r" % (rest,))
502+
"'%' must be followed by '%' or '(', "
503+
f"found: {rest!r}")
484504

485505

486506
class ExtendedInterpolation(Interpolation):
@@ -498,12 +518,12 @@ def before_set(self, parser, section, option, value):
498518
tmp_value = value.replace('$$', '') # escaped dollar signs
499519
tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax
500520
if '$' in tmp_value:
501-
raise ValueError("invalid interpolation syntax in %r at "
502-
"position %d" % (value, tmp_value.find('$')))
521+
raise ValueError(f"invalid interpolation syntax in {value!r} "
522+
f"at position {tmp_value.find('$'):d}")
503523
return value
504524

505-
def _interpolate_some(self, parser, option, accum, rest, section, map,
506-
depth):
525+
def _interpolate_some(self, parser, option, # noqa: C901 # gh-159686
526+
accum, rest, section, map, depth):
507527
rawval = parser.get(section, option, raw=True, fallback=rest)
508528
if depth > MAX_INTERPOLATION_DEPTH:
509529
raise InterpolationDepthError(option, section, rawval)
@@ -524,7 +544,7 @@ def _interpolate_some(self, parser, option, accum, rest, section, map,
524544
m = self._KEYCRE.match(rest)
525545
if m is None:
526546
raise InterpolationSyntaxError(option, section,
527-
"bad interpolation variable reference %r" % rest)
547+
f"bad interpolation variable reference {rest!r}")
528548
path = m.group(1).split(':')
529549
rest = rest[m.end():]
530550
sect = section
@@ -540,7 +560,7 @@ def _interpolate_some(self, parser, option, accum, rest, section, map,
540560
else:
541561
raise InterpolationSyntaxError(
542562
option, section,
543-
"More than one ':' found: %r" % (rest,))
563+
f"More than one ':' found: {rest!r}")
544564
except (KeyError, NoSectionError, NoOptionError):
545565
raise InterpolationMissingOptionError(
546566
option, section, rawval, ":".join(path)) from None
@@ -556,7 +576,7 @@ def _interpolate_some(self, parser, option, accum, rest, section, map,
556576
raise InterpolationSyntaxError(
557577
option, section,
558578
"'$' must be followed by '$' or '{', "
559-
"found: %r" % (rest,))
579+
f"found: {rest!r}")
560580

561581

562582
class _ReadState:
@@ -570,7 +590,7 @@ class _ReadState:
570590

571591
def __init__(self):
572592
self.elements_added = set()
573-
self.errors = list()
593+
self.errors = []
574594

575595

576596
class _Line(str):
@@ -650,8 +670,10 @@ class RawConfigParser(MutableMapping):
650670
# Compiled regular expression for matching leading whitespace in a line
651671
NONSPACECRE = re.compile(r"\S")
652672
# Possible boolean values in the configuration.
653-
BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True,
654-
'0': False, 'no': False, 'false': False, 'off': False}
673+
BOOLEAN_STATES: ClassVar[dict[str, bool]] = {
674+
'1': True, 'yes': True, 'true': True, 'on': True,
675+
'0': False, 'no': False, 'false': False, 'off': False,
676+
}
655677

656678
def __init__(self, defaults=None, dict_type=_default_dict,
657679
allow_no_value=False, *, delimiters=('=', ':'),
@@ -714,11 +736,10 @@ def add_section(self, section):
714736
already exists. Raise ValueError if name is DEFAULT.
715737
"""
716738
if section == self.default_section:
717-
raise ValueError('Invalid section name: %r' % section)
739+
raise ValueError(f'Invalid section name: {section!r}')
718740

719-
if section is UNNAMED_SECTION:
720-
if not self._allow_unnamed_section:
721-
raise UnnamedSectionDisabledError
741+
if section is UNNAMED_SECTION and not self._allow_unnamed_section:
742+
raise UnnamedSectionDisabledError
722743

723744
if section in self._sections:
724745
raise DuplicateSectionError(section)
@@ -970,13 +991,13 @@ def write(self, fp, space_around_delimiters=True):
970991
preserved when writing the configuration back.
971992
"""
972993
if space_around_delimiters:
973-
d = " {} ".format(self._delimiters[0])
994+
d = f" {self._delimiters[0]} "
974995
else:
975996
d = self._delimiters[0]
976997
if self._defaults:
977998
self._write_section(fp, self.default_section,
978999
self._defaults.items(), d)
979-
if UNNAMED_SECTION in self._sections and self._sections[UNNAMED_SECTION]:
1000+
if self._sections.get(UNNAMED_SECTION):
9801001
self._write_section(fp, UNNAMED_SECTION, self._sections[UNNAMED_SECTION].items(), d, unnamed=True)
9811002

9821003
for section in self._sections:
@@ -988,7 +1009,7 @@ def write(self, fp, space_around_delimiters=True):
9881009
def _write_section(self, fp, section_name, section_items, delimiter, unnamed=False):
9891010
"""Write a single section to the specified 'fp'."""
9901011
if not unnamed:
991-
fp.write("[{}]\n".format(section_name))
1012+
fp.write(f"[{section_name}]\n")
9921013
for key, value in section_items:
9931014
self._validate_key_contents(key)
9941015
value = self._interpolation.before_write(self, section_name, key,
@@ -999,7 +1020,7 @@ def _write_section(self, fp, section_name, section_items, delimiter, unnamed=Fal
9991020
.replace('\r', '\n').replace('\n', '\n\t'))
10001021
else:
10011022
value = ""
1002-
fp.write("{}{}\n".format(key, value))
1023+
fp.write(f"{key}{value}\n")
10031024
fp.write("\n")
10041025

10051026
def remove_option(self, section, option):
@@ -1165,7 +1186,7 @@ def _handle_option(self, st, line, fpname):
11651186
st.errors.append(ParsingError(fpname, st.lineno, line))
11661187
return
11671188

1168-
st.optname, vi, optval = mo.group('option', 'vi', 'value')
1189+
st.optname, optval = mo.group('option', 'value')
11691190
if not st.optname:
11701191
st.errors.append(ParsingError(fpname, st.lineno, line))
11711192
st.optname = self.optionxform(st.optname.rstrip())
@@ -1225,7 +1246,7 @@ def _convert_to_boolean(self, value):
12251246
"""Return a boolean value translating from other types if necessary.
12261247
"""
12271248
if value.lower() not in self.BOOLEAN_STATES:
1228-
raise ValueError('Not a boolean: %s' % value)
1249+
raise ValueError(f'Not a boolean: {value}')
12291250
return self.BOOLEAN_STATES[value.lower()]
12301251

12311252
def _validate_key_contents(self, key):
@@ -1256,9 +1277,8 @@ def _validate_value_types(self, *, section="", option="", value=""):
12561277
raise TypeError("section names must be strings or UNNAMED_SECTION")
12571278
if not isinstance(option, str):
12581279
raise TypeError("option keys must be strings")
1259-
if not self._allow_no_value or value:
1260-
if not isinstance(value, str):
1261-
raise TypeError("option values must be strings")
1280+
if (not self._allow_no_value or value) and not isinstance(value, str):
1281+
raise TypeError("option values must be strings")
12621282

12631283
@property
12641284
def converters(self):
@@ -1310,7 +1330,7 @@ def __init__(self, parser, name):
13101330
setattr(self, key, getter)
13111331

13121332
def __repr__(self):
1313-
return '<Section: {}>'.format(self._name)
1333+
return f'<Section: {self._name}>'
13141334

13151335
def __getitem__(self, key):
13161336
if not self._parser.has_option(self._name, key):
@@ -1393,8 +1413,7 @@ def __setitem__(self, key, value):
13931413
try:
13941414
k = 'get' + key
13951415
except TypeError:
1396-
raise ValueError('Incompatible key: {} (type: {})'
1397-
''.format(key, type(key)))
1416+
raise ValueError(f'Incompatible key: {key} (type: {type(key)})')
13981417
if k == 'get':
13991418
raise ValueError('Incompatible key: cannot use "" as a name')
14001419
self._data[key] = value

0 commit comments

Comments
 (0)