diff --git a/Lib/configparser.py b/Lib/configparser.py index 239fda60a02ca0..e41e082d45f92a 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -670,6 +670,7 @@ def __init__(self, defaults=None, dict_type=_default_dict, self._optcre = re.compile(self._OPT_TMPL.format(delim=d), re.VERBOSE) self._comments = _CommentSpec(comment_prefixes or (), inline_comment_prefixes or ()) + self._loaded_sources = [] self._strict = strict self._allow_no_value = allow_no_value self._empty_lines_in_values = empty_lines_in_values @@ -757,6 +758,7 @@ def read(self, filenames, encoding=None): if isinstance(filename, os.PathLike): filename = os.fspath(filename) read_ok.append(filename) + self._loaded_sources.append(read_ok) return read_ok def read_file(self, f, source=None): @@ -773,6 +775,7 @@ def read_file(self, f, source=None): except AttributeError: source = '' self._read(f, source) + self._loaded_sources.append(source) def read_string(self, string, source=''): """Read configuration from a given string.""" @@ -809,6 +812,7 @@ def read_dict(self, dictionary, source=''): raise DuplicateOptionError(section, key, source) elements_added.add((section, key)) self.set(section, key, value) + self._loaded_sources.append(source) def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET): """Get an option value for a given section. @@ -1048,6 +1052,38 @@ def __iter__(self): # XXX does it break when underlying container state changed? return itertools.chain((self.default_section,), self._sections.keys()) + def __str__(self): + config_dict = { + section: dict(self.items(section, raw=True)) + for section in self.sections() + } + return f"" + + def __repr__(self): + init_params = { + "defaults": self._defaults if self._defaults else None, + "dict_type": type(self._dict).__name__, + "allow_no_value": self._allow_no_value, + "delimiters": self._delimiters, + "strict": self._strict, + "default_section": self.default_section, + "interpolation": type(self._interpolation).__name__, + } + init_params = {k: v for k, v in init_params.items() if v is not None} + sections_count = len(self._sections) + state_summary = { + "loaded_sources": self._loaded_sources, + "sections_count": sections_count, + "sections": list(self._sections)[:5], # limit to 5 section names for readability + } + + if sections_count > 5: + state_summary["sections_truncated"] = f"...and {sections_count - 5} more" + + return (f"<{self.__class__.__name__}(" + f"params={init_params}, " + f"state={state_summary})>") + def _read(self, fp, fpname): """Parse a sectioned configuration file. diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 23904d17d326d8..cfd9b59fe0ab4d 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -980,6 +980,50 @@ def test_set_nonstring_types(self): self.assertRaises(TypeError, cf.set, "sect", 123, "invalid opt name!") self.assertRaises(TypeError, cf.add_section, 123) + def test_str(self): + self.maxDiff = None + cf = self.config_class(allow_no_value=True, delimiters=('=',), strict=True) + cf.add_section("sect1") + cf.add_section("sect2") + cf.set("sect1", "option1", "foo") + cf.set("sect2", "option2", "bar") + + expected_str = ( + "" + ) + self.assertEqual(str(cf), expected_str) + + def test_repr(self): + self.maxDiff = None + cf = self.config_class(allow_no_value=True, delimiters=('=',), strict=True) + cf.add_section("sect1") + cf.add_section("sect2") + cf.add_section("sect3") + cf.add_section("sect4") + cf.add_section("sect5") + cf.add_section("sect6") + cf.set("sect1", "option1", "foo") + cf.set("sect2", "option2", "bar") + cf.read_string("") # to trigger the loading of sources + + dict_type = type(cf._dict).__name__ + params = { + 'dict_type': dict_type, + 'allow_no_value': True, + 'delimiters': ('=',), + 'strict': True, + 'default_section': 'DEFAULT', + 'interpolation': 'BasicInterpolation', + } + state = { + 'loaded_sources': [''], + 'sections_count': 6, + 'sections': ['sect1', 'sect2', 'sect3', 'sect4', 'sect5'], + 'sections_truncated': '...and 1 more', + } + expected = f"<{type(cf).__name__}({params=}, {state=})>" + self.assertEqual(repr(cf), expected) + def test_add_section_default(self): cf = self.newconfig() self.assertRaises(ValueError, cf.add_section, self.default_section) diff --git a/Misc/ACKS b/Misc/ACKS index 42068ec6aefbd2..9fef76f6d06bf8 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1598,6 +1598,7 @@ Joel Rosdahl Erik Rose Mark Roseman Josh Rosenberg +Prince Roshan Jim Roskind Brian Rosner Ignacio Rossi diff --git a/Misc/NEWS.d/next/Library/2025-04-25-18-29-10.gh-issue-127011.Ipem5z.rst b/Misc/NEWS.d/next/Library/2025-04-25-18-29-10.gh-issue-127011.Ipem5z.rst new file mode 100644 index 00000000000000..f98b5f36d59585 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-25-18-29-10.gh-issue-127011.Ipem5z.rst @@ -0,0 +1,2 @@ +Implement :meth:`~object.__str__` and :meth:`~object.__repr__` +for :class:`configparser.RawConfigParser` objects.