Skip to content

Commit 4cdb0ae

Browse files
committed
gh-127011: Add __str__ and __repr__ to ConfigParser
- Implement __str__ method showing formatted configuration content - Implement __repr__ method with constructor parameters and state summary - Add comprehensive tests for both methods - Update ACKS and add NEWS entry
1 parent 3704171 commit 4cdb0ae

File tree

4 files changed

+97
-10
lines changed

4 files changed

+97
-10
lines changed

Lib/configparser.py

Lines changed: 44 additions & 4 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
@@ -757,6 +758,7 @@ def read(self, filenames, encoding=None):
757758
if isinstance(filename, os.PathLike):
758759
filename = os.fspath(filename)
759760
read_ok.append(filename)
761+
self._loaded_sources.append(read_ok)
760762
return read_ok
761763

762764
def read_file(self, f, source=None):
@@ -773,6 +775,7 @@ def read_file(self, f, source=None):
773775
except AttributeError:
774776
source = '<???>'
775777
self._read(f, source)
778+
self._loaded_sources.append(source)
776779

777780
def read_string(self, string, source='<string>'):
778781
"""Read configuration from a given string."""
@@ -809,6 +812,7 @@ def read_dict(self, dictionary, source='<dict>'):
809812
raise DuplicateOptionError(section, key, source)
810813
elements_added.add((section, key))
811814
self.set(section, key, value)
815+
self._loaded_sources.append(source)
812816

813817
def get(self, section, option, *, raw=False, vars=None, fallback=_UNSET):
814818
"""Get an option value for a given section.
@@ -1048,6 +1052,38 @@ def __iter__(self):
10481052
# XXX does it break when underlying container state changed?
10491053
return itertools.chain((self.default_section,), self._sections.keys())
10501054

1055+
def __str__(self):
1056+
config_dict = {
1057+
section: dict(self.items(section, raw=True))
1058+
for section in self.sections()
1059+
}
1060+
return f"<ConfigParser: {config_dict}>"
1061+
1062+
def __repr__(self):
1063+
params = {
1064+
"defaults": self._defaults if self._defaults else None,
1065+
"dict_type": type(self._dict).__name__,
1066+
"allow_no_value": self._allow_no_value,
1067+
"delimiters": self._delimiters,
1068+
"strict": self._strict,
1069+
"default_section": self.default_section,
1070+
"interpolation": type(self._interpolation).__name__,
1071+
}
1072+
params = {k: v for k, v in params.items() if v is not None}
1073+
sections_count = len(self._sections)
1074+
state = {
1075+
"loaded_sources": self._loaded_sources,
1076+
"sections_count": sections_count,
1077+
"sections": list(self._sections)[:5], # limit to 5 section names for readability
1078+
}
1079+
1080+
if sections_count > 5:
1081+
state["sections_truncated"] = f"...and {sections_count - 5} more"
1082+
1083+
return (f"<{self.__class__.__name__}("
1084+
f"params={params}, "
1085+
f"state={state})>")
1086+
10511087
def _read(self, fp, fpname):
10521088
"""Parse a sectioned configuration file.
10531089
@@ -1068,6 +1104,7 @@ def _read(self, fp, fpname):
10681104
try:
10691105
ParsingError._raise_all(self._read_inner(fp, fpname))
10701106
finally:
1107+
self._loaded_sources.append(fpname)
10711108
self._join_multiline_values()
10721109

10731110
def _read_inner(self, fp, fpname):
@@ -1218,11 +1255,14 @@ def _convert_to_boolean(self, value):
12181255

12191256
def _validate_key_contents(self, key):
12201257
"""Raises an InvalidWriteError for any keys containing
1221-
delimiters or that match the section header pattern"""
1258+
delimiters or that begins with the section header pattern"""
12221259
if re.match(self.SECTCRE, key):
1223-
raise InvalidWriteError("Cannot write keys matching section pattern")
1224-
if any(delim in key for delim in self._delimiters):
1225-
raise InvalidWriteError("Cannot write key that contains delimiters")
1260+
raise InvalidWriteError(
1261+
f"Cannot write key {key}; begins with section pattern")
1262+
for delim in self._delimiters:
1263+
if delim in key:
1264+
raise InvalidWriteError(
1265+
f"Cannot write key {key}; contains delimiter {delim}")
12261266

12271267
def _validate_value_types(self, *, section="", option="", value=""):
12281268
"""Raises a TypeError for illegal non-string values.

