From 1c82c07a0dbdff9af76da7a4096fd3f29d505cfc Mon Sep 17 00:00:00 2001 From: hanjinpeng Date: Sun, 13 Sep 2026 21:44:00 -0400 Subject: [PATCH] Make _compat_skip_empty_lines a per-instance attribute INISection._compat_skip_empty_lines was a mutable set defined at class level and never re-assigned in __init__(), so every section of every INIConfig/RawConfigParser in the process shared the same set. As a consequence, operations on one parser changed the values returned by another one: >>> a = RawConfigParser() >>> a.readfp(StringIO('[a]\nfoo = line1\n\n line2\n')) >>> a.get('a', 'foo') 'line1\nline2' >>> b = RawConfigParser(); b.add_section('b'); b.set('b', 'foo', 'x') >>> a.get('a', 'foo') 'line1\n\nline2' Create the set in INISection.__init__() and keep only an immutable placeholder on the class. --- iniparse/ini.py | 6 ++++- tests/test_regressions.py | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/test_regressions.py diff --git a/iniparse/ini.py b/iniparse/ini.py index 548c794..bd4b3fc 100644 --- a/iniparse/ini.py +++ b/iniparse/ini.py @@ -355,7 +355,10 @@ class INISection(config.ConfigNamespace): _defaults: Optional["INISection"] = None _optionxformvalue: "INIConfig" = None _optionxformsource: "INIConfig" = None - _compat_skip_empty_lines: Set[str] = set() + # The real set is created per instance in __init__(). The immutable + # class-level value only makes ConfigNamespace.__setattr__ treat the + # name as a member; a mutable set here would be shared by all sections. + _compat_skip_empty_lines: Set[str] = frozenset() # type: ignore[assignment] def __init__( self, @@ -369,6 +372,7 @@ def __init__( self._optionxformvalue = optionxformvalue self._optionxformsource = optionxformsource self._options = {} + self._compat_skip_empty_lines = set() _optionxform = _make_xform_property('_optionxform') diff --git a/tests/test_regressions.py b/tests/test_regressions.py new file mode 100644 index 0000000..b6fc1a4 --- /dev/null +++ b/tests/test_regressions.py @@ -0,0 +1,53 @@ +""" +:component: python-iniparse +:requirement: RHSS-291606 +:polarion-project-id: RHELSS +:polarion-include-skipped: false +:polarion-lookup-method: id +:poolteam: rhel-sst-csi-client-tools +:caseautomation: Automated +:upstream: No +""" + +import unittest +from io import StringIO + +from iniparse import compat + + +class TestCompatSkipEmptyLines(unittest.TestCase): + def test_not_shared_between_instances(self): + """ + :id: fce0ac15-8871-4af4-8017-5f37cbc905b0 + :title: Empty-line skipping state is not shared between parsers + :description: + Verifies that the per-option "skip empty lines" compatibility + state of one parser is not modified by operations performed + on another, unrelated parser. + :tags: Tier 1 + :steps: + 1. Parse a multi-line value containing an empty line with parser A. + 2. Set an option with the same name in parser B. + 3. Set a multi-line value in parser C, then parse a file with + parser D that has a same-named multi-line option. + :expectedresults: + 1. Parser A returns the value without the empty line. + 2. Parser A still returns the value without the empty line. + 3. Parser C still returns its value with the empty line kept. + """ + a = compat.RawConfigParser() + a.readfp(StringIO('[a]\nfoo = line1\n\n line2\n')) + self.assertEqual(a.get('a', 'foo'), 'line1\nline2') + + b = compat.RawConfigParser() + b.add_section('b') + b.set('b', 'foo', 'x') + self.assertEqual(a.get('a', 'foo'), 'line1\nline2') + + c = compat.RawConfigParser() + c.add_section('c') + c.set('c', 'bar', 'x\n\ny') + d = compat.RawConfigParser() + d.readfp(StringIO('[d]\nbar = 1\n\n 2\n')) + self.assertEqual(c.get('c', 'bar'), 'x\n\ny') + self.assertEqual(d.get('d', 'bar'), '1\n2')