From 371c554141a70f9cf90a57dd4390a2ed8798e356 Mon Sep 17 00:00:00 2001 From: lipengyu Date: Thu, 9 Jul 2026 20:23:51 +0800 Subject: [PATCH 1/5] gh-153418: Fix FrameLocalsProxy error propagation in update and |= --- Lib/test/test_frame.py | 10 ++++++++++ ...0-00-00.gh-issue-153418.framelocalsproxy-errors.rst | 3 +++ Objects/frameobject.c | 8 ++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index 18ade18d1a1708..f47133795fa65c 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -18,6 +18,11 @@ from test import mapping_tests +class FailingKeysDict(dict): + def keys(self): + raise RuntimeError("keys() failed") + + class ClearTest(unittest.TestCase): """ Tests for frame.clear(). @@ -380,6 +385,9 @@ def test_as_dict(self): self.assertEqual(d['x'], 3) self.assertEqual(d['z'], 4) + with self.assertRaisesRegex(RuntimeError, r"keys\(\) failed"): + d.update(FailingKeysDict()) + with self.assertRaises(TypeError): d.update([1, 2]) @@ -399,6 +407,8 @@ def test_as_number(self): self.assertEqual(d['z'], 3) d |= {'y': 3} self.assertEqual(d['y'], 3) + with self.assertRaisesRegex(RuntimeError, r"keys\(\) failed"): + operator.ior(d, FailingKeysDict()) with self.assertRaises(TypeError): d |= 3 with self.assertRaises(TypeError): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst new file mode 100644 index 00000000000000..0d5f1876cd1e44 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst @@ -0,0 +1,3 @@ +Fix ``frame.f_locals |= ...`` mishandling exceptions raised by the right-hand +mapping, and fix ``frame.f_locals.update()`` unconditionally replacing any +exception with :exc:`TypeError`. \ No newline at end of file diff --git a/Objects/frameobject.c b/Objects/frameobject.c index e7ac59379dcfbc..3fa9b6494d4144 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -573,7 +573,7 @@ framelocalsproxy_inplace_or(PyObject *self, PyObject *other) } if (framelocalsproxy_merge(self, other) < 0) { - Py_RETURN_NOTIMPLEMENTED; + return NULL; } return Py_NewRef(self); @@ -720,11 +720,15 @@ static PyObject* framelocalsproxy___contains__(PyObject *self, PyObject *key) static PyObject* framelocalsproxy_update(PyObject *self, PyObject *other) { - if (framelocalsproxy_merge(self, other) < 0) { + if (!PyDict_Check(other) && !PyFrameLocalsProxy_Check(other)) { PyErr_SetString(PyExc_TypeError, "update() argument must be dict or another FrameLocalsProxy"); return NULL; } + if (framelocalsproxy_merge(self, other) < 0) { + return NULL; + } + Py_RETURN_NONE; } From c5ae4dbf1b78c580cf8aa3d503dd450091efd85e Mon Sep 17 00:00:00 2001 From: lipengyu Date: Thu, 9 Jul 2026 20:44:15 +0800 Subject: [PATCH 2/5] update --- ...6-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst index 0d5f1876cd1e44..e0afa3e629782d 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst @@ -1,3 +1,3 @@ Fix ``frame.f_locals |= ...`` mishandling exceptions raised by the right-hand mapping, and fix ``frame.f_locals.update()`` unconditionally replacing any -exception with :exc:`TypeError`. \ No newline at end of file +exception with :exc:`TypeError`. From 734e8d2a52d440023f85ee0c6c74d184ab8fbba9 Mon Sep 17 00:00:00 2001 From: stevens Date: Fri, 10 Jul 2026 09:11:04 +0800 Subject: [PATCH 3/5] apply suggestion Co-authored-by: Brij Kapadia <97006829+brijkapadia@users.noreply.github.com> --- Lib/test/test_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index f47133795fa65c..5c58ceeb69f441 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -408,7 +408,7 @@ def test_as_number(self): d |= {'y': 3} self.assertEqual(d['y'], 3) with self.assertRaisesRegex(RuntimeError, r"keys\(\) failed"): - operator.ior(d, FailingKeysDict()) + d |= FailingKeysDict() with self.assertRaises(TypeError): d |= 3 with self.assertRaises(TypeError): From a596d13ec45bb262ebab3e728bdc8c216232be05 Mon Sep 17 00:00:00 2001 From: lipengyu Date: Sat, 19 Sep 2026 13:40:51 +0800 Subject: [PATCH 4/5] update --- Lib/test/test_frame.py | 16 +++++++++------- ...0.gh-issue-153418.framelocalsproxy-errors.rst | 7 ++++--- Objects/frameobject.c | 7 ++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index 5c58ceeb69f441..b357ce262702a7 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -18,9 +18,9 @@ from test import mapping_tests -class FailingKeysDict(dict): +class FailingKeys(dict): def keys(self): - raise RuntimeError("keys() failed") + raise RuntimeError("keys failed") class ClearTest(unittest.TestCase): @@ -385,10 +385,12 @@ def test_as_dict(self): self.assertEqual(d['x'], 3) self.assertEqual(d['z'], 4) - with self.assertRaisesRegex(RuntimeError, r"keys\(\) failed"): - d.update(FailingKeysDict()) + with self.assertRaisesRegex(RuntimeError, "keys failed"): + d.update(FailingKeys()) - with self.assertRaises(TypeError): + with self.assertRaisesRegex( + TypeError, r"update\(\) argument must be dict or another FrameLocalsProxy" + ): d.update([1, 2]) self.assertEqual(d.setdefault('x', 5), 3) @@ -407,8 +409,8 @@ def test_as_number(self): self.assertEqual(d['z'], 3) d |= {'y': 3} self.assertEqual(d['y'], 3) - with self.assertRaisesRegex(RuntimeError, r"keys\(\) failed"): - d |= FailingKeysDict() + with self.assertRaisesRegex(RuntimeError, "keys failed"): + d |= FailingKeys() with self.assertRaises(TypeError): d |= 3 with self.assertRaises(TypeError): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst index e0afa3e629782d..19eb4f75909f7c 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst @@ -1,3 +1,4 @@ -Fix ``frame.f_locals |= ...`` mishandling exceptions raised by the right-hand -mapping, and fix ``frame.f_locals.update()`` unconditionally replacing any -exception with :exc:`TypeError`. +Fix error handling in the ``FrameLocalsProxy`` returned by +:attr:`frame.f_locals`: ``update()`` no longer replaces the exception raised +by its argument with :exc:`TypeError`, and ``|=`` no longer returns while an +exception is set, which made debug builds abort with a fatal error. diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 3fa9b6494d4144..40b39a83b485c1 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -325,6 +325,8 @@ static int framelocalsproxy_merge(PyObject* self, PyObject* other) { if (!PyDict_Check(other) && !PyFrameLocalsProxy_Check(other)) { + PyErr_SetString(PyExc_TypeError, + "update() argument must be dict or another FrameLocalsProxy"); return -1; } @@ -720,11 +722,6 @@ static PyObject* framelocalsproxy___contains__(PyObject *self, PyObject *key) static PyObject* framelocalsproxy_update(PyObject *self, PyObject *other) { - if (!PyDict_Check(other) && !PyFrameLocalsProxy_Check(other)) { - PyErr_SetString(PyExc_TypeError, "update() argument must be dict or another FrameLocalsProxy"); - return NULL; - } - if (framelocalsproxy_merge(self, other) < 0) { return NULL; } From 7cc4721b9dd95e683675999bc02cf869c55fcd0c Mon Sep 17 00:00:00 2001 From: Pengyu Lee Date: Thu, 24 Sep 2026 23:46:44 +0800 Subject: [PATCH 5/5] updates NEWS --- ...07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst | 4 ---- .../2026-07-09-12-25-06.gh-issue-153418.taxDqe.rst | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-12-25-06.gh-issue-153418.taxDqe.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst deleted file mode 100644 index 19eb4f75909f7c..00000000000000 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-00-00-00.gh-issue-153418.framelocalsproxy-errors.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fix error handling in the ``FrameLocalsProxy`` returned by -:attr:`frame.f_locals`: ``update()`` no longer replaces the exception raised -by its argument with :exc:`TypeError`, and ``|=`` no longer returns while an -exception is set, which made debug builds abort with a fatal error. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-12-25-06.gh-issue-153418.taxDqe.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-12-25-06.gh-issue-153418.taxDqe.rst new file mode 100644 index 00000000000000..11c88c2850509b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-09-12-25-06.gh-issue-153418.taxDqe.rst @@ -0,0 +1,4 @@ +Fix error handling in the ``FrameLocalsProxy`` returned by +:attr:`frame.f_locals`. Exceptions raised by the argument of ``update()`` +or ``|=`` are now propagated instead of being replaced with +:exc:`TypeError` or :exc:`SystemError`, or aborting debug builds.