1"""
2The code in this module is a backport of cPython changes in Pdb
3that were introduced in Python 3.13 by gh-83151: Make closure work on pdb
4https://github.com/python/cpython/pull/111094.
5This file should be removed once IPython drops supports for Python 3.12.
6
7The only changes are:
8- reformatting by darker (black) formatter
9- addition of type-ignore comments to satisfy mypy
10
11Copyright (c) 2001 Python Software Foundation; All Rights Reserved
12
13PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
14--------------------------------------------
15
161. This LICENSE AGREEMENT is between the Python Software Foundation
17("PSF"), and the Individual or Organization ("Licensee") accessing and
18otherwise using this software ("Python") in source or binary form and
19its associated documentation.
20
212. Subject to the terms and conditions of this License Agreement, PSF hereby
22grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
23analyze, test, perform and/or display publicly, prepare derivative works,
24distribute, and otherwise use Python alone or in any derivative version,
25provided, however, that PSF's License Agreement and PSF's notice of copyright,
26i.e., "Copyright (c) 2001 Python Software Foundation; All Rights Reserved"
27are retained in Python alone or in any derivative version prepared by Licensee.
28
293. In the event Licensee prepares a derivative work that is based on
30or incorporates Python or any part thereof, and wants to make
31the derivative work available to others as provided herein, then
32Licensee hereby agrees to include in any such work a brief summary of
33the changes made to Python.
34
354. PSF is making Python available to Licensee on an "AS IS"
36basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
37IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
38DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
39FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
40INFRINGE ANY THIRD PARTY RIGHTS.
41
425. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
43FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
44A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
45OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
46
476. This License Agreement will automatically terminate upon a material
48breach of its terms and conditions.
49
507. Nothing in this License Agreement shall be deemed to create any
51relationship of agency, partnership, or joint venture between PSF and
52Licensee. This License Agreement does not grant permission to use PSF
53trademarks or trade name in a trademark sense to endorse or promote
54products or services of Licensee, or any third party.
55
568. By copying, installing or otherwise using Python, Licensee
57agrees to be bound by the terms and conditions of this License
58Agreement.
59"""
60
61import sys
62import types
63import codeop
64import textwrap
65from types import CodeType
66
67
68class PdbClosureBackport:
69 def _exec_in_closure(self, source, globals, locals): # type: ignore[no-untyped-def]
70 """Run source code in closure so code object created within source
71 can find variables in locals correctly
72 returns True if the source is executed, False otherwise
73 """
74
75 # Determine if the source should be executed in closure. Only when the
76 # source compiled to multiple code objects, we should use this feature.
77 # Otherwise, we can just raise an exception and normal exec will be used.
78
79 code = compile(source, "<string>", "exec")
80 if not any(isinstance(const, CodeType) for const in code.co_consts):
81 return False
82
83 # locals could be a proxy which does not support pop
84 # copy it first to avoid modifying the original locals
85 locals_copy = dict(locals)
86
87 locals_copy["__pdb_eval__"] = {"result": None, "write_back": {}}
88
89 # If the source is an expression, we need to print its value
90 try:
91 compile(source, "<string>", "eval")
92 except SyntaxError:
93 pass
94 else:
95 source = "__pdb_eval__['result'] = " + source
96
97 # Add write-back to update the locals
98 source = (
99 "try:\n"
100 + textwrap.indent(source, " ")
101 + "\n"
102 + "finally:\n"
103 + " __pdb_eval__['write_back'] = locals()"
104 )
105
106 # Build a closure source code with freevars from locals like:
107 # def __pdb_outer():
108 # var = None
109 # def __pdb_scope(): # This is the code object we want to execute
110 # nonlocal var
111 # <source>
112 # return __pdb_scope.__code__
113 source_with_closure = (
114 "def __pdb_outer():\n"
115 + "\n".join(f" {var} = None" for var in locals_copy)
116 + "\n"
117 + " def __pdb_scope():\n"
118 + "\n".join(f" nonlocal {var}" for var in locals_copy)
119 + "\n"
120 + textwrap.indent(source, " ")
121 + "\n"
122 + " return __pdb_scope.__code__"
123 )
124
125 # Get the code object of __pdb_scope()
126 # The exec fills locals_copy with the __pdb_outer() function and we can call
127 # that to get the code object of __pdb_scope()
128 ns = {}
129 try:
130 exec(source_with_closure, {}, ns)
131 except Exception:
132 return False
133 code = ns["__pdb_outer"]()
134
135 cells = tuple(types.CellType(locals_copy.get(var)) for var in code.co_freevars)
136
137 try:
138 exec(code, globals, locals_copy, closure=cells)
139 except Exception:
140 return False
141
142 # get the data we need from the statement
143 pdb_eval = locals_copy["__pdb_eval__"]
144
145 # __pdb_eval__ should not be updated back to locals
146 pdb_eval["write_back"].pop("__pdb_eval__")
147
148 # Write all local variables back to locals
149 locals.update(pdb_eval["write_back"])
150 eval_result = pdb_eval["result"]
151 if eval_result is not None:
152 print(repr(eval_result))
153
154 return True
155
156 def default(self, line): # type: ignore[no-untyped-def]
157 if line[:1] == "!":
158 line = line[1:].strip()
159 locals = self.curframe_locals
160 globals = self.curframe.f_globals
161 try:
162 buffer = line
163 if (
164 code := codeop.compile_command(line + "\n", "<stdin>", "single")
165 ) is None:
166 # Multi-line mode
167 with self._disable_command_completion():
168 buffer = line
169 continue_prompt = "... "
170 while (
171 code := codeop.compile_command(buffer, "<stdin>", "single")
172 ) is None:
173 if self.use_rawinput:
174 try:
175 line = input(continue_prompt)
176 except (EOFError, KeyboardInterrupt):
177 self.lastcmd = ""
178 print("\n")
179 return
180 else:
181 self.stdout.write(continue_prompt)
182 self.stdout.flush()
183 line = self.stdin.readline()
184 if not len(line):
185 self.lastcmd = ""
186 self.stdout.write("\n")
187 self.stdout.flush()
188 return
189 else:
190 line = line.rstrip("\r\n")
191 buffer += "\n" + line
192 save_stdout = sys.stdout
193 save_stdin = sys.stdin
194 save_displayhook = sys.displayhook
195 try:
196 sys.stdin = self.stdin
197 sys.stdout = self.stdout
198 sys.displayhook = self.displayhook
199 if not self._exec_in_closure(buffer, globals, locals):
200 exec(code, globals, locals)
201 finally:
202 sys.stdout = save_stdout
203 sys.stdin = save_stdin
204 sys.displayhook = save_displayhook
205 except:
206 self._error_exc()