From 886bc9d2c613f96ea9b323ee4a1f7e827c29cc00 Mon Sep 17 00:00:00 2001
From: Nowa Ammerlaan <nowa@gentoo.org>
Date: Fri, 12 Jun 2026 16:56:09 +0200
Subject: [PATCH] spyderpdb.py: replace curframe_locals with curframe.f_locals

From the python 3.14 changelog:

The undocumented pdb.Pdb.curframe_locals attribute is now a deprecated
read-only property, which will be removed in a future version of
Python. The low overhead dynamic frame locals access added in Python
3.13 by PEP 667 means the frame locals cache reference previously
stored in this attribute is no longer needed. Derived debuggers should
access pdb.Pdb.curframe.f_locals directly in Python 3.13 and later
versions.

Signed-off-by: Nowa Ammerlaan <nowa@gentoo.org>
---
 spyder_kernels/console/shell.py                    |  5 ++-
 .../console/tests/test_console_kernel.py          | 34 ++++++++++++++--------
 spyder_kernels/customize/spyderpdb.py             | 24 ++++++++++++---
 3 files changed, 46 insertions(+), 17 deletions(-)

Index: spyder-kernels/spyder_kernels/console/shell.py
===================================================================
--- spyder-kernels.orig/spyder_kernels/console/shell.py
+++ spyder-kernels/spyder_kernels/console/shell.py
@@ -297,7 +297,10 @@ class SpyderShell(ZMQInteractiveShell):
         for session in self._namespace_stack[::-1]:
             if isinstance(session, SpyderPdb) and session.curframe is not None:
                 if frame is None or frame == session.curframe:
-                    return session.curframe_locals
+                    if sys.version_info[:2] <= (3, 12):
+                        return session.curframe_locals
+                    else:
+                        return session.curframe.f_locals
             elif frame is None and isinstance(session, NamespaceManager):
                 return session.ns_locals
 
Index: spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
===================================================================
--- spyder-kernels.orig/spyder_kernels/console/tests/test_console_kernel.py
+++ spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py
@@ -975,7 +975,8 @@ def test_comprehensions_with_locals_in_p
     """
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
     kernel.shell._namespace_stack = [pdb_obj]
 
     # Create a local variable.
@@ -991,7 +992,8 @@ def test_comprehensions_with_locals_in_p
     assert kernel.get_value('in_globals') is False
 
     pdb_obj.curframe = None
-    pdb_obj.curframe_locals = None
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = None
 
 
 def test_comprehensions_with_locals_in_pdb_2(kernel):
@@ -1002,7 +1004,8 @@ def test_comprehensions_with_locals_in_p
     """
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
     kernel.shell._namespace_stack = [pdb_obj]
 
     # Create a local variable.
@@ -1016,7 +1019,8 @@ def test_comprehensions_with_locals_in_p
     assert kernel.get_value('res') == [[(1, 3), (1, 4)], [(2, 3), (2, 4)]]
 
     pdb_obj.curframe = None
-    pdb_obj.curframe_locals = None
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = None
 
 
 def test_namespaces_in_pdb(kernel):
@@ -1028,7 +1032,8 @@ def test_namespaces_in_pdb(kernel):
     kernel.shell.user_ns["test"] = 0
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
     kernel.shell._namespace_stack = [pdb_obj]
 
     # Check adding something to globals works
@@ -1051,7 +1056,10 @@ def test_namespaces_in_pdb(kernel):
     assert not pdb_obj._error_occured
 
     # Test locals are visible
-    pdb_obj.curframe_locals["test4"] = 0
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals["test4"] = 0
+    else:
+        pdb_obj.curframe.f_locals["test4"] = 0
     pdb_obj.default("%timeit test4")
     assert not pdb_obj._error_occured
 
@@ -1060,7 +1068,8 @@ def test_namespaces_in_pdb(kernel):
     assert pdb_obj._error_occured
 
     pdb_obj.curframe = None
-    pdb_obj.curframe_locals = None
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = None
 
 
 def test_functions_with_locals_in_pdb(kernel):
