diff --git a/Lib/configparser.py b/Lib/configparser.py index 88015ef6086569..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" @@ -190,7 +213,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 +231,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 +257,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 +275,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 +295,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 +315,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) @@ -327,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. """ @@ -343,8 +364,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 @@ -357,8 +378,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 @@ -437,8 +457,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, @@ -463,7 +483,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 +499,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): @@ -498,12 +518,12 @@ 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, - 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) @@ -524,7 +544,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 +560,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 +576,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: @@ -570,7 +590,7 @@ class _ReadState: def __init__(self): self.elements_added = set() - self.errors = list() + self.errors = [] class _Line(str): @@ -650,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=('=', ':'), @@ -714,11 +736,10 @@ 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: - raise UnnamedSectionDisabledError + if section is UNNAMED_SECTION and not self._allow_unnamed_section: + raise UnnamedSectionDisabledError if section in self._sections: raise DuplicateSectionError(section) @@ -970,13 +991,13 @@ 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: 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: @@ -988,7 +1009,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 +1020,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): @@ -1165,7 +1186,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()) @@ -1225,7 +1246,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): @@ -1256,9 +1277,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): @@ -1310,7 +1330,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 +1413,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..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 @@ -102,10 +103,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) @@ -293,37 +294,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 +334,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 +492,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 +533,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 +569,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,33 +625,33 @@ 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)) - 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() @@ -701,18 +702,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 +728,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 +756,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 +822,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 +864,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 +891,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 +932,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 +985,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 +1011,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) @@ -1070,9 +1075,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): @@ -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) @@ -1409,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): @@ -1419,8 +1423,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', @@ -1694,7 +1698,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') @@ -2221,7 +2225,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']) @@ -2343,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__': 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.