Skip to content

Commit 4d10dea

Browse files
committed
gh-127011: Add __str__ and __repr__ to ConfigParser
1 parent ac5424d commit 4d10dea

File tree

3 files changed

+73
-0
lines changed

3 files changed

+73
-0
lines changed

Lib/configparser.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@ def __init__(self, defaults=None, dict_type=_default_dict,
670670
self._optcre = re.compile(self._OPT_TMPL.format(delim=d),
671671
re.VERBOSE)
672672
self._comments = _CommentSpec(comment_prefixes or (), inline_comment_prefixes or ())
673+
self._loaded_sources = []
673674
self._strict = strict
674675
self._allow_no_value = allow_no_value
675676
self._empty_lines_in_values = empty_lines_in_values
@@ -690,6 +691,7 @@ def __init__(self, defaults=None, dict_type=_default_dict,
690691
self._read_defaults(defaults)
691692
self._allow_unnamed_section = allow_unnamed_section
692693

694+
693695
def defaults(self):
694696
return self._defaults
695697

@@ -752,6 +754,7 @@ def read(self, filenames, encoding=None):
752754
try:
753755
with open(filename, encoding=encoding) as fp:
754756
self._read(fp, filename)
757+
self._loaded_sources.append(filename)
755758
except OSError:
756759
continue
757760
if isinstance(filename, os.PathLike):
@@ -773,11 +776,13 @@ def read_file(self, f, source=None):
773776
except AttributeError:
774777
source = '<???>'
775778
self._read(f, source)
779+
self._loaded_sources.append(source)
776780

777781
def read_string(self, string, source='<string>'):
778782
"""Read configuration from a given string."""
779783
sfile = io.StringIO(string)
780784
self.read_file(sfile, source)
785+
self._loaded_sources.append(source)
781786

782787
def read_dict(self, dictionary, source='<dict>'):
783788
"""Read configuration from a dictionary.
@@ -809,6 +814,7 @@ def read_dict(self, dictionary, source='<dict>'):
809814
raise DuplicateOptionError(section, key, source)
810815
elements_added.add((section, key))
811816
self.set(section, key, value)
817+
self._loaded_sources.append(source)
812818

813819
def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
814820
"""Get an option value for a given section.
@@ -1048,6 +1054,39 @@ def __iter__(self):
10481054
# XXX does it break when underlying container state changed?
10491055
return itertools.chain((self.default_section,), self._sections.keys())
10501056

1057+
def __str__(self):
1058+
config_dict = {
1059+
section: {key: value for key, value in self.items(section, raw=True)}
1060+
for section in self.sections()
1061+
}
1062+
return f"<ConfigParser: {config_dict}>"
1063+
1064+
1065+
def __repr__(self):
1066+
init_params = {
1067+
"defaults": self._defaults if self._defaults else None,
1068+
"dict_type": type(self._dict).__name__,
1069+
"allow_no_value": self._allow_no_value,
1070+
"delimiters": self._delimiters,
1071+
"strict": self._strict,
1072+
"default_section": self.default_section,
1073+
"interpolation": type(self._interpolation).__name__,
1074+
}
1075+
init_params = {k: v for k, v in init_params.items() if v is not None}
1076+
state_summary = {
1077+
"loaded_sources": self._loaded_sources,
1078+
"sections_count": len(self._sections),
1079+
"sections": list(self._sections.keys())[:5], # Limit to 5 section names for readability
1080+
}
1081+
1082+
if len(self._sections) > 5:
1083+
state_summary["sections_truncated"] = f"...and {len(self._sections) - 5} more"
1084+
1085+
return (f"<{self.__class__.__name__}("
1086+
f"params={init_params}, "
1087+
f"state={state_summary})>")
1088+
1089+
10511090
def _read(self, fp, fpname):
10521091
"""Parse a sectioned configuration file.
10531092

Lib/test/test_configparser.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,39 @@ def test_set_nonstring_types(self):
980980
self.assertRaises(TypeError, cf.set, "sect", 123, "invalid opt name!")
981981
self.assertRaises(TypeError, cf.add_section, 123)
982982

983+
def test_str_and_repr(self):
984+
self.maxDiff = None
985+
cf = self.config_class(allow_no_value=True, delimiters=('=',), strict=True)
986+
cf.add_section("sect1")
987+
cf.add_section("sect2")
988+
cf.add_section("sect3")
989+
cf.add_section("sect4")
990+
cf.add_section("sect5")
991+
cf.add_section("sect6")
992+
cf.set("sect1", "option1", "foo")
993+
cf.set("sect2", "option2", "bar")
994+
995+
996+
expected_str = (
997+
"<ConfigParser: {'sect1': {'option1': 'foo'}, 'sect2': {'option2': 'bar'}, "
998+
"'sect3': {}, 'sect4': {}, 'sect5': {}, 'sect6': {}}>"
999+
)
1000+
self.assertEqual(str(cf), expected_str)
1001+
1002+
1003+
dict_type = type(cf._dict).__name__
1004+
1005+
expected_repr = (
1006+
f"<{cf.__class__.__name__}("
1007+
f"params={{'dict_type': '{dict_type}', 'allow_no_value': True, "
1008+
"'delimiters': ('=',), 'strict': True, 'default_section': 'DEFAULT', "
1009+
"'interpolation': 'BasicInterpolation'}, "
1010+
"state={'loaded_sources': [], 'sections_count': 6, "
1011+
"'sections': ['sect1', 'sect2', 'sect3', 'sect4', 'sect5'], "
1012+
"'sections_truncated': '...and 1 more'})>"
1013+
)
1014+
self.assertEqual(repr(cf), expected_repr)
1015+
9831016
def test_add_section_default(self):
9841017
cf = self.newconfig()
9851018
self.assertRaises(ValueError, cf.add_section, self.default_section)

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,6 +1599,7 @@ Erik Rose
15991599
Mark Roseman
16001600
Josh Rosenberg
16011601
Jim Roskind
1602+
Prince Roshan
16021603
Brian Rosner
16031604
Ignacio Rossi
16041605
Guido van Rossum

0 commit comments

Comments
 (0)