@@ -1071,7 +1080,7 @@ def test_functions_with_locals_in_pdb(ke
     """
     pdb_obj = SpyderPdb()
 
-    if sys.version_info[:2] >=(3, 14):
+    if sys.version_info[:2] >= (3, 13):
         Frame = namedtuple("Frame", ["f_globals", "f_locals"])
         pdb_obj.curframe = Frame(
             f_globals=kernel.shell.user_ns,
@@ -1100,7 +1109,8 @@ def test_functions_with_locals_in_pdb(ke
     assert kernel.get_value('zz') == 1
 
     pdb_obj.curframe = None
-    pdb_obj.curframe_locals = None
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = None
 
 
 def test_functions_with_locals_in_pdb_2(kernel):
@@ -1112,7 +1122,8 @@ def test_functions_with_locals_in_pdb_2(
     baba = 1  # noqa
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
     kernel.shell._namespace_stack = [pdb_obj]
 
     # Create a local function.
@@ -1139,7 +1150,8 @@ def test_functions_with_locals_in_pdb_2(
     assert "baba" not in kernel.get_value('gg')
 
     pdb_obj.curframe = None
-    pdb_obj.curframe_locals = None
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = None
 
 
 def test_locals_globals_in_pdb(kernel):
@@ -1149,7 +1161,8 @@ def test_locals_globals_in_pdb(kernel):
     a = 1  # noqa
     pdb_obj = SpyderPdb()
     pdb_obj.curframe = inspect.currentframe()
-    pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = pdb_obj.curframe.f_locals
     kernel.shell._namespace_stack = [pdb_obj]
 
     assert kernel.get_value('a') == 1
@@ -1181,7 +1194,8 @@ def test_locals_globals_in_pdb(kernel):
     assert kernel.get_value('test') is True
 
     pdb_obj.curframe = None
-    pdb_obj.curframe_locals = None
+    if sys.version_info[:2] <= (3, 12):
+        pdb_obj.curframe_locals = None
 
 
 @pytest.mark.flaky(max_runs=3)
Index: spyder-kernels/spyder_kernels/customize/spyderpdb.py
===================================================================
--- spyder-kernels.orig/spyder_kernels/customize/spyderpdb.py
+++ spyder-kernels/spyder_kernels/customize/spyderpdb.py
@@ -83,7 +83,8 @@ class SpyderPdb(ipyPdb):
 
     def __init__(self, *args, **kwargs):
         """Init Pdb."""
-        self.curframe_locals = None
+        if sys.version_info[:2] <= (3, 12):
+            self.curframe_locals = None
         # Only set to true when calling debugfile
         self.continue_if_has_breakpoints = False
         self.pdb_ignore_lib = False
@@ -159,7 +160,10 @@ class SpyderPdb(ipyPdb):
             if cmd == "debug":
                 return self.do_debug(arg)
 
-        local_ns = self.curframe_locals
+        if sys.version_info[:2] >= (3, 13):
+            local_ns = self.curframe.f_locals
+        else:
+            local_ns = self.curframe_locals
         global_ns = self.curframe.f_globals
 
         if self.pdb_use_exclamation_mark:
@@ -424,12 +428,22 @@ class SpyderPdb(ipyPdb):
         if ipython_do_complete:
             # Make complete call with current frame
             if self.curframe:
-                if self.curframe_locals:
-                    Frame = namedtuple("Frame", ["f_locals", "f_globals"])
-                    frame = Frame(self.curframe_locals,
-                                  self.curframe.f_globals)
+                if sys.version_info[:2] >= (3, 13):
+                    if self.curframe.f_locals:
+                        Frame = namedtuple("Frame", ["f_locals", "f_globals"])
+                        frame = Frame(
+                            self.curframe.f_locals, self.curframe.f_globals
+                        )
+                    else:
+                        frame = self.curframe
                 else:
-                    frame = self.curframe
+                    if self.curframe_locals:
+                        Frame = namedtuple("Frame", ["f_locals", "f_globals"])
+                        frame = Frame(
+                            self.curframe_locals, self.curframe.f_globals
+                        )
+                    else:
+                        frame = self.curframe
                 self.shell.set_completer_frame(frame)
             result = await self.shell.kernel._do_complete(code, cursor_pos)
             # Reset frame