Lib/test/test_configparser.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -980,18 +980,62 @@ 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(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.set("sect1", "option1", "foo")
989+
cf.set("sect2", "option2", "bar")
990+
991+
expected_str = (
992+
"<ConfigParser: {'sect1': {'option1': 'foo'}, 'sect2': {'option2': 'bar'}}>"
993+
)
994+
self.assertEqual(str(cf), expected_str)
995+
996+
def test_repr(self):
997+
self.maxDiff = None
998+
cf = self.config_class(allow_no_value=True, delimiters=('=',), strict=True)
999+
cf.add_section("sect1")
1000+
cf.add_section("sect2")
1001+
cf.add_section("sect3")
1002+
cf.add_section("sect4")
1003+
cf.add_section("sect5")
1004+
cf.add_section("sect6")
1005+
cf.set("sect1", "option1", "foo")
1006+
cf.set("sect2", "option2", "bar")
1007+
cf.read_string("") # to trigger the loading of sources
1008+
1009+
dict_type = type(cf._dict).__name__
1010+
params = {
1011+
'dict_type': dict_type,
1012+
'allow_no_value': True,
1013+
'delimiters': ('=',),
1014+
'strict': True,
1015+
'default_section': 'DEFAULT',
1016+
'interpolation': 'BasicInterpolation',
1017+
}
1018+
state = {
1019+
'loaded_sources': ['<string>'],
1020+
'sections_count': 6,
1021+
'sections': ['sect1', 'sect2', 'sect3', 'sect4', 'sect5'],
1022+
'sections_truncated': '...and 1 more',
1023+
}
1024+
expected = f"<{type(cf).__name__}({params=}, {state=})>"
1025+
self.assertEqual(repr(cf), expected)
1026+
9831027
def test_add_section_default(self):
9841028
cf = self.newconfig()
9851029
self.assertRaises(ValueError, cf.add_section, self.default_section)
9861030

9871031
def test_defaults_keyword(self):
9881032
"""bpo-23835 fix for ConfigParser"""
989-
cf = self.newconfig(defaults={1: 2.4})
990-
self.assertEqual(cf[self.default_section]['1'], '2.4')
991-
self.assertAlmostEqual(cf[self.default_section].getfloat('1'), 2.4)
992-
cf = self.newconfig(defaults={"A": 5.2})
993-
self.assertEqual(cf[self.default_section]['a'], '5.2')
994-
self.assertAlmostEqual(cf[self.default_section].getfloat('a'), 5.2)
1033+
cf = self.newconfig(defaults={1: 2.5})
1034+
self.assertEqual(cf[self.default_section]['1'], '2.5')
1035+
self.assertAlmostEqual(cf[self.default_section].getfloat('1'), 2.5)
1036+
cf = self.newconfig(defaults={"A": 5.25})
1037+
self.assertEqual(cf[self.default_section]['a'], '5.25')
1038+
self.assertAlmostEqual(cf[self.default_section].getfloat('a'), 5.25)
9951039

9961040

9971041
class ConfigParserTestCaseNoInterpolation(BasicTestCase, unittest.TestCase):

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,6 +1602,7 @@ Joel Rosdahl
16021602
Erik Rose
16031603
Mark Roseman
16041604
Josh Rosenberg
1605+
Prince Roshan
16051606
Jim Roskind
16061607
Brian Rosner
16071608
Ignacio Rossi
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implement :meth:`~object.__str__` and :meth:`~object.__repr__`
2+
for :class:`configparser.RawConfigParser` objects.

0 commit comments

Comments
 (0)