diff --git a/Lib/configparser.py b/Lib/configparser.py index 239fda60a02ca0..c738de2f385b5d 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 @@ -690,6 +691,7 @@ def __init__(self, defaults=None, dict_type=_default_dict, self._read_defaults(defaults) self._allow_unnamed_section = allow_unnamed_section + def defaults(self): return self._defaults @@ -752,6 +754,7 @@ def read(self, filenames, encoding=None): try: with open(filename, encoding=encoding) as fp: self._read(fp, filename) + self._loaded_sources.append(filename) except OSError: continue if isinstance(filename, os.PathLike): @@ -773,11 +776,13 @@ 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.""" sfile = io.StringIO(string) self.read_file(sfile, source) + self._loaded_sources.append(source) def read_dict(self, dictionary, source=''): """Read configuration from a dictionary. @@ -809,6 +814,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 +1054,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: {key: value for key, value in 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} + state_summary = { + "loaded_sources": self._loaded_sources, + "sections_count": len(self._sections), + "sections": list(self._sections.keys())[:5], # Limit to 5 section names for readability + } + + if len(self._sections) > 5: + state_summary["sections_truncated"] = f"...and {len(self._sections) - 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..40863d3e0f2c4f 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -980,6 +980,39 @@ 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_and_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") + + + expected_str = ( + "" + ) + self.assertEqual(str(cf), expected_str) + + + dict_type = type(cf._dict).__name__ + + expected_repr = ( + f"<{cf.__class__.__name__}(" + f"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'})>" + ) + self.assertEqual(repr(cf), expected_repr) + 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..61ddc9fcce5836 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1599,6 +1599,7 @@ Erik Rose Mark Roseman Josh Rosenberg Jim Roskind +Prince Roshan Brian Rosner Ignacio Rossi Guido van Rossum diff --git a/Misc/NEWS.d/next/Library/2024-11-23-13-34-55.gh-issue-127011.dT88uF.rst b/Misc/NEWS.d/next/Library/2024-11-23-13-34-55.gh-issue-127011.dT88uF.rst new file mode 100644 index 00000000000000..78d3e7ec7742b9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-11-23-13-34-55.gh-issue-127011.dT88uF.rst @@ -0,0 +1 @@ +The ``__str__`` and ``__repr__`` methods have been added to the :class:`configparser.RawConfigParser`