From 2db122ce2db632fc83666ec023f30a7fa08a8954 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Oct 2026 15:45:13 -0400 Subject: [PATCH 1/5] 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 --- Lib/configparser.py | 51 ++++--- Lib/test/test_configparser.py | 250 +++++++++++++++++----------------- 2 files changed, 151 insertions(+), 150 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index 88015ef6086569..6965c277634ffe 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -190,7 +190,7 @@ class NoSectionError(Error): """Raised when no section matches a requested option.""" def __init__(self, section): - Error.__init__(self, 'No section: %r' % (section,)) + Error.__init__(self, f'No section: {section!r}') self.section = section self.args = (section, ) @@ -208,7 +208,7 @@ def __init__(self, section, source=None, lineno=None): if source is not None: message = ["While reading from ", repr(source)] if lineno is not None: - message.append(" [line {0:2d}]".format(lineno)) + message.append(f" [line {lineno:2d}]") message.append(": section ") message.extend(msg) msg = message @@ -234,7 +234,7 @@ def __init__(self, section, option, source=None, lineno=None): if source is not None: message = ["While reading from ", repr(source)] if lineno is not None: - message.append(" [line {0:2d}]".format(lineno)) + message.append(f" [line {lineno:2d}]") message.append(": option ") message.extend(msg) msg = message @@ -252,8 +252,7 @@ class NoOptionError(Error): """A requested option was not found.""" def __init__(self, option, section): - Error.__init__(self, "No option %r in section: %r" % - (option, section)) + Error.__init__(self, f"No option {option!r} in section: {section!r}") self.option = option self.section = section self.args = (option, section) @@ -273,9 +272,9 @@ class InterpolationMissingOptionError(InterpolationError): """A string substitution required a setting which was not available.""" def __init__(self, option, section, rawval, reference): - msg = ("Bad value substitution: option {!r} in section {!r} contains " - "an interpolation key {!r} which is not a valid option name. " - "Raw value: {!r}".format(option, section, reference, rawval)) + msg = (f"Bad value substitution: option {option!r} in section " + f"{section!r} contains an interpolation key {reference!r} " + f"which is not a valid option name. Raw value: {rawval!r}") InterpolationError.__init__(self, option, section, msg) self.reference = reference self.args = (option, section, rawval, reference) @@ -293,11 +292,10 @@ class InterpolationDepthError(InterpolationError): """Raised when substitutions are nested too deeply.""" def __init__(self, option, section, rawval): - msg = ("Recursion limit exceeded in value substitution: option {!r} " - "in section {!r} contains an interpolation key which " - "cannot be substituted in {} steps. Raw value: {!r}" - "".format(option, section, MAX_INTERPOLATION_DEPTH, - rawval)) + msg = (f"Recursion limit exceeded in value substitution: option " + f"{option!r} in section {section!r} contains an interpolation " + f"key which cannot be substituted in " + f"{MAX_INTERPOLATION_DEPTH} steps. Raw value: {rawval!r}") InterpolationError.__init__(self, option, section, msg) self.args = (option, section, rawval) @@ -463,7 +461,7 @@ def _interpolate_some(self, parser, option, accum, rest, section, map, m = self._KEYCRE.match(rest) if m is None: raise InterpolationSyntaxError(option, section, - "bad interpolation variable reference %r" % rest) + f"bad interpolation variable reference {rest!r}") var = parser.optionxform(m.group(1)) rest = rest[m.end():] try: @@ -479,8 +477,8 @@ def _interpolate_some(self, parser, option, accum, rest, section, map, else: raise InterpolationSyntaxError( option, section, - "'%%' must be followed by '%%' or '(', " - "found: %r" % (rest,)) + "'%' must be followed by '%' or '(', " + f"found: {rest!r}") class ExtendedInterpolation(Interpolation): @@ -524,7 +522,7 @@ def _interpolate_some(self, parser, option, accum, rest, section, map, m = self._KEYCRE.match(rest) if m is None: raise InterpolationSyntaxError(option, section, - "bad interpolation variable reference %r" % rest) + f"bad interpolation variable reference {rest!r}") path = m.group(1).split(':') rest = rest[m.end():] sect = section @@ -540,7 +538,7 @@ def _interpolate_some(self, parser, option, accum, rest, section, map, else: raise InterpolationSyntaxError( option, section, - "More than one ':' found: %r" % (rest,)) + f"More than one ':' found: {rest!r}") except (KeyError, NoSectionError, NoOptionError): raise InterpolationMissingOptionError( option, section, rawval, ":".join(path)) from None @@ -556,7 +554,7 @@ def _interpolate_some(self, parser, option, accum, rest, section, map, raise InterpolationSyntaxError( option, section, "'$' must be followed by '$' or '{', " - "found: %r" % (rest,)) + f"found: {rest!r}") class _ReadState: @@ -714,7 +712,7 @@ def add_section(self, section): already exists. Raise ValueError if name is DEFAULT. """ if section == self.default_section: - raise ValueError('Invalid section name: %r' % section) + raise ValueError(f'Invalid section name: {section!r}') if section is UNNAMED_SECTION: if not self._allow_unnamed_section: @@ -970,7 +968,7 @@ def write(self, fp, space_around_delimiters=True): preserved when writing the configuration back. """ if space_around_delimiters: - d = " {} ".format(self._delimiters[0]) + d = f" {self._delimiters[0]} " else: d = self._delimiters[0] if self._defaults: @@ -988,7 +986,7 @@ def write(self, fp, space_around_delimiters=True): def _write_section(self, fp, section_name, section_items, delimiter, unnamed=False): """Write a single section to the specified 'fp'.""" if not unnamed: - fp.write("[{}]\n".format(section_name)) + fp.write(f"[{section_name}]\n") for key, value in section_items: self._validate_key_contents(key) value = self._interpolation.before_write(self, section_name, key, @@ -999,7 +997,7 @@ def _write_section(self, fp, section_name, section_items, delimiter, unnamed=Fal .replace('\r', '\n').replace('\n', '\n\t')) else: value = "" - fp.write("{}{}\n".format(key, value)) + fp.write(f"{key}{value}\n") fp.write("\n") def remove_option(self, section, option): @@ -1225,7 +1223,7 @@ def _convert_to_boolean(self, value): """Return a boolean value translating from other types if necessary. """ if value.lower() not in self.BOOLEAN_STATES: - raise ValueError('Not a boolean: %s' % value) + raise ValueError(f'Not a boolean: {value}') return self.BOOLEAN_STATES[value.lower()] def _validate_key_contents(self, key): @@ -1310,7 +1308,7 @@ def __init__(self, parser, name): setattr(self, key, getter) def __repr__(self): - return ''.format(self._name) + return f'' def __getitem__(self, key): if not self._parser.has_option(self._name, key): @@ -1393,8 +1391,7 @@ def __setitem__(self, key, value): try: k = 'get' + key except TypeError: - raise ValueError('Incompatible key: {} (type: {})' - ''.format(key, type(key))) + raise ValueError(f'Incompatible key: {key} (type: {type(key)})') if k == 'get': raise ValueError('Incompatible key: cannot use "" as a name') self._data[key] = value diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 7d1e68fe38100e..bf9b1d7ac97f36 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -293,37 +293,37 @@ def basic_test(self, cf): # and sections are now removed. def test_basic(self): - config_string = """\ + config_string = f"""\ [Foo Bar] -foo{0[0]}bar1 +foo{self.delimiters[0]}bar1 [Spacey Bar] -foo {0[0]} bar2 +foo {self.delimiters[0]} bar2 [Spacey Bar From The Beginning] - foo {0[0]} bar3 - baz {0[0]} qwe + foo {self.delimiters[0]} bar3 + baz {self.delimiters[0]} qwe [Commented Bar] -foo{0[1]} bar4 {1[1]} comment -baz{0[0]}qwe {1[0]}another one +foo{self.delimiters[1]} bar4 {self.comment_prefixes[1]} comment +baz{self.delimiters[0]}qwe {self.comment_prefixes[0]}another one [Long Line] -foo{0[1]} this line is much, much longer than my editor +foo{self.delimiters[1]} this line is much, much longer than my editor likes it. [Section\\with$weird%characters[\t] [Internationalized Stuff] -foo[bg]{0[1]} Bulgarian -foo{0[0]}Default -foo[en]{0[0]}English -foo[de]{0[0]}Deutsch +foo[bg]{self.delimiters[1]} Bulgarian +foo{self.delimiters[0]}Default +foo[en]{self.delimiters[0]}English +foo[de]{self.delimiters[0]}Deutsch [Spaces] -key with spaces {0[1]} value -another with spaces {0[0]} splat! +key with spaces {self.delimiters[1]} value +another with spaces {self.delimiters[0]} splat! [Types] -int {0[1]} 42 -float {0[0]} 0.44 -boolean {0[0]} NO -123 {0[1]} strange but acceptable +int {self.delimiters[1]} 42 +float {self.delimiters[0]} 0.44 +boolean {self.delimiters[0]} NO +123 {self.delimiters[1]} strange but acceptable [This One Has A ] In It] - forks {0[0]} spoons -""".format(self.delimiters, self.comment_prefixes) + forks {self.delimiters[0]} spoons +""" if self.allow_no_value: config_string += ( "[NoValue]\n" @@ -333,31 +333,31 @@ def test_basic(self): self.basic_test(cf) if self.strict: with self.assertRaises(configparser.DuplicateOptionError): - cf.read_string(textwrap.dedent("""\ + cf.read_string(textwrap.dedent(f"""\ [Duplicate Options Here] - option {0[0]} with a value - option {0[1]} with another value - """.format(self.delimiters))) + option {self.delimiters[0]} with a value + option {self.delimiters[1]} with another value + """)) with self.assertRaises(configparser.DuplicateSectionError): - cf.read_string(textwrap.dedent("""\ + cf.read_string(textwrap.dedent(f"""\ [And Now For Something] - completely different {0[0]} True + completely different {self.delimiters[0]} True [And Now For Something] - the larch {0[1]} 1 - """.format(self.delimiters))) + the larch {self.delimiters[1]} 1 + """)) else: - cf.read_string(textwrap.dedent("""\ + cf.read_string(textwrap.dedent(f"""\ [Duplicate Options Here] - option {0[0]} with a value - option {0[1]} with another value - """.format(self.delimiters))) + option {self.delimiters[0]} with a value + option {self.delimiters[1]} with another value + """)) - cf.read_string(textwrap.dedent("""\ + cf.read_string(textwrap.dedent(f"""\ [And Now For Something] - completely different {0[0]} True + completely different {self.delimiters[0]} True [And Now For Something] - the larch {0[1]} 1 - """.format(self.delimiters))) + the larch {self.delimiters[1]} 1 + """)) @support.subTests('data', [ 'foo bar=baz', @@ -491,14 +491,14 @@ def test_case_sensitivity(self): # SF bug #432369: cf = self.fromstring( - "[MySection]\nOption{} first line \n\tsecond line \n".format( - self.delimiters[0])) + f"[MySection]\nOption{self.delimiters[0]} first line \n" + "\tsecond line \n") eq(cf.options("MySection"), ["option"]) eq(cf.get("MySection", "Option"), "first line\nsecond line") # SF bug #561822: cf = self.fromstring("[section]\n" - "nekey{}nevalue\n".format(self.delimiters[0]), + f"nekey{self.delimiters[0]}nevalue\n", defaults={"key":"value"}) self.assertTrue(cf.has_option("section", "Key")) @@ -532,14 +532,14 @@ def test_case_sensitivity_mapping_access(self): # SF bug #432369: cf = self.fromstring( - "[MySection]\nOption{} first line \n\tsecond line \n".format( - self.delimiters[0])) + f"[MySection]\nOption{self.delimiters[0]} first line \n" + "\tsecond line \n") 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]), + f"nekey{self.delimiters[0]}nevalue\n", defaults={"key":"value"}) self.assertTrue("Key" in cf["section"]) @@ -568,10 +568,10 @@ def test_parse_errors(self): cf = self.newconfig() self.parse_error(cf, configparser.ParsingError, "[Foo]\n" - "{}val-without-opt-name\n".format(self.delimiters[0])) + f"{self.delimiters[0]}val-without-opt-name\n") self.parse_error(cf, configparser.ParsingError, "[Foo]\n" - "{}val-without-opt-name\n".format(self.delimiters[1])) + f"{self.delimiters[1]}val-without-opt-name\n") e = self.parse_error(cf, configparser.MissingSectionHeaderError, "No Section!\n") self.assertEqual(e.args, ('', 1, "No Section!\n")) @@ -624,27 +624,27 @@ def get_error(self, cf, exc, section, option): except exc as e: return e else: - self.fail("expected exception type %s.%s" - % (exc.__module__, exc.__qualname__)) + self.fail( + f"expected exception type {exc.__module__}.{exc.__qualname__}") def test_boolean(self): cf = self.fromstring( "[BOOLTEST]\n" - "T1{equals}1\n" - "T2{equals}TRUE\n" - "T3{equals}True\n" - "T4{equals}oN\n" - "T5{equals}yes\n" - "F1{equals}0\n" - "F2{equals}FALSE\n" - "F3{equals}False\n" - "F4{equals}oFF\n" - "F5{equals}nO\n" - "E1{equals}2\n" - "E2{equals}foo\n" - "E3{equals}-1\n" - "E4{equals}0.1\n" - "E5{equals}FALSE AND MORE".format(equals=self.delimiters[0]) + f"T1{self.delimiters[0]}1\n" + f"T2{self.delimiters[0]}TRUE\n" + f"T3{self.delimiters[0]}True\n" + f"T4{self.delimiters[0]}oN\n" + f"T5{self.delimiters[0]}yes\n" + f"F1{self.delimiters[0]}0\n" + f"F2{self.delimiters[0]}FALSE\n" + f"F3{self.delimiters[0]}False\n" + f"F4{self.delimiters[0]}oFF\n" + f"F5{self.delimiters[0]}nO\n" + f"E1{self.delimiters[0]}2\n" + f"E2{self.delimiters[0]}foo\n" + f"E3{self.delimiters[0]}-1\n" + f"E4{self.delimiters[0]}0.1\n" + f"E5{self.delimiters[0]}FALSE AND MORE" ) for x in range(1, 5): self.assertTrue(cf.getboolean('BOOLTEST', 't%d' % x)) @@ -701,18 +701,19 @@ def test_get_after_duplicate_option_error(self): def test_write(self): config_string = ( "[Long Line]\n" - "foo{0[0]} this line is much, much longer than my editor\n" + f"foo{self.delimiters[0]} this line is much, much longer " + "than my editor\n" " likes it.\n" - "[{default_section}]\n" - "foo{0[1]} another very\n" + f"[{self.default_section}]\n" + f"foo{self.delimiters[1]} another very\n" " long line\n" "[Long Line - With Comments!]\n" - "test {0[1]} we {comment} can\n" - " also {comment} place\n" - " comments {comment} in\n" - " multiline {comment} values" - "\n".format(self.delimiters, comment=self.comment_prefixes[0], - default_section=self.default_section) + f"test {self.delimiters[1]} we " + f"{self.comment_prefixes[0]} can\n" + f" also {self.comment_prefixes[0]} place\n" + f" comments {self.comment_prefixes[0]} in\n" + f" multiline {self.comment_prefixes[0]} values" + "\n" ) if self.allow_no_value: config_string += ( @@ -726,23 +727,23 @@ def test_write(self): cf.write(output, space_around_delimiters=space_around_delimiters) delimiter = self.delimiters[0] if space_around_delimiters: - delimiter = " {} ".format(delimiter) + delimiter = f" {delimiter} " expect_string = ( - "[{default_section}]\n" - "foo{equals}another very\n" + f"[{self.default_section}]\n" + f"foo{delimiter}another very\n" "\tlong line\n" "\n" "[Long Line]\n" - "foo{equals}this line is much, much longer than my editor\n" + f"foo{delimiter}this line is much, much longer " + "than my editor\n" "\tlikes it.\n" "\n" "[Long Line - With Comments!]\n" - "test{equals}we\n" + f"test{delimiter}we\n" "\talso\n" "\tcomments\n" "\tmultiline\n" - "\n".format(equals=delimiter, - default_section=self.default_section) + "\n" ) if self.allow_no_value: expect_string += ( @@ -754,7 +755,7 @@ def test_write(self): def test_set_string_types(self): cf = self.fromstring("[sect]\n" - "option1{eq}foo\n".format(eq=self.delimiters[0])) + f"option1{self.delimiters[0]}foo\n") # Check that we don't get an exception when setting values in # an existing section using strings: class mystr(str): @@ -820,37 +821,41 @@ def test_read_returns_file_list_with_bytestring_path(self): def get_interpolation_config(self): return self.fromstring( "[Foo]\n" - "bar{equals}something %(with1)s interpolation (1 step)\n" - "bar9{equals}something %(with9)s lots of interpolation (9 steps)\n" - "bar10{equals}something %(with10)s lots of interpolation (10 steps)\n" - "bar11{equals}something %(with11)s lots of interpolation (11 steps)\n" - "with11{equals}%(with10)s\n" - "with10{equals}%(with9)s\n" - "with9{equals}%(with8)s\n" - "with8{equals}%(With7)s\n" - "with7{equals}%(WITH6)s\n" - "with6{equals}%(with5)s\n" - "With5{equals}%(with4)s\n" - "WITH4{equals}%(with3)s\n" - "with3{equals}%(with2)s\n" - "with2{equals}%(with1)s\n" - "with1{equals}with\n" + f"bar{self.delimiters[0]}something %(with1)s " + "interpolation (1 step)\n" + f"bar9{self.delimiters[0]}something %(with9)s " + "lots of interpolation (9 steps)\n" + f"bar10{self.delimiters[0]}something %(with10)s " + "lots of interpolation (10 steps)\n" + f"bar11{self.delimiters[0]}something %(with11)s " + "lots of interpolation (11 steps)\n" + f"with11{self.delimiters[0]}%(with10)s\n" + f"with10{self.delimiters[0]}%(with9)s\n" + f"with9{self.delimiters[0]}%(with8)s\n" + f"with8{self.delimiters[0]}%(With7)s\n" + f"with7{self.delimiters[0]}%(WITH6)s\n" + f"with6{self.delimiters[0]}%(with5)s\n" + f"With5{self.delimiters[0]}%(with4)s\n" + f"WITH4{self.delimiters[0]}%(with3)s\n" + f"with3{self.delimiters[0]}%(with2)s\n" + f"with2{self.delimiters[0]}%(with1)s\n" + f"with1{self.delimiters[0]}with\n" "\n" "[Mutual Recursion]\n" - "foo{equals}%(bar)s\n" - "bar{equals}%(foo)s\n" + f"foo{self.delimiters[0]}%(bar)s\n" + f"bar{self.delimiters[0]}%(foo)s\n" "\n" "[Interpolation Error]\n" # no definition for 'reference' - "name{equals}%(reference)s\n".format(equals=self.delimiters[0])) + f"name{self.delimiters[0]}%(reference)s\n") def check_items_config(self, expected): - cf = self.fromstring(""" + cf = self.fromstring(f""" [section] - name {0[0]} %(value)s - key{0[1]} |%(name)s| - getdefault{0[1]} |%(default)s| - """.format(self.delimiters), defaults={"default": ""}) + name {self.delimiters[0]} %(value)s + key{self.delimiters[1]} |%(name)s| + getdefault{self.delimiters[1]} |%(default)s| + """, defaults={"default": ""}) L = list(cf.items("section", vars={'value': 'value'})) L.sort() self.assertEqual(L, expected) @@ -858,14 +863,14 @@ def check_items_config(self, expected): cf.items("no such section") def test_popitem(self): - cf = self.fromstring(""" + cf = self.fromstring(f""" [section1] - name1 {0[0]} value1 + name1 {self.delimiters[0]} value1 [section2] - name2 {0[0]} value2 + name2 {self.delimiters[0]} value2 [section3] - name3 {0[0]} value3 - """.format(self.delimiters), defaults={"default": ""}) + name3 {self.delimiters[0]} value3 + """, defaults={"default": ""}) self.assertEqual(cf.popitem()[0], 'section1') self.assertEqual(cf.popitem()[0], 'section2') self.assertEqual(cf.popitem()[0], 'section3') @@ -885,14 +890,14 @@ def test_clear(self): self.assertEqual(set(cf[self.default_section].keys()), {'foo'}) def test_setitem(self): - cf = self.fromstring(""" + cf = self.fromstring(f""" [section1] - name1 {0[0]} value1 + name1 {self.delimiters[0]} value1 [section2] - name2 {0[0]} value2 + name2 {self.delimiters[0]} value2 [section3] - name3 {0[0]} value3 - """.format(self.delimiters), defaults={"nameD": "valueD"}) + name3 {self.delimiters[0]} value3 + """, defaults={"nameD": "valueD"}) self.assertEqual(set(cf['section1'].keys()), {'name1', 'named'}) self.assertEqual(set(cf['section2'].keys()), {'name2', 'named'}) self.assertEqual(set(cf['section3'].keys()), {'name3', 'named'}) @@ -926,10 +931,10 @@ def test_invalid_multiline_value(self): if self.allow_no_value: self.skipTest('if no_value is allowed, ParsingError is not raised') - invalid = textwrap.dedent("""\ + invalid = textwrap.dedent(f"""\ [DEFAULT] - test {0} test - invalid""".format(self.delimiters[0]) + test {self.delimiters[0]} test + invalid""" ) cf = self.newconfig() with self.assertRaises(configparser.ParsingError): @@ -979,18 +984,17 @@ def test_items(self): def test_safe_interpolation(self): # See https://bugs.python.org/issue511737 cf = self.fromstring("[section]\n" - "option1{eq}xxx\n" - "option2{eq}%(option1)s/xxx\n" - "ok{eq}%(option1)s/%%s\n" - "not_ok{eq}%(option2)s/%%s".format( - eq=self.delimiters[0])) + f"option1{self.delimiters[0]}xxx\n" + f"option2{self.delimiters[0]}%(option1)s/xxx\n" + f"ok{self.delimiters[0]}%(option1)s/%%s\n" + f"not_ok{self.delimiters[0]}%(option2)s/%%s") self.assertEqual(cf.get("section", "ok"), "xxx/%s") if self.interpolation == configparser._UNSET: self.assertEqual(cf.get("section", "not_ok"), "xxx/xxx/%s") def test_set_malformatted_interpolation(self): cf = self.fromstring("[sect]\n" - "option1{eq}foo\n".format(eq=self.delimiters[0])) + f"option1{self.delimiters[0]}foo\n") self.assertEqual(cf.get('sect', "option1"), "foo") @@ -1006,7 +1010,7 @@ def test_set_malformatted_interpolation(self): def test_set_nonstring_types(self): cf = self.fromstring("[sect]\n" - "option1{eq}foo\n".format(eq=self.delimiters[0])) + f"option1{self.delimiters[0]}foo\n") # Check that we get a TypeError when setting non-string values # in an existing section: self.assertRaises(TypeError, cf.set, "sect", "option1", 1) @@ -1094,10 +1098,10 @@ class MultilineValuesTestCase(BasicTestCase, unittest.TestCase): def setUp(self): cf = self.newconfig() for i in range(100): - s = 'section{}'.format(i) + s = f'section{i}' cf.add_section(s) for j in range(10): - cf.set(s, 'lovely_spam{}'.format(j), self.wonderful_spam) + cf.set(s, f'lovely_spam{j}', self.wonderful_spam) with open(os_helper.TESTFN, 'w', encoding="utf-8") as f: cf.write(f) From 3a49bcb23e4db61049156b4bb2e7c8e748e40496 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Oct 2026 15:47:55 -0400 Subject: [PATCH 2/5] 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 --- Lib/configparser.py | 16 +++++++--------- Lib/test/test_configparser.py | 18 +++++++++--------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index 6965c277634ffe..2aed6703aed068 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -568,7 +568,7 @@ class _ReadState: def __init__(self): self.elements_added = set() - self.errors = list() + self.errors = [] class _Line(str): @@ -714,9 +714,8 @@ def add_section(self, section): if section == self.default_section: raise ValueError(f'Invalid section name: {section!r}') - if section is UNNAMED_SECTION: - if not self._allow_unnamed_section: - raise UnnamedSectionDisabledError + if section is UNNAMED_SECTION and not self._allow_unnamed_section: + raise UnnamedSectionDisabledError if section in self._sections: raise DuplicateSectionError(section) @@ -974,7 +973,7 @@ def write(self, fp, space_around_delimiters=True): if self._defaults: self._write_section(fp, self.default_section, self._defaults.items(), d) - if UNNAMED_SECTION in self._sections and self._sections[UNNAMED_SECTION]: + if self._sections.get(UNNAMED_SECTION): self._write_section(fp, UNNAMED_SECTION, self._sections[UNNAMED_SECTION].items(), d, unnamed=True) for section in self._sections: @@ -1163,7 +1162,7 @@ def _handle_option(self, st, line, fpname): st.errors.append(ParsingError(fpname, st.lineno, line)) return - st.optname, vi, optval = mo.group('option', 'vi', 'value') + st.optname, optval = mo.group('option', 'value') if not st.optname: st.errors.append(ParsingError(fpname, st.lineno, line)) st.optname = self.optionxform(st.optname.rstrip()) @@ -1254,9 +1253,8 @@ def _validate_value_types(self, *, section="", option="", value=""): raise TypeError("section names must be strings or UNNAMED_SECTION") if not isinstance(option, str): raise TypeError("option keys must be strings") - if not self._allow_no_value or value: - if not isinstance(value, str): - raise TypeError("option values must be strings") + if (not self._allow_no_value or value) and not isinstance(value, str): + raise TypeError("option values must be strings") @property def converters(self): diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index bf9b1d7ac97f36..0a4917ee4846cf 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -102,10 +102,10 @@ def basic_test(self, cf): E.sort() eq(L, E) L = cf['Spacey Bar From The Beginning'].items() - L = sorted(list(L)) + L = sorted(L) eq(L, F) L = cf.items() - L = sorted(list(L)) + L = sorted(L) self.assertEqual(len(L), len(E)) for name, section in L: eq(name, section.name) @@ -1074,9 +1074,8 @@ class CustomConfigParser(configparser.ConfigParser): class ConfigParserTestCaseInvalidInterpolationType(unittest.TestCase): def test_error_on_wrong_type_for_interpolation(self): for value in [configparser.ExtendedInterpolation, 42, "a string"]: - with self.subTest(value=value): - with self.assertRaises(TypeError): - configparser.ConfigParser(interpolation=value) + with self.subTest(value=value), self.assertRaises(TypeError): + configparser.ConfigParser(interpolation=value) class ConfigParserTestCaseNonStandardDelimiters(ConfigParserTestCase): @@ -1423,8 +1422,8 @@ def test_cfgparser_dot_3(self): self.assertEqual(len(cf.read(tricky, encoding='utf-8')), 1) self.assertEqual(cf.sections(), ['strange', 'corruption', - 'yeah, sections can be ' - 'indented as well', + ('yeah, sections can be ' + 'indented as well'), 'another one!', 'no values here', 'tricky interpolation', @@ -1698,7 +1697,7 @@ def test_interpolation_depth_error(self): self.assertEqual(error.section, 'section') def test_parsing_error(self): - with self.assertRaises(TypeError) as cm: + with self.assertRaises(TypeError): configparser.ParsingError() error = configparser.ParsingError(source='source') self.assertEqual(error.source, 'source') @@ -2225,7 +2224,8 @@ def test_no_first_section(self): c = 3 """) - self.assertEqual(set([configparser.UNNAMED_SECTION, 'sect1']), set(cfg1.sections())) + self.assertEqual({configparser.UNNAMED_SECTION, 'sect1'}, + set(cfg1.sections())) self.assertEqual('1', cfg1[configparser.UNNAMED_SECTION]['a']) self.assertEqual('2', cfg1[configparser.UNNAMED_SECTION]['b']) self.assertEqual('3', cfg1['sect1']['c']) From a63f02da461a401ae36b11ec469de50d3cb041e6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Oct 2026 15:48:55 -0400 Subject: [PATCH 3/5] 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 --- Lib/configparser.py | 15 +++++++-------- Lib/test/test_configparser.py | 6 +++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index 2aed6703aed068..04c199fd060d13 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -341,8 +341,8 @@ class MissingSectionHeaderError(ParsingError): def __init__(self, filename, lineno, line): Error.__init__( self, - 'File contains no section headers.\nfile: %r, line: %d\n%r' % - (filename, lineno, line)) + 'File contains no section headers.\n' + f'file: {filename!r}, line: {lineno:d}\n{line!r}') self.source = filename self.lineno = lineno self.line = line @@ -355,8 +355,7 @@ def __init__(self, filename, lineno, line): Error.__init__( self, "Key without value continued with an indented line.\n" - "file: %r, line: %d\n%r" - %(filename, lineno, line)) + f"file: {filename!r}, line: {lineno:d}\n{line!r}") self.source = filename self.lineno = lineno self.line = line @@ -435,8 +434,8 @@ def before_set(self, parser, section, option, value): tmp_value = value.replace('%%', '') # escaped percent signs tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax if '%' in tmp_value: - raise ValueError("invalid interpolation syntax in %r at " - "position %d" % (value, tmp_value.find('%'))) + raise ValueError(f"invalid interpolation syntax in {value!r} " + f"at position {tmp_value.find('%'):d}") return value def _interpolate_some(self, parser, option, accum, rest, section, map, @@ -496,8 +495,8 @@ def before_set(self, parser, section, option, value): tmp_value = value.replace('$$', '') # escaped dollar signs tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax if '$' in tmp_value: - raise ValueError("invalid interpolation syntax in %r at " - "position %d" % (value, tmp_value.find('$'))) + raise ValueError(f"invalid interpolation syntax in {value!r} " + f"at position {tmp_value.find('$'):d}") return value def _interpolate_some(self, parser, option, accum, rest, section, map, diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 0a4917ee4846cf..d1f80ac5ce6dae 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -647,10 +647,10 @@ def test_boolean(self): f"E5{self.delimiters[0]}FALSE AND MORE" ) for x in range(1, 5): - self.assertTrue(cf.getboolean('BOOLTEST', 't%d' % x)) - self.assertFalse(cf.getboolean('BOOLTEST', 'f%d' % x)) + self.assertTrue(cf.getboolean('BOOLTEST', f't{x}')) + self.assertFalse(cf.getboolean('BOOLTEST', f'f{x}')) self.assertRaises(ValueError, - cf.getboolean, 'BOOLTEST', 'e%d' % x) + cf.getboolean, 'BOOLTEST', f'e{x}') def test_weird_errors(self): cf = self.newconfig() From 105bddee96ed78575b7058ce7afbe4ef403005d9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Oct 2026 15:57:48 -0400 Subject: [PATCH 4/5] 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 --- Lib/configparser.py | 59 +++++++++++++++++++++++++---------- Lib/test/test_configparser.py | 8 +++-- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index 04c199fd060d13..00e072d98eef2c 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -143,10 +143,10 @@ between keys and values are surrounded by spaces. """ -# Do not import dataclasses; overhead is unacceptable (gh-117703) +# Defer annotations so that typing need not be imported at runtime. +from __future__ import annotations -from collections.abc import Iterable, MutableMapping -from collections import ChainMap as _ChainMap +# Do not import dataclasses; overhead is unacceptable (gh-117703) import contextlib import functools import io @@ -154,16 +154,39 @@ import os import re import sys +from collections import ChainMap as _ChainMap +from collections.abc import Iterable, MutableMapping -__all__ = ("NoSectionError", "DuplicateOptionError", "DuplicateSectionError", - "NoOptionError", "InterpolationError", "InterpolationDepthError", - "InterpolationMissingOptionError", "InterpolationSyntaxError", - "ParsingError", "MissingSectionHeaderError", - "MultilineContinuationError", "UnnamedSectionDisabledError", - "InvalidWriteError", "ConfigParser", "RawConfigParser", - "Interpolation", "BasicInterpolation", "ExtendedInterpolation", - "SectionProxy", "ConverterMapping", - "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION") +# Do not import typing either; it is only needed for annotations. +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import ClassVar + +__all__ = ( + "DEFAULTSECT", + "MAX_INTERPOLATION_DEPTH", + "UNNAMED_SECTION", + "BasicInterpolation", + "ConfigParser", + "ConverterMapping", + "DuplicateOptionError", + "DuplicateSectionError", + "ExtendedInterpolation", + "Interpolation", + "InterpolationDepthError", + "InterpolationError", + "InterpolationMissingOptionError", + "InterpolationSyntaxError", + "InvalidWriteError", + "MissingSectionHeaderError", + "MultilineContinuationError", + "NoOptionError", + "NoSectionError", + "ParsingError", + "RawConfigParser", + "SectionProxy", + "UnnamedSectionDisabledError", +) _default_dict = dict DEFAULTSECT = "DEFAULT" @@ -325,7 +348,7 @@ def combine(self, others): return self @staticmethod - def _raise_all(exceptions: Iterable['ParsingError']): + def _raise_all(exceptions: Iterable[ParsingError]): """ Combine any number of ParsingErrors into one and raise it. """ @@ -499,8 +522,8 @@ def before_set(self, parser, section, option, value): f"at position {tmp_value.find('$'):d}") return value - def _interpolate_some(self, parser, option, accum, rest, section, map, - depth): + def _interpolate_some(self, parser, option, # noqa: C901 # gh-159686 + accum, rest, section, map, depth): rawval = parser.get(section, option, raw=True, fallback=rest) if depth > MAX_INTERPOLATION_DEPTH: raise InterpolationDepthError(option, section, rawval) @@ -647,8 +670,10 @@ class RawConfigParser(MutableMapping): # Compiled regular expression for matching leading whitespace in a line NONSPACECRE = re.compile(r"\S") # Possible boolean values in the configuration. - BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True, - '0': False, 'no': False, 'false': False, 'off': False} + BOOLEAN_STATES: ClassVar[dict[str, bool]] = { + '1': True, 'yes': True, 'true': True, 'on': True, + '0': False, 'no': False, 'false': False, 'off': False, + } def __init__(self, defaults=None, dict_type=_default_dict, allow_no_value=False, *, delimiters=('=', ':'), diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index d1f80ac5ce6dae..82a2272be1ed6a 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -4,6 +4,7 @@ import os import textwrap import unittest +from typing import ClassVar from test import support from test.support import os_helper @@ -1412,8 +1413,8 @@ class RawConfigParserNoValueAndExtendedInterpolationTest( class ConfigParserTestCaseTrickyFile(CfgParserTestCaseClass, unittest.TestCase): config_class = configparser.ConfigParser - delimiters = {'='} - comment_prefixes = {'#'} + delimiters: ClassVar[set[str]] = {'='} + comment_prefixes: ClassVar[set[str]] = {'#'} allow_no_value = True def test_cfgparser_dot_3(self): @@ -2347,7 +2348,8 @@ def test_option_regex_no_value_does_not_backtrack(self): class MiscTestCase(unittest.TestCase): def test__all__(self): - support.check__all__(self, configparser, not_exported={"Error"}) + support.check__all__(self, configparser, + not_exported={"Error", "TYPE_CHECKING"}) if __name__ == '__main__': From c45a20e8427a9aa57c9bc03c7fbbe57c9637c1bb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Oct 2026 16:09:29 -0400 Subject: [PATCH 5/5] gh-159684: Add a NEWS entry Co-Authored-By: Claude Opus 5 --- .../Library/2026-10-18-16-09-24.gh-issue-159684.VHVYjVuE.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-18-16-09-24.gh-issue-159684.VHVYjVuE.rst diff --git a/Misc/NEWS.d/next/Library/2026-10-18-16-09-24.gh-issue-159684.VHVYjVuE.rst b/Misc/NEWS.d/next/Library/2026-10-18-16-09-24.gh-issue-159684.VHVYjVuE.rst new file mode 100644 index 00000000000000..43f648af4784d9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-18-16-09-24.gh-issue-159684.VHVYjVuE.rst @@ -0,0 +1,3 @@ +Update :mod:`configparser` and its tests to modern linter standards: +printf-style and :meth:`str.format` call sites are now f-strings, and a +handful of constructs have simpler spellings. No behavior change.