|
77 | 77 | import code
|
78 | 78 | import glob
|
79 | 79 | import token
|
| 80 | +import types |
80 | 81 | import codeop
|
81 | 82 | import pprint
|
82 | 83 | import signal
|
83 | 84 | import inspect
|
| 85 | +import textwrap |
84 | 86 | import tokenize
|
85 | 87 | import traceback
|
86 | 88 | import linecache
|
@@ -624,11 +626,96 @@ def _disable_command_completion(self):
|
624 | 626 | self.completenames = completenames
|
625 | 627 | return
|
626 | 628 |
|
| 629 | + def _exec_in_closure(self, source, globals, locals): |
| 630 | + """ Run source code in closure so code object created within source |
| 631 | + can find variables in locals correctly |
| 632 | +
|
| 633 | + returns True if the source is executed, False otherwise |
| 634 | + """ |
| 635 | + |
| 636 | + # Determine if the source should be executed in closure. Only when the |
| 637 | + # source compiled to multiple code objects, we should use this feature. |
| 638 | + # Otherwise, we can just raise an exception and normal exec will be used. |
| 639 | + |
| 640 | + code = compile(source, "<string>", "exec") |
| 641 | + if not any(isinstance(const, CodeType) for const in code.co_consts): |
| 642 | + return False |
| 643 | + |
| 644 | + # locals could be a proxy which does not support pop |
| 645 | + # copy it first to avoid modifying the original locals |
| 646 | + locals_copy = dict(locals) |
| 647 | + |
| 648 | + locals_copy["__pdb_eval__"] = { |
| 649 | + "result": None, |
| 650 | + "write_back": {} |
| 651 | + } |
| 652 | + |
| 653 | + # If the source is an expression, we need to print its value |
| 654 | + try: |
| 655 | + compile(source, "<string>", "eval") |
| 656 | + except SyntaxError: |
| 657 | + pass |
| 658 | + else: |
| 659 | + source = "__pdb_eval__['result'] = " + source |
| 660 | + |
| 661 | + # Add write-back to update the locals |
| 662 | + source = ("try:\n" + |
| 663 | + textwrap.indent(source, " ") + "\n" + |
| 664 | + "finally:\n" + |
| 665 | + " __pdb_eval__['write_back'] = locals()") |
| 666 | + |
| 667 | + # Build a closure source code with freevars from locals like: |
| 668 | + # def __pdb_outer(): |
| 669 | + # var = None |
| 670 | + # def __pdb_scope(): # This is the code object we want to execute |
| 671 | + # nonlocal var |
| 672 | + # <source> |
| 673 | + # return __pdb_scope.__code__ |
| 674 | + source_with_closure = ("def __pdb_outer():\n" + |
| 675 | + "\n".join(f" {var} = None" for var in locals_copy) + "\n" + |
| 676 | + " def __pdb_scope():\n" + |
| 677 | + "\n".join(f" nonlocal {var}" for var in locals_copy) + "\n" + |
| 678 | + textwrap.indent(source, " ") + "\n" + |
| 679 | + " return __pdb_scope.__code__" |
| 680 | + ) |
| 681 | + |
| 682 | + # Get the code object of __pdb_scope() |
| 683 | + # The exec fills locals_copy with the __pdb_outer() function and we can call |
| 684 | + # that to get the code object of __pdb_scope() |
| 685 | + ns = {} |
| 686 | + try: |
| 687 | + exec(source_with_closure, {}, ns) |
| 688 | + except Exception: |
| 689 | + return False |
| 690 | + code = ns["__pdb_outer"]() |
| 691 | + |
| 692 | + cells = tuple(types.CellType(locals_copy.get(var)) for var in code.co_freevars) |
| 693 | + |
| 694 | + try: |
| 695 | + exec(code, globals, locals_copy, closure=cells) |
| 696 | + except Exception: |
| 697 | + return False |
| 698 | + |
| 699 | + # get the data we need from the statement |
| 700 | + pdb_eval = locals_copy["__pdb_eval__"] |
| 701 | + |
| 702 | + # __pdb_eval__ should not be updated back to locals |
| 703 | + pdb_eval["write_back"].pop("__pdb_eval__") |
| 704 | + |
| 705 | + # Write all local variables back to locals |
| 706 | + locals.update(pdb_eval["write_back"]) |
| 707 | + eval_result = pdb_eval["result"] |
| 708 | + if eval_result is not None: |
| 709 | + print(repr(eval_result)) |
| 710 | + |
| 711 | + return True |
| 712 | + |
627 | 713 | def default(self, line):
|
628 | 714 | if line[:1] == '!': line = line[1:].strip()
|
629 | 715 | locals = self.curframe_locals
|
630 | 716 | globals = self.curframe.f_globals
|
631 | 717 | try:
|
| 718 | + buffer = line |
632 | 719 | if (code := codeop.compile_command(line + '\n', '<stdin>', 'single')) is None:
|
633 | 720 | # Multi-line mode
|
634 | 721 | with self._disable_command_completion():
|
@@ -661,7 +748,8 @@ def default(self, line):
|
661 | 748 | sys.stdin = self.stdin
|
662 | 749 | sys.stdout = self.stdout
|
663 | 750 | sys.displayhook = self.displayhook
|
664 |
| - exec(code, globals, locals) |
| 751 | + if not self._exec_in_closure(buffer, globals, locals): |
| 752 | + exec(code, globals, locals) |
665 | 753 | finally:
|
666 | 754 | sys.stdout = save_stdout
|
667 | 755 | sys.stdin = save_stdin
|
|
0 commit comments