From 0ea8e9ddc77023a723578947bee7d23443c2084e Mon Sep 17 00:00:00 2001 From: Nikolaus Schuetz Date: Sat, 15 Aug 2026 23:04:55 -0400 Subject: [PATCH] gh-70999: Preserve caller vars in ExtendedInterpolation nested interpolation ExtendedInterpolation honoured caller-supplied ``vars`` only at the first level of interpolation. When a value referenced another option in the same section, the recursive call rebuilt the lookup map from ``parser.items(section, raw=True)``, discarding the map that carried ``vars``. Mirror BasicInterpolation, which threads the same ``(section, map)`` through recursion, by reusing ``map`` when the reference stays in the current section and only rebuilding it when crossing into another section (so ``vars`` do not leak across sections). --- Lib/configparser.py | 7 ++++-- Lib/test/test_configparser.py | 24 +++++++++++++++++++ ...6-08-15-12-00-00.gh-issue-70999.NZ96LZ.rst | 3 +++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-15-12-00-00.gh-issue-70999.NZ96LZ.rst diff --git a/Lib/configparser.py b/Lib/configparser.py index 3c452afe8ade48..ac1b2ac3b74aa1 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -547,9 +547,12 @@ def _interpolate_some(self, parser, option, accum, rest, section, map, if v is None: continue if "$" in v: + if sect == section: + submap = map + else: + submap = dict(parser.items(sect, raw=True)) self._interpolate_some(parser, opt, accum, v, sect, - dict(parser.items(sect, raw=True)), - depth + 1) + submap, depth + 1) else: accum.append(v) else: diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 4783943f71a109..48625c26a9139a 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -1334,6 +1334,30 @@ def test_other_errors(self): with self.assertRaises(ValueError): cf['interpolation fail']['case6'] = "BLACK $ABBATH" + def test_get_with_vars_nested(self): + # gh-70999: caller-supplied ``vars`` must be honoured at every level + # of a same-section interpolation chain, not just the first. + cf = self.fromstring(textwrap.dedent(""" + [section] + a = ${b} + b = ${c} + c = default + + [cross] + via = ${section:c} + """).strip()) + + eq = self.assertEqual + # Directly referencing the overridden option already worked. + eq(cf.get('section', 'b', vars={'c': 'OVERRIDE'}), 'OVERRIDE') + # Reaching it through another same-section option must too. + eq(cf.get('section', 'a', vars={'c': 'OVERRIDE'}), 'OVERRIDE') + # Without an override the configured value is still used. + eq(cf.get('section', 'a'), 'default') + # ``vars`` are scoped to the requested section and must not leak + # into a different section reached via ``${section:option}``. + eq(cf.get('cross', 'via', vars={'c': 'OVERRIDE'}), 'default') + class ConfigParserTestCaseNoValue(ConfigParserTestCase): allow_no_value = True diff --git a/Misc/NEWS.d/next/Library/2026-08-15-12-00-00.gh-issue-70999.NZ96LZ.rst b/Misc/NEWS.d/next/Library/2026-08-15-12-00-00.gh-issue-70999.NZ96LZ.rst new file mode 100644 index 00000000000000..3fab7ece4994d6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-15-12-00-00.gh-issue-70999.NZ96LZ.rst @@ -0,0 +1,3 @@ +Fix :class:`configparser.ExtendedInterpolation` dropping caller-supplied +``vars`` during nested interpolation within a section. Patch by Nikolaus +Schuetz.