1import _ast 2import ast 3import builtins 4import dis 5import functools 6import importlib 7import inspect 8import io 9import os.path 10import pkgutil 11import pydoc 12import queue 13import re 14import site 15import subprocess 16import sys 17import tokenize 18import traceback 19import types 20import warnings 21from collections import namedtuple 22from importlib.machinery import SourceFileLoader, PathFinder 23from typing import Optional, Dict 24 25import __main__ 26 27import thonny 28from thonny import jedi_utils 29from thonny.backend import MainBackend, interrupt_local_process, logger 30from thonny.common import ( 31 InputSubmission, 32 EOFCommand, 33 ToplevelResponse, 34 CommandToBackend, 35 ToplevelCommand, 36 InlineCommand, 37 UserError, 38 InlineResponse, 39 MessageFromBackend, 40 serialize_message, 41 ValueInfo, 42 path_startswith, 43 get_exe_dirs, 44 ImmediateCommand, 45 get_single_dir_child_data, 46 BackendEvent, 47 DebuggerCommand, 48 execute_system_command, 49 FrameInfo, 50 is_same_path, 51 TextRange, 52 range_contains_smaller_or_equal, 53 OBJECT_LINK_START, 54 OBJECT_LINK_END, 55 range_contains_smaller, 56 DebuggerResponse, 57 get_python_version_string, 58 update_system_path, 59 get_augmented_system_path, 60 try_load_modules_with_frontend_sys_path, 61) 62 63BEFORE_STATEMENT_MARKER = "_thonny_hidden_before_stmt" 64BEFORE_EXPRESSION_MARKER = "_thonny_hidden_before_expr" 65AFTER_STATEMENT_MARKER = "_thonny_hidden_after_stmt" 66AFTER_EXPRESSION_MARKER = "_thonny_hidden_after_expr" 67 68_CO_GENERATOR = getattr(inspect, "CO_GENERATOR", 0) 69_CO_COROUTINE = getattr(inspect, "CO_COROUTINE", 0) 70_CO_ITERABLE_COROUTINE = getattr(inspect, "CO_ITERABLE_COROUTINE", 0) 71_CO_ASYNC_GENERATOR = getattr(inspect, "CO_ASYNC_GENERATOR", 0) 72_CO_WEIRDO = _CO_GENERATOR | _CO_COROUTINE | _CO_ITERABLE_COROUTINE | _CO_ASYNC_GENERATOR 73 74_REPL_HELPER_NAME = "_thonny_repl_print" 75 76_CONFIG_FILENAME = os.path.join(thonny.THONNY_USER_DIR, "backend_configuration.ini") 77 78TempFrameInfo = namedtuple( 79 "TempFrameInfo", 80 [ 81 "system_frame", 82 "locals", 83 "globals", 84 "event", 85 "focus", 86 "node_tags", 87 "current_statement", 88 "current_root_expression", 89 "current_evaluations", 90 ], 91) 92 93 94_backend = None 95 96 97class MainCPythonBackend(MainBackend): 98 def __init__(self, target_cwd): 99 100 MainBackend.__init__(self) 101 102 global _backend 103 _backend = self 104 105 self._ini = None 106 self._command_handlers = {} 107 self._object_info_tweakers = [] 108 self._import_handlers = {} 109 self._input_queue = queue.Queue() 110 self._source_preprocessors = [] 111 self._ast_postprocessors = [] 112 self._main_dir = os.path.dirname(sys.modules["thonny"].__file__) 113 self._heap = {} # WeakValueDictionary would be better, but can't store reference to None 114 self._source_info_by_frame = {} 115 site.sethelper() # otherwise help function is not available 116 pydoc.pager = pydoc.plainpager # otherwise help command plays tricks 117 self._install_fake_streams() 118 self._install_repl_helper() 119 self._current_executor = None 120 self._io_level = 0 121 self._tty_mode = True 122 self._tcl = None 123 124 update_system_path(os.environ, get_augmented_system_path(get_exe_dirs())) 125 126 # clean __main__ global scope 127 for key in list(__main__.__dict__.keys()): 128 if not key.startswith("__") or key in {"__file__", "__cached__"}: 129 del __main__.__dict__[key] 130 131 # unset __doc__, then exec dares to write doc of the script there 132 __main__.__doc__ = None 133 134 self._load_plugins() 135 136 # preceding code was run in the directory containing thonny module, now switch to provided 137 try: 138 os.chdir(os.path.expanduser(target_cwd)) 139 except OSError: 140 try: 141 os.chdir(os.path.expanduser("~")) 142 except OSError: 143 os.chdir("/") # yes, this works also in Windows 144 145 # ... and replace current-dir path item 146 # start in shell mode (may be later switched to script mode) 147 # required in shell mode and also required to overwrite thonny location dir 148 sys.path[0] = "" 149 sys.argv[:] = [""] # empty "script name" 150 151 if os.name == "nt": 152 self._install_signal_handler() 153 154 def _init_command_reader(self): 155 # Can't use threaded reader 156 # https://github.com/thonny/thonny/issues/1363 157 pass 158 159 def _install_signal_handler(self): 160 import signal 161 162 def signal_handler(signal_, frame): 163 raise KeyboardInterrupt("Execution interrupted") 164 165 if os.name == "nt": 166 signal.signal(signal.SIGBREAK, signal_handler) # pylint: disable=no-member 167 else: 168 signal.signal(signal.SIGINT, signal_handler) 169 170 def _fetch_next_incoming_message(self, timeout=None) -> CommandToBackend: 171 # Reading must be done synchronously 172 # https://github.com/thonny/thonny/issues/1363 173 self._read_one_incoming_message() 174 return self._incoming_message_queue.get() 175 176 def add_command(self, command_name, handler): 177 """Handler should be 1-argument function taking command object. 178 179 Handler may return None (in this case no response is sent to frontend) 180 or a BackendResponse 181 """ 182 self._command_handlers[command_name] = handler 183 184 def add_object_info_tweaker(self, tweaker): 185 """Tweaker should be 2-argument function taking value and export record""" 186 self._object_info_tweakers.append(tweaker) 187 188 def add_import_handler(self, module_name, handler): 189 if module_name not in self._import_handlers: 190 self._import_handlers[module_name] = [] 191 self._import_handlers[module_name].append(handler) 192 193 def add_source_preprocessor(self, func): 194 self._source_preprocessors.append(func) 195 196 def add_ast_postprocessor(self, func): 197 self._ast_postprocessors.append(func) 198 199 def get_main_module(self): 200 return __main__ 201 202 def _read_incoming_msg_line(self) -> str: 203 return self._original_stdin.readline() 204 205 def _handle_user_input(self, msg: InputSubmission) -> None: 206 self._input_queue.put(msg) 207 208 def _handle_eof_command(self, msg: EOFCommand) -> None: 209 self.send_message(ToplevelResponse(SystemExit=True)) 210 sys.exit() 211 212 def _handle_normal_command(self, cmd: CommandToBackend) -> None: 213 assert isinstance(cmd, (ToplevelCommand, InlineCommand)) 214 215 if isinstance(cmd, ToplevelCommand): 216 self._source_info_by_frame = {} 217 self._input_queue = queue.Queue() 218 219 if cmd.name in self._command_handlers: 220 handler = self._command_handlers[cmd.name] 221 else: 222 handler = getattr(self, "_cmd_" + cmd.name, None) 223 224 if handler is None: 225 response = {"error": "Unknown command: " + cmd.name} 226 else: 227 try: 228 response = handler(cmd) 229 except SystemExit as e: 230 # Must be caused by Thonny or plugins code 231 if isinstance(cmd, ToplevelCommand): 232 logger.exception("Unexpected SystemExit", exc_info=e) 233 response = {"SystemExit": True} 234 except UserError as e: 235 sys.stderr.write(str(e) + "\n") 236 response = {} 237 except KeyboardInterrupt: 238 response = {"user_exception": self._prepare_user_exception()} 239 except Exception as e: 240 self._report_internal_exception(e) 241 response = {"context_info": "other unhandled exception"} 242 243 if response is False: 244 # Command doesn't want to send any response 245 return 246 247 real_response = self._prepare_command_response(response, cmd) 248 249 if isinstance(real_response, ToplevelResponse): 250 real_response["gui_is_active"] = ( 251 self._get_tcl() is not None or self._get_qt_app() is not None 252 ) 253 254 self.send_message(real_response) 255 256 def _handle_immediate_command(self, cmd: ImmediateCommand) -> None: 257 if cmd.name == "interrupt": 258 with self._interrupt_lock: 259 interrupt_local_process() 260 261 def _should_keep_going(self) -> bool: 262 return True 263 264 def get_option(self, name, default=None): 265 section, subname = self._parse_option_name(name) 266 val = self._get_ini().get(section, subname, fallback=default) 267 try: 268 return ast.literal_eval(val) 269 except Exception: 270 return val 271 272 def set_option(self, name, value): 273 ini = self._get_ini() 274 section, subname = self._parse_option_name(name) 275 if not ini.has_section(section): 276 ini.add_section(section) 277 if not isinstance(value, str): 278 value = repr(value) 279 ini.set(section, subname, value) 280 self.save_settings() 281 282 def _parse_option_name(self, name): 283 if "." in name: 284 return name.split(".", 1) 285 else: 286 return "general", name 287 288 def _get_ini(self): 289 if self._ini is None: 290 import configparser 291 292 self._ini = configparser.ConfigParser(interpolation=None) 293 self._ini.read(_CONFIG_FILENAME) 294 295 return self._ini 296 297 def save_settings(self): 298 if self._ini is None: 299 return 300 301 with open(_CONFIG_FILENAME, "w") as fp: 302 self._ini.write(fp) 303 304 def switch_env_to_script_mode(self, cmd): 305 if "" in sys.path: 306 sys.path.remove("") # current directory 307 308 filename = cmd.args[0] 309 if os.path.isfile(filename): 310 sys.path.insert(0, os.path.abspath(os.path.dirname(filename))) 311 __main__.__dict__["__file__"] = filename 312 313 def _custom_import(self, *args, **kw): 314 module = self._original_import(*args, **kw) 315 316 if not hasattr(module, "__name__"): 317 return module 318 319 # module specific handlers 320 for handler in self._import_handlers.get(module.__name__, []): 321 try: 322 handler(module) 323 except Exception as e: 324 self._report_internal_exception(e) 325 326 # general handlers 327 for handler in self._import_handlers.get("*", []): 328 try: 329 handler(module) 330 except Exception as e: 331 self._report_internal_exception(e) 332 333 return module 334 335 def _load_plugins(self): 336 # built-in plugins 337 try: 338 import thonny.plugins.backend # pylint: disable=redefined-outer-name 339 except ImportError: 340 # May happen eg. in ssh session 341 return 342 343 try_load_modules_with_frontend_sys_path("thonnycontrib") 344 self._load_plugins_from_path(thonny.plugins.backend.__path__, "thonny.plugins.backend.") 345 346 # 3rd party plugins from namespace package 347 try: 348 import thonnycontrib.backend # @UnresolvedImport 349 except ImportError: 350 # No 3rd party plugins installed 351 pass 352 else: 353 self._load_plugins_from_path(thonnycontrib.backend.__path__, "thonnycontrib.backend.") 354 355 def _load_plugins_from_path(self, path, prefix): 356 load_function_name = "load_plugin" 357 for _, module_name, _ in sorted(pkgutil.iter_modules(path, prefix), key=lambda x: x[1]): 358 try: 359 m = importlib.import_module(module_name) 360 if hasattr(m, load_function_name): 361 f = getattr(m, load_function_name) 362 sig = inspect.signature(f) 363 if len(sig.parameters) == 0: 364 f() 365 else: 366 f(self) 367 except Exception as e: 368 logger.exception("Failed loading plugin '" + module_name + "'", exc_info=e) 369 370 def _cmd_get_environment_info(self, cmd): 371 return ToplevelResponse( 372 main_dir=self._main_dir, 373 sys_path=sys.path, 374 usersitepackages=site.getusersitepackages() if site.ENABLE_USER_SITE else None, 375 prefix=sys.prefix, 376 welcome_text="Python " + get_python_version_string(), 377 executable=sys.executable, 378 exe_dirs=get_exe_dirs(), 379 in_venv=( 380 hasattr(sys, "base_prefix") 381 and sys.base_prefix != sys.prefix 382 or hasattr(sys, "real_prefix") 383 and getattr(sys, "real_prefix") != sys.prefix 384 ), 385 python_version=get_python_version_string(), 386 cwd=os.getcwd(), 387 ) 388 389 def _cmd_cd(self, cmd): 390 if len(cmd.args) == 1: 391 path = cmd.args[0] 392 try: 393 os.chdir(path) 394 return ToplevelResponse() 395 except FileNotFoundError: 396 raise UserError("No such folder: " + path) 397 except OSError as e: 398 raise UserError("\n".join(traceback.format_exception_only(type(e), e))) 399 else: 400 raise UserError("cd takes one parameter") 401 402 def _cmd_Run(self, cmd): 403 self.switch_env_to_script_mode(cmd) 404 return self._execute_file(cmd, SimpleRunner) 405 406 def _cmd_run(self, cmd): 407 return self._execute_file(cmd, SimpleRunner) 408 409 def _cmd_FastDebug(self, cmd): 410 self.switch_env_to_script_mode(cmd) 411 return self._execute_file(cmd, FastTracer) 412 413 def _cmd_Debug(self, cmd): 414 self.switch_env_to_script_mode(cmd) 415 return self._execute_file(cmd, NiceTracer) 416 417 def _cmd_debug(self, cmd): 418 return self._execute_file(cmd, NiceTracer) 419 420 def _cmd_execute_source(self, cmd): 421 """Executes Python source entered into shell""" 422 self._check_update_tty_mode(cmd) 423 filename = "<pyshell>" 424 source = cmd.source.strip() 425 426 try: 427 root = ast.parse(source, filename=filename, mode="exec") 428 except SyntaxError as e: 429 error = "".join(traceback.format_exception_only(type(e), e)) 430 sys.stderr.write(error) 431 return ToplevelResponse() 432 433 assert isinstance(root, ast.Module) 434 435 result_attributes = self._execute_source( 436 source, 437 filename, 438 "repl", 439 NiceTracer if getattr(cmd, "debug_mode", False) else SimpleRunner, 440 cmd, 441 ) 442 443 return ToplevelResponse(command_name="execute_source", **result_attributes) 444 445 def _cmd_execute_system_command(self, cmd): 446 self._check_update_tty_mode(cmd) 447 try: 448 returncode = execute_system_command(cmd, disconnect_stdin=True) 449 return {"returncode": returncode} 450 except Exception as e: 451 logger.exception("Could not execute system command %s", cmd, exc_info=e) 452 453 def _cmd_process_gui_events(self, cmd): 454 # advance the event loop 455 try: 456 # First try Tkinter. 457 # Need to update even when tkinter._default_root is None 458 # because otherwise destroyed window will stay up in macOS. 459 460 # When switching between closed user Tk window and another window, 461 # the closed window may reappear in IDLE and CLI REPL 462 tcl = self._get_tcl() 463 if tcl is not None: 464 # http://bugs.python.org/issue989712 465 # http://bugs.python.org/file6090/run.py.diff 466 # https://bugs.python.org/review/989712/diff/4528/Lib/idlelib/run.py 467 tcl.eval("update") 468 return {} 469 else: 470 # Try Qt only when Tkinter is not used 471 app = self._get_qt_app() 472 if app is not None: 473 app.processEvents() 474 return {} 475 476 except Exception: 477 pass 478 479 return {"gui_is_active": False} 480 481 def _cmd_get_globals(self, cmd): 482 # warnings.warn("_cmd_get_globals is deprecated for CPython") 483 try: 484 return InlineResponse( 485 "get_globals", 486 module_name=cmd.module_name, 487 globals=self.export_globals(cmd.module_name), 488 ) 489 except Exception as e: 490 return InlineResponse("get_globals", module_name=cmd.module_name, error=str(e)) 491 492 def _cmd_get_frame_info(self, cmd): 493 atts = {} 494 try: 495 # TODO: make it work also in past states 496 frame, location = self._lookup_frame_by_id(cmd["frame_id"]) 497 if frame is None: 498 atts["error"] = "Frame not found" 499 else: 500 atts["code_name"] = frame.f_code.co_name 501 atts["module_name"] = frame.f_globals["__name__"] 502 atts["locals"] = ( 503 None 504 if frame.f_locals is frame.f_globals 505 else self.export_variables(frame.f_locals) 506 ) 507 atts["globals"] = self.export_variables(frame.f_globals) 508 atts["freevars"] = frame.f_code.co_freevars 509 atts["location"] = location 510 except Exception as e: 511 atts["error"] = str(e) 512 513 return InlineResponse("get_frame_info", frame_id=cmd.frame_id, **atts) 514 515 def _cmd_get_active_distributions(self, cmd): 516 try: 517 # if it is called after first installation to user site packages 518 # this dir is not yet in sys.path 519 if ( 520 site.ENABLE_USER_SITE 521 and site.getusersitepackages() 522 and os.path.exists(site.getusersitepackages()) 523 and site.getusersitepackages() not in sys.path 524 ): 525 # insert before first site packages item 526 for i, item in enumerate(sys.path): 527 if "site-packages" in item or "dist-packages" in item: 528 sys.path.insert(i, site.getusersitepackages()) 529 break 530 else: 531 sys.path.append(site.getusersitepackages()) 532 533 import pkg_resources 534 535 pkg_resources._initialize_master_working_set() 536 dists = { 537 dist.key: { 538 "project_name": dist.project_name, 539 "key": dist.key, 540 "location": dist.location, 541 "version": dist.version, 542 } 543 for dist in pkg_resources.working_set # pylint: disable=not-an-iterable 544 } 545 546 return InlineResponse( 547 "get_active_distributions", 548 distributions=dists, 549 usersitepackages=site.getusersitepackages() if site.ENABLE_USER_SITE else None, 550 ) 551 except Exception: 552 return InlineResponse("get_active_distributions", error=traceback.format_exc()) 553 554 def _cmd_get_locals(self, cmd): 555 for frame in inspect.stack(): 556 if id(frame) == cmd.frame_id: 557 return InlineResponse("get_locals", locals=self.export_variables(frame.f_locals)) 558 559 raise RuntimeError("Frame '{0}' not found".format(cmd.frame_id)) 560 561 def _cmd_get_heap(self, cmd): 562 result = {} 563 for key in self._heap: 564 result[key] = self.export_value(self._heap[key]) 565 566 return InlineResponse("get_heap", heap=result) 567 568 def _cmd_shell_autocomplete(self, cmd): 569 try_load_modules_with_frontend_sys_path(["jedi", "parso"]) 570 571 error = None 572 try: 573 import jedi 574 except ImportError: 575 jedi = None 576 completions = [] 577 error = "Could not import jedi" 578 else: 579 try: 580 with warnings.catch_warnings(): 581 jedi_completions = jedi_utils.get_interpreter_completions( 582 cmd.source, [__main__.__dict__] 583 ) 584 completions = self._export_completions(jedi_completions) 585 except Exception as e: 586 completions = [] 587 logger.info("Autocomplete error", exc_info=e) 588 error = "Autocomplete error: " + str(e) 589 590 return InlineResponse( 591 "shell_autocomplete", source=cmd.source, completions=completions, error=error 592 ) 593 594 def _cmd_editor_autocomplete(self, cmd): 595 try_load_modules_with_frontend_sys_path(["jedi", "parso"]) 596 597 error = None 598 try: 599 import jedi 600 601 self._debug(jedi.__file__, sys.path) 602 with warnings.catch_warnings(): 603 jedi_completions = jedi_utils.get_script_completions( 604 cmd.source, cmd.row, cmd.column, cmd.filename 605 ) 606 completions = self._export_completions(jedi_completions) 607 608 except ImportError: 609 jedi = None 610 completions = [] 611 error = "Could not import jedi" 612 except Exception as e: 613 completions = [] 614 logger.info("Autocomplete error", exc_info=e) 615 error = "Autocomplete error: " + str(e) 616 617 return InlineResponse( 618 "editor_autocomplete", 619 source=cmd.source, 620 row=cmd.row, 621 column=cmd.column, 622 filename=cmd.filename, 623 completions=completions, 624 error=error, 625 ) 626 627 def _cmd_get_object_info(self, cmd): 628 if isinstance(self._current_executor, NiceTracer) and self._current_executor.is_in_past(): 629 info = {"id": cmd.object_id, "error": "past info not available"} 630 631 elif cmd.object_id in self._heap: 632 value = self._heap[cmd.object_id] 633 attributes = {} 634 if cmd.include_attributes: 635 for name in dir(value): 636 if not name.startswith("__") or cmd.all_attributes: 637 # attributes[name] = inspect.getattr_static(value, name) 638 try: 639 attributes[name] = getattr(value, name) 640 except Exception: 641 pass 642 643 self._heap[id(type(value))] = type(value) 644 info = { 645 "id": cmd.object_id, 646 "repr": repr(value), 647 "type": str(type(value)), 648 "full_type_name": str(type(value)) 649 .replace("<class '", "") 650 .replace("'>", "") 651 .strip(), 652 "attributes": self.export_variables(attributes), 653 } 654 655 if isinstance(value, io.TextIOWrapper): 656 self._add_file_handler_info(value, info) 657 elif isinstance( 658 value, 659 ( 660 types.BuiltinFunctionType, 661 types.BuiltinMethodType, 662 types.FunctionType, 663 types.LambdaType, 664 types.MethodType, 665 ), 666 ): 667 self._add_function_info(value, info) 668 elif isinstance(value, (list, tuple, set)): 669 self._add_elements_info(value, info) 670 elif isinstance(value, dict): 671 self._add_entries_info(value, info) 672 elif isinstance(value, float): 673 self._add_float_info(value, info) 674 elif hasattr(value, "image_data"): 675 info["image_data"] = value.image_data 676 677 for tweaker in self._object_info_tweakers: 678 try: 679 tweaker(value, info, cmd) 680 except Exception: 681 logger.exception("Failed object info tweaker: " + str(tweaker)) 682 683 else: 684 info = {"id": cmd.object_id, "error": "object info not available"} 685 686 return InlineResponse("get_object_info", id=cmd.object_id, info=info) 687 688 def _cmd_mkdir(self, cmd): 689 os.mkdir(cmd.path) 690 691 def _cmd_delete(self, cmd): 692 for path in cmd.paths: 693 try: 694 if os.path.isfile(path): 695 os.remove(path) 696 elif os.path.isdir(path): 697 import shutil 698 699 shutil.rmtree(path) 700 except Exception as e: 701 print("Could not delete %s: %s" % (path, str(e)), file=sys.stderr) 702 703 def _get_sep(self) -> str: 704 return os.path.sep 705 706 def _get_dir_children_info( 707 self, path: str, include_hidden: bool = False 708 ) -> Optional[Dict[str, Dict]]: 709 return get_single_dir_child_data(path, include_hidden) 710 711 def _get_path_info(self, path: str) -> Optional[Dict]: 712 713 try: 714 if not os.path.exists(path): 715 return None 716 except OSError: 717 pass 718 719 try: 720 kind = "dir" if os.path.isdir(path) else "file" 721 return { 722 "path": path, 723 "kind": kind, 724 "size": None if kind == "dir" else os.path.getsize(path), 725 "modified": os.path.getmtime(path), 726 "error": None, 727 } 728 except OSError as e: 729 return { 730 "path": path, 731 "kind": None, 732 "size": None, 733 "modified": None, 734 "error": str(e), 735 } 736 737 def _export_completions(self, jedi_completions): 738 result = [] 739 for c in jedi_completions: 740 if not c.name.startswith("__"): 741 record = { 742 "name": c.name, 743 "complete": c.complete, 744 "type": c.type, 745 "description": c.description, 746 } 747 """ TODO: 748 try: 749 if c.type in ["class", "module", "function"]: 750 if c.type == "function": 751 record["docstring"] = c.docstring() 752 else: 753 record["docstring"] = c.description + "\n" + c.docstring() 754 except Exception: 755 pass 756 """ 757 result.append(record) 758 return result 759 760 def _get_tcl(self): 761 if self._tcl is not None: 762 return self._tcl 763 764 tkinter = sys.modules.get("tkinter") 765 if tkinter is None: 766 return None 767 768 if self._tcl is None: 769 try: 770 self._tcl = tkinter.Tcl() 771 except Exception as e: 772 logger.error("Could not get Tcl", exc_info=e) 773 self._tcl = None 774 return None 775 776 return self._tcl 777 778 def _get_qt_app(self): 779 mod = sys.modules.get("PyQt5.QtCore") 780 if mod is None: 781 mod = sys.modules.get("PyQt4.QtCore") 782 if mod is None: 783 mod = sys.modules.get("PySide.QtCore") 784 if mod is None: 785 return None 786 787 app_class = getattr(mod, "QCoreApplication", None) 788 if app_class is not None: 789 try: 790 return app_class.instance() 791 except Exception: 792 return None 793 else: 794 return None 795 796 def _add_file_handler_info(self, value, info): 797 try: 798 assert isinstance(value.name, str) 799 assert value.mode in ("r", "rt", "tr", "br", "rb") 800 assert value.errors in ("strict", None) 801 assert value.newlines is None or value.tell() > 0 802 # TODO: cache the content 803 # TODO: don't read too big files 804 with open(value.name, encoding=value.encoding) as f: 805 info["file_encoding"] = f.encoding 806 info["file_content"] = f.read() 807 info["file_tell"] = value.tell() 808 except Exception as e: 809 info["file_error"] = "Could not get file content, error:" + str(e) 810 811 def _add_function_info(self, value, info): 812 try: 813 info["source"] = inspect.getsource(value) 814 except Exception: 815 pass 816 817 def _add_elements_info(self, value, info): 818 info["elements"] = [] 819 for element in value: 820 info["elements"].append(self.export_value(element)) 821 822 def _add_entries_info(self, value, info): 823 info["entries"] = [] 824 for key in value: 825 info["entries"].append((self.export_value(key), self.export_value(value[key]))) 826 827 def _add_float_info(self, value, info): 828 if not value.is_integer(): 829 info["as_integer_ratio"] = value.as_integer_ratio() 830 831 def _execute_file(self, cmd, executor_class): 832 self._check_update_tty_mode(cmd) 833 834 if len(cmd.args) >= 1: 835 sys.argv = cmd.args 836 filename = cmd.args[0] 837 if filename == "-c" or os.path.isabs(filename): 838 full_filename = filename 839 else: 840 full_filename = os.path.abspath(filename) 841 842 if full_filename == "-c": 843 source = cmd.source 844 else: 845 with tokenize.open(full_filename) as fp: 846 source = fp.read() 847 848 for preproc in self._source_preprocessors: 849 source = preproc(source, cmd) 850 851 result_attributes = self._execute_source( 852 source, full_filename, "exec", executor_class, cmd, self._ast_postprocessors 853 ) 854 result_attributes["filename"] = full_filename 855 return ToplevelResponse(command_name=cmd.name, **result_attributes) 856 else: 857 raise UserError("Command '%s' takes at least one argument" % cmd.name) 858 859 def _execute_source( 860 self, source, filename, execution_mode, executor_class, cmd, ast_postprocessors=[] 861 ): 862 self._current_executor = executor_class(self, cmd) 863 864 try: 865 return self._current_executor.execute_source( 866 source, filename, execution_mode, ast_postprocessors 867 ) 868 finally: 869 self._current_executor = None 870 871 def _install_repl_helper(self): 872 def _handle_repl_value(obj): 873 if obj is not None: 874 try: 875 obj_repr = repr(obj) 876 if len(obj_repr) > 5000: 877 obj_repr = obj_repr[:5000] + "…" 878 except Exception as e: 879 obj_repr = "<repr error: " + str(e) + ">" 880 print(OBJECT_LINK_START % id(obj), obj_repr, OBJECT_LINK_END, sep="") 881 self._heap[id(obj)] = obj 882 builtins._ = obj 883 884 setattr(builtins, _REPL_HELPER_NAME, _handle_repl_value) 885 886 def _install_fake_streams(self): 887 self._original_stdin = sys.stdin 888 self._original_stdout = sys.stdout 889 self._original_stderr = sys.stderr 890 891 # yes, both out and err will be directed to out (but with different tags) 892 # this allows client to see the order of interleaving writes to stdout/stderr 893 sys.stdin = FakeInputStream(self, sys.stdin) 894 sys.stdout = FakeOutputStream(self, sys.stdout, "stdout") 895 sys.stderr = FakeOutputStream(self, sys.stdout, "stderr") 896 897 # fake it properly: replace also "backup" streams 898 sys.__stdin__ = sys.stdin 899 sys.__stdout__ = sys.stdout 900 sys.__stderr__ = sys.stderr 901 902 def _install_custom_import(self): 903 self._original_import = builtins.__import__ 904 builtins.__import__ = self._custom_import 905 906 def _restore_original_import(self): 907 builtins.__import__ = self._original_import 908 909 def send_message(self, msg: MessageFromBackend) -> None: 910 sys.stdout.flush() 911 912 if isinstance(msg, ToplevelResponse): 913 if "cwd" not in msg: 914 msg["cwd"] = os.getcwd() 915 if "globals" not in msg: 916 msg["globals"] = self.export_globals() 917 918 self._original_stdout.write(serialize_message(msg) + "\n") 919 self._original_stdout.flush() 920 921 def export_value(self, value, max_repr_length=5000): 922 self._heap[id(value)] = value 923 try: 924 rep = repr(value) 925 except Exception: 926 # See https://bitbucket.org/plas/thonny/issues/584/problem-with-thonnys-back-end-obj-no 927 rep = "??? <repr error>" 928 929 if len(rep) > max_repr_length: 930 rep = rep[:max_repr_length] + "…" 931 932 return ValueInfo(id(value), rep) 933 934 def export_variables(self, variables): 935 result = {} 936 with warnings.catch_warnings(): 937 warnings.simplefilter("ignore") 938 for name in variables: 939 if not name.startswith("__"): 940 result[name] = self.export_value(variables[name], 100) 941 942 return result 943 944 def export_globals(self, module_name="__main__"): 945 if module_name in sys.modules: 946 return self.export_variables(sys.modules[module_name].__dict__) 947 else: 948 raise RuntimeError("Module '{0}' is not loaded".format(module_name)) 949 950 def _debug(self, *args): 951 logger.debug("MainCPythonBackend: " + str(args)) 952 953 def _enter_io_function(self): 954 self._io_level += 1 955 956 def _exit_io_function(self): 957 self._io_level -= 1 958 959 def is_doing_io(self): 960 return self._io_level > 0 961 962 def _export_stack(self, newest_frame, relevance_checker=None): 963 result = [] 964 965 system_frame = newest_frame 966 967 while system_frame is not None: 968 module_name = system_frame.f_globals["__name__"] 969 code_name = system_frame.f_code.co_name 970 971 if not relevance_checker or relevance_checker(system_frame): 972 source, firstlineno, in_library = self._get_frame_source_info(system_frame) 973 974 result.insert( 975 0, 976 FrameInfo( 977 # TODO: can this id be reused by a later frame? 978 # Need to store the reference to avoid GC? 979 # I guess it is not required, as id will be required 980 # only for stacktrace inspection, and sys.last_exception 981 # will have the reference anyway 982 # (NiceTracer has its own reference keeping) 983 id=id(system_frame), 984 filename=system_frame.f_code.co_filename, 985 module_name=module_name, 986 code_name=code_name, 987 locals=self.export_variables(system_frame.f_locals), 988 globals=self.export_variables(system_frame.f_globals), 989 freevars=system_frame.f_code.co_freevars, 990 source=source, 991 lineno=system_frame.f_lineno, 992 firstlineno=firstlineno, 993 in_library=in_library, 994 event="line", 995 focus=TextRange(system_frame.f_lineno, 0, system_frame.f_lineno + 1, 0), 996 node_tags=None, 997 current_statement=None, 998 current_evaluations=None, 999 current_root_expression=None, 1000 ), 1001 ) 1002 1003 if module_name == "__main__" and code_name == "<module>": 1004 # this was last frame relevant to the user 1005 break 1006 1007 system_frame = system_frame.f_back 1008 1009 assert result # not empty 1010 return result 1011 1012 def _lookup_frame_by_id(self, frame_id): 1013 def lookup_from_stack(frame): 1014 if frame is None: 1015 return None 1016 elif id(frame) == frame_id: 1017 return frame 1018 else: 1019 return lookup_from_stack(frame.f_back) 1020 1021 def lookup_from_tb(entry): 1022 if entry is None: 1023 return None 1024 elif id(entry.tb_frame) == frame_id: 1025 return entry.tb_frame 1026 else: 1027 return lookup_from_tb(entry.tb_next) 1028 1029 result = lookup_from_stack(inspect.currentframe()) 1030 if result is not None: 1031 return result, "stack" 1032 1033 if getattr(sys, "last_traceback"): 1034 result = lookup_from_tb(getattr(sys, "last_traceback")) 1035 if result: 1036 return result, "last_traceback" 1037 1038 _, _, tb = sys.exc_info() 1039 return lookup_from_tb(tb), "current_exception" 1040 1041 def _get_frame_source_info(self, frame): 1042 fid = id(frame) 1043 if fid not in self._source_info_by_frame: 1044 self._source_info_by_frame[fid] = _fetch_frame_source_info(frame) 1045 1046 return self._source_info_by_frame[fid] 1047 1048 def _prepare_user_exception(self): 1049 e_type, e_value, e_traceback = sys.exc_info() 1050 sys.last_type, sys.last_value, sys.last_traceback = (e_type, e_value, e_traceback) 1051 1052 processed_tb = traceback.extract_tb(e_traceback) 1053 1054 tb = e_traceback 1055 while tb.tb_next is not None: 1056 tb = tb.tb_next 1057 last_frame = tb.tb_frame 1058 1059 if e_type is SyntaxError: 1060 # Don't show ast frame 1061 while last_frame.f_code.co_filename and last_frame.f_code.co_filename == ast.__file__: 1062 last_frame = last_frame.f_back 1063 1064 if e_type is SyntaxError: 1065 msg = ( 1066 traceback.format_exception_only(e_type, e_value)[-1] 1067 .replace(e_type.__name__ + ":", "") 1068 .strip() 1069 ) 1070 else: 1071 msg = str(e_value) 1072 1073 return { 1074 "type_name": e_type.__name__, 1075 "message": msg, 1076 "stack": self._export_stack(last_frame), 1077 "items": format_exception_with_frame_info(e_type, e_value, e_traceback), 1078 "filename": getattr(e_value, "filename", processed_tb[-1].filename), 1079 "lineno": getattr(e_value, "lineno", processed_tb[-1].lineno), 1080 "col_offset": getattr(e_value, "offset", None), 1081 "line": getattr(e_value, "text", processed_tb[-1].line), 1082 } 1083 1084 def _check_update_tty_mode(self, cmd): 1085 if "tty_mode" in cmd: 1086 self._tty_mode = cmd["tty_mode"] 1087 1088 1089class FakeStream: 1090 def __init__(self, backend: MainCPythonBackend, target_stream): 1091 self._backend = backend 1092 self._target_stream = target_stream 1093 self._processed_symbol_count = 0 1094 1095 def isatty(self): 1096 return self._backend._tty_mode and (os.name != "nt" or "click" not in sys.modules) 1097 1098 def __getattr__(self, name): 1099 # TODO: is it safe to perform those other functions without notifying backend 1100 # via _enter_io_function? 1101 return getattr(self._target_stream, name) 1102 1103 1104class FakeOutputStream(FakeStream): 1105 def __init__(self, backend: MainCPythonBackend, target_stream, stream_name): 1106 FakeStream.__init__(self, backend, target_stream) 1107 self._stream_name = stream_name 1108 1109 def write(self, data): 1110 try: 1111 self._backend._enter_io_function() 1112 # click may send bytes instead of strings 1113 if isinstance(data, bytes): 1114 data = data.decode(errors="replace") 1115 1116 if data != "": 1117 self._backend._send_output(data=data, stream_name=self._stream_name) 1118 self._processed_symbol_count += len(data) 1119 finally: 1120 self._backend._exit_io_function() 1121 1122 def writelines(self, lines): 1123 try: 1124 self._backend._enter_io_function() 1125 self.write("".join(lines)) 1126 finally: 1127 self._backend._exit_io_function() 1128 1129 1130class FakeInputStream(FakeStream): 1131 def __init__(self, backend: MainCPythonBackend, target_stream): 1132 super().__init__(backend, target_stream) 1133 self._buffer = "" 1134 self._eof = False 1135 1136 def _generic_read(self, method, original_limit): 1137 if original_limit is None: 1138 effective_limit = -1 1139 elif method == "readlines" and original_limit > -1: 1140 # NB! size hint is defined in weird way 1141 # "no more lines will be read if the total size (in bytes/characters) 1142 # of all lines so far **exceeds** the hint". 1143 effective_limit = original_limit + 1 1144 else: 1145 effective_limit = original_limit 1146 1147 try: 1148 self._backend._enter_io_function() 1149 while True: 1150 if effective_limit == 0: 1151 result = "" 1152 break 1153 1154 elif effective_limit > 0 and len(self._buffer) >= effective_limit: 1155 result = self._buffer[:effective_limit] 1156 self._buffer = self._buffer[effective_limit:] 1157 if method == "readlines" and not result.endswith("\n") and "\n" in self._buffer: 1158 # limit is just a hint 1159 # https://docs.python.org/3/library/io.html#io.IOBase.readlines 1160 extra = self._buffer[: self._buffer.find("\n") + 1] 1161 result += extra 1162 self._buffer = self._buffer[len(extra) :] 1163 break 1164 1165 elif method == "readline" and "\n" in self._buffer: 1166 pos = self._buffer.find("\n") + 1 1167 result = self._buffer[:pos] 1168 self._buffer = self._buffer[pos:] 1169 break 1170 1171 elif self._eof: 1172 result = self._buffer 1173 self._buffer = "" 1174 self._eof = False # That's how official implementation does 1175 break 1176 1177 else: 1178 self._backend.send_message( 1179 BackendEvent("InputRequest", method=method, limit=original_limit) 1180 ) 1181 msg = self._backend._fetch_next_incoming_message() 1182 if isinstance(msg, InputSubmission): 1183 self._buffer += msg.data 1184 self._processed_symbol_count += len(msg.data) 1185 elif isinstance(msg, EOFCommand): 1186 self._eof = True 1187 elif isinstance(msg, InlineCommand): 1188 self._backend._handle_normal_command(msg) 1189 else: 1190 raise RuntimeError( 1191 "Wrong type of command (%r) when waiting for input" % (msg,) 1192 ) 1193 1194 return result 1195 1196 finally: 1197 self._backend._exit_io_function() 1198 1199 def read(self, limit=-1): 1200 return self._generic_read("read", limit) 1201 1202 def readline(self, limit=-1): 1203 return self._generic_read("readline", limit) 1204 1205 def readlines(self, limit=-1): 1206 return self._generic_read("readlines", limit).splitlines(True) 1207 1208 def __next__(self): 1209 result = self.readline() 1210 if not result: 1211 raise StopIteration 1212 1213 return result 1214 1215 def __iter__(self): 1216 return self 1217 1218 1219def prepare_hooks(method): 1220 @functools.wraps(method) 1221 def wrapper(self, *args, **kwargs): 1222 try: 1223 sys.meta_path.insert(0, self) 1224 self._backend._install_custom_import() 1225 return method(self, *args, **kwargs) 1226 finally: 1227 del sys.meta_path[0] 1228 if hasattr(self._backend, "_original_import"): 1229 self._backend._restore_original_import() 1230 1231 return wrapper 1232 1233 1234def return_execution_result(method): 1235 @functools.wraps(method) 1236 def wrapper(self, *args, **kwargs): 1237 try: 1238 result = method(self, *args, **kwargs) 1239 if result is not None: 1240 return result 1241 return {"context_info": "after normal execution"} 1242 except Exception: 1243 return {"user_exception": self._backend._prepare_user_exception()} 1244 1245 return wrapper 1246 1247 1248class Executor: 1249 def __init__(self, backend: MainCPythonBackend, original_cmd): 1250 self._backend = backend 1251 self._original_cmd = original_cmd 1252 self._main_module_path = None 1253 1254 def execute_source(self, source, filename, mode, ast_postprocessors): 1255 if isinstance(source, str): 1256 # TODO: simplify this or make sure encoding is correct 1257 source = source.encode("utf-8") 1258 1259 if os.path.exists(filename): 1260 self._main_module_path = filename 1261 1262 global_vars = __main__.__dict__ 1263 1264 try: 1265 if mode == "repl": 1266 assert not ast_postprocessors 1267 # Useful in shell to get last expression value in multi-statement block 1268 root = self._prepare_ast(source, filename, "exec") 1269 # https://bugs.python.org/issue35894 1270 # https://github.com/pallets/werkzeug/pull/1552/files#diff-9e75ca133f8601f3b194e2877d36df0eR950 1271 module = ast.parse("") 1272 module.body = root.body 1273 self._instrument_repl_code(module) 1274 statements = compile(module, filename, "exec") 1275 elif mode == "exec": 1276 root = self._prepare_ast(source, filename, mode) 1277 for func in ast_postprocessors: 1278 func(root) 1279 statements = compile(root, filename, mode) 1280 else: 1281 raise ValueError("Unknown mode", mode) 1282 1283 return self._execute_prepared_user_code(statements, global_vars) 1284 except SyntaxError: 1285 return {"user_exception": self._backend._prepare_user_exception()} 1286 except SystemExit: 1287 return {"SystemExit": True} 1288 except Exception as e: 1289 self._backend._report_internal_exception(e) 1290 return {} 1291 1292 @return_execution_result 1293 @prepare_hooks 1294 def _execute_prepared_user_code(self, statements, global_vars): 1295 exec(statements, global_vars) 1296 1297 def find_spec(self, fullname, path=None, target=None): 1298 """override in subclass for custom-loading user modules""" 1299 return None 1300 1301 def _prepare_ast(self, source, filename, mode): 1302 return ast.parse(source, filename, mode) 1303 1304 def _instrument_repl_code(self, root): 1305 # modify all expression statements to print and register their non-None values 1306 for node in ast.walk(root): 1307 if ( 1308 isinstance(node, ast.FunctionDef) 1309 or hasattr(ast, "AsyncFunctionDef") 1310 and isinstance(node, ast.AsyncFunctionDef) 1311 ): 1312 first_stmt = node.body[0] 1313 if isinstance(first_stmt, ast.Expr) and isinstance(first_stmt.value, ast.Str): 1314 first_stmt.contains_docstring = True 1315 if isinstance(node, ast.Expr) and not getattr(node, "contains_docstring", False): 1316 node.value = ast.Call( 1317 func=ast.Name(id=_REPL_HELPER_NAME, ctx=ast.Load()), 1318 args=[node.value], 1319 keywords=[], 1320 ) 1321 ast.fix_missing_locations(node) 1322 1323 1324class SimpleRunner(Executor): 1325 pass 1326 1327 1328class Tracer(Executor): 1329 def __init__(self, backend, original_cmd): 1330 super().__init__(backend, original_cmd) 1331 self._thonny_src_dir = os.path.dirname(sys.modules["thonny"].__file__) 1332 self._fresh_exception = None 1333 self._prev_breakpoints = {} 1334 self._last_reported_frame_ids = set() 1335 self._affected_frame_ids_per_exc_id = {} 1336 self._canonic_path_cache = {} 1337 self._file_interest_cache = {} 1338 self._file_breakpoints_cache = {} 1339 self._command_completion_handler = None 1340 1341 # first (automatic) stepping command depends on whether any breakpoints were set or not 1342 breakpoints = self._original_cmd.breakpoints 1343 assert isinstance(breakpoints, dict) 1344 if breakpoints: 1345 command_name = "resume" 1346 else: 1347 command_name = "step_into" 1348 1349 self._current_command = DebuggerCommand( 1350 command_name, 1351 state=None, 1352 focus=None, 1353 frame_id=None, 1354 exception=None, 1355 breakpoints=breakpoints, 1356 ) 1357 1358 self._initialize_new_command(None) 1359 1360 def _get_canonic_path(self, path): 1361 # adapted from bdb 1362 result = self._canonic_path_cache.get(path) 1363 if result is None: 1364 if path.startswith("<"): 1365 result = path 1366 else: 1367 result = os.path.normcase(os.path.abspath(path)) 1368 1369 self._canonic_path_cache[path] = result 1370 1371 return result 1372 1373 def _trace(self, frame, event, arg): 1374 raise NotImplementedError() 1375 1376 def _execute_prepared_user_code(self, statements, global_vars): 1377 try: 1378 sys.settrace(self._trace) 1379 if hasattr(sys, "breakpointhook"): 1380 old_breakpointhook = sys.breakpointhook 1381 sys.breakpointhook = self._breakpointhook 1382 1383 return super()._execute_prepared_user_code(statements, global_vars) 1384 finally: 1385 sys.settrace(None) 1386 if hasattr(sys, "breakpointhook"): 1387 sys.breakpointhook = old_breakpointhook 1388 1389 def _is_interesting_frame(self, frame): 1390 code = frame.f_code 1391 1392 return not ( 1393 code is None 1394 or code.co_filename is None 1395 or not self._is_interesting_module_file(code.co_filename) 1396 or code.co_flags & _CO_GENERATOR 1397 and code.co_flags & _CO_COROUTINE 1398 and code.co_flags & _CO_ITERABLE_COROUTINE 1399 and code.co_flags & _CO_ASYNC_GENERATOR 1400 # or "importlib._bootstrap" in code.co_filename 1401 or code.co_name in ["<listcomp>", "<setcomp>", "<dictcomp>"] 1402 ) 1403 1404 def _is_interesting_module_file(self, path): 1405 # interesting files are the files in the same directory as main module 1406 # or the ones with breakpoints 1407 # When command is "resume", then only modules with breakpoints are interesting 1408 # (used to be more flexible, but this caused problems 1409 # when main script was in ~/. Then user site library became interesting as well) 1410 1411 result = self._file_interest_cache.get(path, None) 1412 if result is not None: 1413 return result 1414 1415 _, extension = os.path.splitext(path.lower()) 1416 1417 result = ( 1418 self._get_breakpoints_in_file(path) 1419 or self._main_module_path is not None 1420 and is_same_path(path, self._main_module_path) 1421 or extension in (".py", ".pyw") 1422 and ( 1423 self._current_command.get("allow_stepping_into_libraries", False) 1424 or ( 1425 path_startswith(path, os.path.dirname(self._main_module_path)) 1426 # main module may be at the root of the fs 1427 and not path_startswith(path, sys.prefix) 1428 and not path_startswith(path, sys.base_prefix) 1429 and not path_startswith(path, site.getusersitepackages() or "usersitenotexists") 1430 ) 1431 ) 1432 and not path_startswith(path, self._thonny_src_dir) 1433 ) 1434 1435 self._file_interest_cache[path] = result 1436 1437 return result 1438 1439 def _is_interesting_exception(self, frame, arg): 1440 return arg[0] not in (StopIteration, StopAsyncIteration) 1441 1442 def _fetch_next_debugger_command(self, current_frame): 1443 while True: 1444 cmd = self._backend._fetch_next_incoming_message() 1445 if isinstance(cmd, InlineCommand): 1446 self._backend._handle_normal_command(cmd) 1447 else: 1448 assert isinstance(cmd, DebuggerCommand) 1449 self._prev_breakpoints = self._current_command.breakpoints 1450 self._current_command = cmd 1451 self._initialize_new_command(current_frame) 1452 return 1453 1454 def _initialize_new_command(self, current_frame): 1455 self._command_completion_handler = getattr( 1456 self, "_cmd_%s_completed" % self._current_command.name 1457 ) 1458 1459 if self._current_command.breakpoints != self._prev_breakpoints: 1460 self._file_interest_cache = {} # because there may be new breakpoints 1461 self._file_breakpoints_cache = {} 1462 for path, linenos in self._current_command.breakpoints.items(): 1463 self._file_breakpoints_cache[path] = linenos 1464 self._file_breakpoints_cache[self._get_canonic_path(path)] = linenos 1465 1466 def _register_affected_frame(self, exception_obj, frame): 1467 # I used to store the frame ids in a new field inside exception object, 1468 # but Python 3.8 doesn't allow this (https://github.com/thonny/thonny/issues/1403) 1469 exc_id = id(exception_obj) 1470 if exc_id not in self._affected_frame_ids_per_exc_id: 1471 self._affected_frame_ids_per_exc_id[exc_id] = set() 1472 self._affected_frame_ids_per_exc_id[exc_id].add(id(frame)) 1473 1474 def _get_breakpoints_in_file(self, filename): 1475 result = self._file_breakpoints_cache.get(filename, None) 1476 1477 if result is not None: 1478 return result 1479 1480 canonic_path = self._get_canonic_path(filename) 1481 result = self._file_breakpoints_cache.get(canonic_path, set()) 1482 self._file_breakpoints_cache[filename] = result 1483 return result 1484 1485 def _get_current_exception(self): 1486 if self._fresh_exception is not None: 1487 return self._fresh_exception 1488 else: 1489 return sys.exc_info() 1490 1491 def _export_exception_info(self): 1492 exc = self._get_current_exception() 1493 1494 if exc[0] is None: 1495 return { 1496 "id": None, 1497 "msg": None, 1498 "type_name": None, 1499 "lines_with_frame_info": None, 1500 "affected_frame_ids": set(), 1501 "is_fresh": False, 1502 } 1503 else: 1504 return { 1505 "id": id(exc[1]), 1506 "msg": str(exc[1]), 1507 "type_name": exc[0].__name__, 1508 "lines_with_frame_info": format_exception_with_frame_info(*exc), 1509 "affected_frame_ids": self._affected_frame_ids_per_exc_id.get(id(exc[1]), set()), 1510 "is_fresh": exc == self._fresh_exception, 1511 } 1512 1513 def _breakpointhook(self, *args, **kw): 1514 pass 1515 1516 def _check_notify_return(self, frame_id): 1517 if frame_id in self._last_reported_frame_ids: 1518 # Need extra notification, because it may be long time until next interesting event 1519 self._backend.send_message(InlineResponse("debugger_return", frame_id=frame_id)) 1520 1521 def _check_store_main_frame_id(self, frame): 1522 # initial command doesn't have a frame id 1523 if self._current_command.frame_id is None and self._get_canonic_path( 1524 frame.f_code.co_filename 1525 ) == self._get_canonic_path(self._main_module_path): 1526 self._current_command.frame_id = id(frame) 1527 1528 1529class FastTracer(Tracer): 1530 def __init__(self, backend, original_cmd): 1531 super().__init__(backend, original_cmd) 1532 1533 self._command_frame_returned = False 1534 self._code_linenos_cache = {} 1535 self._code_breakpoints_cache = {} 1536 1537 def _initialize_new_command(self, current_frame): 1538 super()._initialize_new_command(current_frame) 1539 self._command_frame_returned = False 1540 if self._current_command.breakpoints != self._prev_breakpoints: 1541 self._code_breakpoints_cache = {} 1542 1543 # restore tracing for active frames which were skipped before 1544 # but have breakpoints now 1545 frame = current_frame 1546 while frame is not None: 1547 if ( 1548 frame.f_trace is None 1549 and frame.f_code is not None 1550 and self._get_breakpoints_in_code(frame.f_code) 1551 ): 1552 frame.f_trace = self._trace 1553 1554 frame = frame.f_back 1555 1556 def _breakpointhook(self, *args, **kw): 1557 frame = inspect.currentframe() 1558 while not self._is_interesting_frame(frame): 1559 frame = frame.f_back 1560 self._report_current_state(frame) 1561 self._fetch_next_debugger_command(frame) 1562 1563 def _should_skip_frame(self, frame, event): 1564 if event == "call": 1565 # new frames 1566 return ( 1567 ( 1568 self._current_command.name == "resume" 1569 and not self._get_breakpoints_in_code(frame.f_code) 1570 or self._current_command.name == "step_over" 1571 and not self._get_breakpoints_in_code(frame.f_code) 1572 and id(frame) not in self._last_reported_frame_ids 1573 or self._current_command.name == "step_out" 1574 and not self._get_breakpoints_in_code(frame.f_code) 1575 ) 1576 or not self._is_interesting_frame(frame) 1577 or self._backend.is_doing_io() 1578 ) 1579 1580 else: 1581 # once we have entered a frame, we need to reach the return event 1582 return False 1583 1584 def _trace(self, frame, event, arg): 1585 if self._should_skip_frame(frame, event): 1586 return None 1587 1588 # return None 1589 # return self._trace 1590 1591 frame_id = id(frame) 1592 1593 if event == "call": 1594 self._check_store_main_frame_id(frame) 1595 1596 self._fresh_exception = None 1597 # can we skip this frame? 1598 if self._current_command.name == "step_over" and not self._current_command.breakpoints: 1599 return None 1600 1601 elif event == "return": 1602 self._fresh_exception = None 1603 if frame_id == self._current_command["frame_id"]: 1604 self._command_frame_returned = True 1605 self._check_notify_return(frame_id) 1606 1607 elif event == "exception": 1608 if self._is_interesting_exception(frame, arg): 1609 self._fresh_exception = arg 1610 self._register_affected_frame(arg[1], frame) 1611 # UI doesn't know about separate exception events 1612 self._report_current_state(frame) 1613 self._fetch_next_debugger_command(frame) 1614 1615 elif event == "line": 1616 self._fresh_exception = None 1617 1618 if self._command_completion_handler(frame): 1619 self._report_current_state(frame) 1620 self._fetch_next_debugger_command(frame) 1621 1622 else: 1623 self._fresh_exception = None 1624 1625 return self._trace 1626 1627 def _report_current_state(self, frame): 1628 stack = self._backend._export_stack(frame, self._is_interesting_frame) 1629 msg = DebuggerResponse( 1630 stack=stack, 1631 in_present=True, 1632 io_symbol_count=None, 1633 exception_info=self._export_exception_info(), 1634 tracer_class="FastTracer", 1635 ) 1636 1637 self._last_reported_frame_ids = set(map(lambda f: f.id, stack)) 1638 1639 self._backend.send_message(msg) 1640 1641 def _cmd_step_into_completed(self, frame): 1642 return True 1643 1644 def _cmd_step_over_completed(self, frame): 1645 return ( 1646 id(frame) == self._current_command.frame_id 1647 or self._command_frame_returned 1648 or self._at_a_breakpoint(frame) 1649 ) 1650 1651 def _cmd_step_out_completed(self, frame): 1652 return self._command_frame_returned or self._at_a_breakpoint(frame) 1653 1654 def _cmd_resume_completed(self, frame): 1655 return self._at_a_breakpoint(frame) 1656 1657 def _get_breakpoints_in_code(self, f_code): 1658 1659 bps_in_file = self._get_breakpoints_in_file(f_code.co_filename) 1660 1661 code_id = id(f_code) 1662 result = self._code_breakpoints_cache.get(code_id, None) 1663 1664 if result is None: 1665 if not bps_in_file: 1666 result = set() 1667 else: 1668 co_linenos = self._code_linenos_cache.get(code_id, None) 1669 if co_linenos is None: 1670 co_linenos = {pair[1] for pair in dis.findlinestarts(f_code)} 1671 self._code_linenos_cache[code_id] = co_linenos 1672 1673 result = bps_in_file.intersection(co_linenos) 1674 1675 self._code_breakpoints_cache[code_id] = result 1676 1677 return result 1678 1679 def _at_a_breakpoint(self, frame): 1680 # TODO: try re-entering same line in loop 1681 return frame.f_lineno in self._get_breakpoints_in_code(frame.f_code) 1682 1683 def _is_interesting_exception(self, frame, arg): 1684 return super()._is_interesting_exception(frame, arg) and ( 1685 self._current_command.name in ["step_into", "step_over"] 1686 and ( 1687 # in command frame or its parent frames 1688 id(frame) == self._current_command["frame_id"] 1689 or self._command_frame_returned 1690 ) 1691 ) 1692 1693 1694class NiceTracer(Tracer): 1695 def __init__(self, backend, original_cmd): 1696 super().__init__(backend, original_cmd) 1697 self._instrumented_files = set() 1698 self._install_marker_functions() 1699 self._custom_stack = [] 1700 self._saved_states = [] 1701 self._current_state_index = 0 1702 1703 from collections import Counter 1704 1705 self._fulltags = Counter() 1706 self._nodes = {} 1707 1708 def _breakpointhook(self, *args, **kw): 1709 self._report_state(len(self._saved_states) - 1) 1710 self._fetch_next_debugger_command(None) 1711 1712 def _install_marker_functions(self): 1713 # Make dummy marker functions universally available by putting them 1714 # into builtin scope 1715 self.marker_function_names = { 1716 BEFORE_STATEMENT_MARKER, 1717 AFTER_STATEMENT_MARKER, 1718 BEFORE_EXPRESSION_MARKER, 1719 AFTER_EXPRESSION_MARKER, 1720 } 1721 1722 for name in self.marker_function_names: 1723 if not hasattr(builtins, name): 1724 setattr(builtins, name, getattr(self, name)) 1725 1726 def _prepare_ast(self, source, filename, mode): 1727 # ast_utils need to be imported after asttokens 1728 # is (custom-)imported 1729 try_load_modules_with_frontend_sys_path(["asttokens", "six", "astroid"]) 1730 from thonny import ast_utils 1731 1732 root = ast.parse(source, filename, mode) 1733 1734 ast_utils.mark_text_ranges(root, source) 1735 self._tag_nodes(root) 1736 self._insert_expression_markers(root) 1737 self._insert_statement_markers(root) 1738 self._insert_for_target_markers(root) 1739 self._instrumented_files.add(filename) 1740 1741 return root 1742 1743 def _should_skip_frame(self, frame, event): 1744 # nice tracer can't skip any of the frames which need to be 1745 # shown in the stacktrace 1746 code = frame.f_code 1747 if code is None: 1748 return True 1749 1750 if event == "call": 1751 # new frames 1752 if code.co_name in self.marker_function_names: 1753 return False 1754 1755 else: 1756 return not self._is_interesting_frame(frame) or self._backend.is_doing_io() 1757 1758 else: 1759 # once we have entered a frame, we need to reach the return event 1760 return False 1761 1762 def _is_interesting_frame(self, frame): 1763 return ( 1764 frame.f_code.co_filename in self._instrumented_files 1765 and super()._is_interesting_frame(frame) 1766 ) 1767 1768 def find_spec(self, fullname, path=None, target=None): 1769 spec = PathFinder.find_spec(fullname, path, target) 1770 1771 if ( 1772 spec is not None 1773 and isinstance(spec.loader, SourceFileLoader) 1774 and getattr(spec, "origin", None) 1775 and self._is_interesting_module_file(spec.origin) 1776 ): 1777 spec.loader = FancySourceFileLoader(fullname, spec.origin, self) 1778 return spec 1779 else: 1780 return super().find_spec(fullname, path, target) 1781 1782 def is_in_past(self): 1783 return self._current_state_index < len(self._saved_states) - 1 1784 1785 def _trace(self, frame, event, arg): 1786 try: 1787 return self._trace_and_catch(frame, event, arg) 1788 except BaseException as e: 1789 logger.exception("Exception in _trace", exc_info=e) 1790 sys.settrace(None) 1791 return None 1792 1793 def _trace_and_catch(self, frame, event, arg): 1794 """ 1795 1) Detects marker calls and responds to client queries in these spots 1796 2) Maintains a customized view of stack 1797 """ 1798 # frame skipping test should be done both in new frames and old ones (because of Resume) 1799 # Note that intermediate frames can't be skipped when jumping to a breakpoint 1800 # because of the need to maintain custom stack 1801 if self._should_skip_frame(frame, event): 1802 return None 1803 1804 code_name = frame.f_code.co_name 1805 1806 if event == "call": 1807 self._fresh_exception = ( 1808 None # some code is running, therefore exception is not fresh anymore 1809 ) 1810 1811 if code_name in self.marker_function_names: 1812 self._check_store_main_frame_id(frame.f_back) 1813 1814 # the main thing 1815 if code_name == BEFORE_STATEMENT_MARKER: 1816 event = "before_statement" 1817 elif code_name == AFTER_STATEMENT_MARKER: 1818 event = "after_statement" 1819 elif code_name == BEFORE_EXPRESSION_MARKER: 1820 event = "before_expression" 1821 elif code_name == AFTER_EXPRESSION_MARKER: 1822 event = "after_expression" 1823 else: 1824 raise AssertionError("Unknown marker function") 1825 1826 marker_function_args = frame.f_locals.copy() 1827 node = self._nodes[marker_function_args["node_id"]] 1828 1829 del marker_function_args["self"] 1830 1831 if "call_function" not in node.tags: 1832 self._handle_progress_event(frame.f_back, event, marker_function_args, node) 1833 self._try_interpret_as_again_event(frame.f_back, event, marker_function_args, node) 1834 1835 # Don't need any more events from these frames 1836 return None 1837 1838 else: 1839 # Calls to proper functions. 1840 # Client doesn't care about these events, 1841 # it cares about "before_statement" events in the first statement of the body 1842 self._custom_stack.append(CustomStackFrame(frame, "call")) 1843 1844 elif event == "exception": 1845 # Note that Nicer can't filter out exception based on current command 1846 # because it must be possible to go back and replay with different command 1847 if self._is_interesting_exception(frame, arg): 1848 self._fresh_exception = arg 1849 self._register_affected_frame(arg[1], frame) 1850 1851 # Last command (step_into or step_over) produced this exception 1852 # Show red after-state for this focus 1853 # use the state prepared by previous event 1854 last_custom_frame = self._custom_stack[-1] 1855 assert last_custom_frame.system_frame == frame 1856 1857 # TODO: instead of producing an event here, next before_-event 1858 # should create matching after event for each before event 1859 # which would remain unclosed because of this exception. 1860 # Existence of these after events would simplify step_over management 1861 1862 assert last_custom_frame.event.startswith("before_") 1863 pseudo_event = last_custom_frame.event.replace("before_", "after_").replace( 1864 "_again", "" 1865 ) 1866 # print("handle", pseudo_event, {}, last_custom_frame.node) 1867 self._handle_progress_event(frame, pseudo_event, {}, last_custom_frame.node) 1868 1869 elif event == "return": 1870 self._fresh_exception = None 1871 1872 if code_name not in self.marker_function_names: 1873 frame_id = id(self._custom_stack[-1].system_frame) 1874 self._check_notify_return(frame_id) 1875 self._custom_stack.pop() 1876 if len(self._custom_stack) == 0: 1877 # We popped last frame, this means our program has ended. 1878 # There may be more events coming from upper (system) frames 1879 # but we're not interested in those 1880 sys.settrace(None) 1881 else: 1882 pass 1883 1884 else: 1885 self._fresh_exception = None 1886 1887 return self._trace 1888 1889 def _handle_progress_event(self, frame, event, args, node): 1890 self._save_current_state(frame, event, args, node) 1891 self._respond_to_commands() 1892 1893 def _save_current_state(self, frame, event, args, node): 1894 """ 1895 Updates custom stack and stores the state 1896 1897 self._custom_stack always keeps last info, 1898 which gets exported as FrameInfos to _saved_states["stack"] 1899 """ 1900 focus = TextRange(node.lineno, node.col_offset, node.end_lineno, node.end_col_offset) 1901 1902 custom_frame = self._custom_stack[-1] 1903 custom_frame.event = event 1904 custom_frame.focus = focus 1905 custom_frame.node = node 1906 custom_frame.node_tags = node.tags 1907 1908 if self._saved_states: 1909 prev_state = self._saved_states[-1] 1910 prev_state_frame = self._create_actual_active_frame(prev_state) 1911 else: 1912 prev_state = None 1913 prev_state_frame = None 1914 1915 # store information about current statement / expression 1916 if "statement" in event: 1917 custom_frame.current_statement = focus 1918 1919 if event == "before_statement_again": 1920 # keep the expression information from last event 1921 pass 1922 else: 1923 custom_frame.current_root_expression = None 1924 custom_frame.current_evaluations = [] 1925 else: 1926 assert "expression" in event 1927 assert prev_state_frame is not None 1928 1929 # may need to update current_statement, because the parent statement was 1930 # not the last one visited (eg. with test expression of a loop, 1931 # starting from 2nd iteration) 1932 if hasattr(node, "parent_statement_focus"): 1933 custom_frame.current_statement = node.parent_statement_focus 1934 1935 # see whether current_root_expression needs to be updated 1936 prev_root_expression = prev_state_frame.current_root_expression 1937 if event == "before_expression" and ( 1938 id(frame) != id(prev_state_frame.system_frame) 1939 or "statement" in prev_state_frame.event 1940 or prev_root_expression 1941 and not range_contains_smaller_or_equal(prev_root_expression, focus) 1942 ): 1943 custom_frame.current_root_expression = focus 1944 custom_frame.current_evaluations = [] 1945 1946 if event == "after_expression" and "value" in args: 1947 # value is missing in case of exception 1948 custom_frame.current_evaluations.append( 1949 (focus, self._backend.export_value(args["value"])) 1950 ) 1951 1952 # Save the snapshot. 1953 # Check if we can share something with previous state 1954 if ( 1955 prev_state is not None 1956 and id(prev_state_frame.system_frame) == id(frame) 1957 and prev_state["exception_value"] is self._get_current_exception()[1] 1958 and prev_state["fresh_exception_id"] == id(self._fresh_exception) 1959 and ("before" in event or "skipexport" in node.tags) 1960 ): 1961 1962 exception_info = prev_state["exception_info"] 1963 # share the stack ... 1964 stack = prev_state["stack"] 1965 # ... but override certain things 1966 active_frame_overrides = { 1967 "event": custom_frame.event, 1968 "focus": custom_frame.focus, 1969 "node_tags": custom_frame.node_tags, 1970 "current_root_expression": custom_frame.current_root_expression, 1971 "current_evaluations": custom_frame.current_evaluations.copy(), 1972 "current_statement": custom_frame.current_statement, 1973 } 1974 else: 1975 # make full export 1976 stack = self._export_stack() 1977 exception_info = self._export_exception_info() 1978 active_frame_overrides = {} 1979 1980 msg = { 1981 "stack": stack, 1982 "active_frame_overrides": active_frame_overrides, 1983 "in_client_log": False, 1984 "io_symbol_count": ( 1985 sys.stdin._processed_symbol_count 1986 + sys.stdout._processed_symbol_count 1987 + sys.stderr._processed_symbol_count 1988 ), 1989 "exception_value": self._get_current_exception()[1], 1990 "fresh_exception_id": id(self._fresh_exception), 1991 "exception_info": exception_info, 1992 } 1993 1994 self._saved_states.append(msg) 1995 1996 def _respond_to_commands(self): 1997 """Tries to respond to client commands with states collected so far. 1998 Returns if these states don't suffice anymore and Python needs 1999 to advance the program""" 2000 2001 # while the state for current index is already saved: 2002 while self._current_state_index < len(self._saved_states): 2003 state = self._saved_states[self._current_state_index] 2004 2005 # Get current state's most recent frame (together with overrides 2006 frame = self._create_actual_active_frame(state) 2007 2008 # Is this state meant to be seen? 2009 if "skip_" + frame.event not in frame.node_tags: 2010 # if True: 2011 # Has the command completed? 2012 tester = getattr(self, "_cmd_" + self._current_command.name + "_completed") 2013 cmd_complete = tester(frame, self._current_command) 2014 2015 if cmd_complete: 2016 state["in_client_log"] = True 2017 self._report_state(self._current_state_index) 2018 self._fetch_next_debugger_command(frame) 2019 2020 if self._current_command.name == "step_back": 2021 if self._current_state_index == 0: 2022 # Already in first state. Remain in this loop 2023 pass 2024 else: 2025 assert self._current_state_index > 0 2026 # Current event is no longer present in GUI "undo log" 2027 self._saved_states[self._current_state_index]["in_client_log"] = False 2028 self._current_state_index -= 1 2029 else: 2030 # Other commands move the pointer forward 2031 self._current_state_index += 1 2032 2033 def _create_actual_active_frame(self, state): 2034 return state["stack"][-1]._replace(**state["active_frame_overrides"]) 2035 2036 def _report_state(self, state_index): 2037 in_present = state_index == len(self._saved_states) - 1 2038 if in_present: 2039 # For reported new events re-export stack to make sure it is not shared. 2040 # (There is tiny chance that sharing previous state 2041 # after executing BinOp, Attribute, Compare or Subscript 2042 # was not the right choice. See tag_nodes for more.) 2043 # Re-exporting reduces the harm by showing correct data at least 2044 # for present states. 2045 self._saved_states[state_index]["stack"] = self._export_stack() 2046 2047 # need to make a copy for applying overrides 2048 # and removing helper fields without modifying original 2049 state = self._saved_states[state_index].copy() 2050 state["stack"] = state["stack"].copy() 2051 2052 state["in_present"] = in_present 2053 if not in_present: 2054 # for past states fix the newest frame 2055 state["stack"][-1] = self._create_actual_active_frame(state) 2056 2057 del state["exception_value"] 2058 del state["active_frame_overrides"] 2059 2060 # Convert stack of TempFrameInfos to stack of FrameInfos 2061 new_stack = [] 2062 self._last_reported_frame_ids = set() 2063 for tframe in state["stack"]: 2064 system_frame = tframe.system_frame 2065 module_name = system_frame.f_globals["__name__"] 2066 code_name = system_frame.f_code.co_name 2067 2068 source, firstlineno, in_library = self._backend._get_frame_source_info(system_frame) 2069 2070 assert firstlineno is not None, "nofir " + str(system_frame) 2071 frame_id = id(system_frame) 2072 new_stack.append( 2073 FrameInfo( 2074 id=frame_id, 2075 filename=system_frame.f_code.co_filename, 2076 module_name=module_name, 2077 code_name=code_name, 2078 locals=tframe.locals, 2079 globals=tframe.globals, 2080 freevars=system_frame.f_code.co_freevars, 2081 source=source, 2082 lineno=system_frame.f_lineno, 2083 firstlineno=firstlineno, 2084 in_library=in_library, 2085 event=tframe.event, 2086 focus=tframe.focus, 2087 node_tags=tframe.node_tags, 2088 current_statement=tframe.current_statement, 2089 current_evaluations=tframe.current_evaluations, 2090 current_root_expression=tframe.current_root_expression, 2091 ) 2092 ) 2093 2094 self._last_reported_frame_ids.add(frame_id) 2095 2096 state["stack"] = new_stack 2097 state["tracer_class"] = "NiceTracer" 2098 2099 self._backend.send_message(DebuggerResponse(**state)) 2100 2101 def _try_interpret_as_again_event(self, frame, original_event, original_args, original_node): 2102 """ 2103 Some after_* events can be interpreted also as 2104 "before_*_again" events (eg. when last argument of a call was 2105 evaluated, then we are just before executing the final stage of the call) 2106 """ 2107 2108 if original_event == "after_expression": 2109 value = original_args.get("value") 2110 2111 if ( 2112 "last_child" in original_node.tags 2113 or "or_arg" in original_node.tags 2114 and value 2115 or "and_arg" in original_node.tags 2116 and not value 2117 ): 2118 2119 # there may be explicit exceptions 2120 if ( 2121 "skip_after_statement_again" in original_node.parent_node.tags 2122 or "skip_after_expression_again" in original_node.parent_node.tags 2123 ): 2124 return 2125 2126 # next step will be finalizing evaluation of parent of current expr 2127 # so let's say we're before that parent expression 2128 again_args = {"node_id": id(original_node.parent_node)} 2129 again_event = ( 2130 "before_expression_again" 2131 if "child_of_expression" in original_node.tags 2132 else "before_statement_again" 2133 ) 2134 2135 self._handle_progress_event( 2136 frame, again_event, again_args, original_node.parent_node 2137 ) 2138 2139 def _cmd_step_over_completed(self, frame, cmd): 2140 """ 2141 Identifies the moment when piece of code indicated by cmd.frame_id and cmd.focus 2142 has completed execution (either successfully or not). 2143 """ 2144 2145 if self._at_a_breakpoint(frame, cmd): 2146 return True 2147 2148 # Make sure the correct frame_id is selected 2149 if id(frame.system_frame) == cmd.frame_id: 2150 # We're in the same frame 2151 if "before_" in cmd.state: 2152 if not range_contains_smaller_or_equal(cmd.focus, frame.focus): 2153 # Focus has changed, command has completed 2154 return True 2155 else: 2156 # Keep running 2157 return False 2158 elif "after_" in cmd.state: 2159 if ( 2160 frame.focus != cmd.focus 2161 or "before_" in frame.event 2162 or "_expression" in cmd.state 2163 and "_statement" in frame.event 2164 or "_statement" in cmd.state 2165 and "_expression" in frame.event 2166 ): 2167 # The state has changed, command has completed 2168 return True 2169 else: 2170 # Keep running 2171 return False 2172 else: 2173 # We're in another frame 2174 if self._frame_is_alive(cmd.frame_id): 2175 # We're in a successor frame, keep running 2176 return False 2177 else: 2178 # Original frame has completed, assumedly because of an exception 2179 # We're done 2180 return True 2181 2182 return True # not actually required, just to make Pylint happy 2183 2184 def _cmd_step_into_completed(self, frame, cmd): 2185 return frame.event != "after_statement" 2186 2187 def _cmd_step_back_completed(self, frame, cmd): 2188 # Check if the selected message has been previously sent to front-end 2189 return ( 2190 self._saved_states[self._current_state_index]["in_client_log"] 2191 or self._current_state_index == 0 2192 ) 2193 2194 def _cmd_step_out_completed(self, frame, cmd): 2195 if self._current_state_index == 0: 2196 return False 2197 2198 if frame.event == "after_statement": 2199 return False 2200 2201 if self._at_a_breakpoint(frame, cmd): 2202 return True 2203 2204 prev_state_frame = self._saved_states[self._current_state_index - 1]["stack"][-1] 2205 2206 return ( 2207 # the frame has completed 2208 not self._frame_is_alive(cmd.frame_id) 2209 # we're in the same frame but on higher level 2210 # TODO: expression inside statement expression has same range as its parent 2211 or id(frame.system_frame) == cmd.frame_id 2212 and range_contains_smaller(frame.focus, cmd.focus) 2213 # or we were there in prev state 2214 or id(prev_state_frame.system_frame) == cmd.frame_id 2215 and range_contains_smaller(prev_state_frame.focus, cmd.focus) 2216 ) 2217 2218 def _cmd_resume_completed(self, frame, cmd): 2219 return self._at_a_breakpoint(frame, cmd) 2220 2221 def _at_a_breakpoint(self, frame, cmd, breakpoints=None): 2222 if breakpoints is None: 2223 breakpoints = cmd["breakpoints"] 2224 2225 return ( 2226 frame.event in ["before_statement", "before_expression"] 2227 and frame.system_frame.f_code.co_filename in breakpoints 2228 and frame.focus.lineno in breakpoints[frame.system_frame.f_code.co_filename] 2229 # consider only first event on a line 2230 # (but take into account that same line may be reentered) 2231 and ( 2232 cmd.focus is None 2233 or (cmd.focus.lineno != frame.focus.lineno) 2234 or (cmd.focus == frame.focus and cmd.state == frame.event) 2235 or id(frame.system_frame) != cmd.frame_id 2236 ) 2237 ) 2238 2239 def _frame_is_alive(self, frame_id): 2240 for frame in self._custom_stack: 2241 if id(frame.system_frame) == frame_id: 2242 return True 2243 2244 return False 2245 2246 def _export_stack(self): 2247 result = [] 2248 2249 exported_globals_per_module = {} 2250 2251 def export_globals(module_name, frame): 2252 if module_name not in exported_globals_per_module: 2253 exported_globals_per_module[module_name] = self._backend.export_variables( 2254 frame.f_globals 2255 ) 2256 return exported_globals_per_module[module_name] 2257 2258 for custom_frame in self._custom_stack: 2259 system_frame = custom_frame.system_frame 2260 module_name = system_frame.f_globals["__name__"] 2261 2262 result.append( 2263 TempFrameInfo( 2264 # need to store the reference to the frame to avoid it being GC-d 2265 # otherwise frame id-s would be reused and this would 2266 # mess up communication with the frontend. 2267 system_frame=system_frame, 2268 locals=None 2269 if system_frame.f_locals is system_frame.f_globals 2270 else self._backend.export_variables(system_frame.f_locals), 2271 globals=export_globals(module_name, system_frame), 2272 event=custom_frame.event, 2273 focus=custom_frame.focus, 2274 node_tags=custom_frame.node_tags, 2275 current_evaluations=custom_frame.current_evaluations.copy(), 2276 current_statement=custom_frame.current_statement, 2277 current_root_expression=custom_frame.current_root_expression, 2278 ) 2279 ) 2280 2281 assert result # not empty 2282 return result 2283 2284 def _thonny_hidden_before_stmt(self, node_id): 2285 # The code to be debugged will be instrumented with this function 2286 # inserted before each statement. 2287 # Entry into this function indicates that statement as given 2288 # by the code range is about to be evaluated next. 2289 return None 2290 2291 def _thonny_hidden_after_stmt(self, node_id): 2292 # The code to be debugged will be instrumented with this function 2293 # inserted after each statement. 2294 # Entry into this function indicates that statement as given 2295 # by the code range was just executed successfully. 2296 return None 2297 2298 def _thonny_hidden_before_expr(self, node_id): 2299 # Entry into this function indicates that expression as given 2300 # by the code range is about to be evaluated next 2301 return node_id 2302 2303 def _thonny_hidden_after_expr(self, node_id, value): 2304 # The code to be debugged will be instrumented with this function 2305 # wrapped around each expression (given as 2nd argument). 2306 # Entry into this function indicates that expression as given 2307 # by the code range was just evaluated to given value 2308 return value 2309 2310 def _tag_nodes(self, root): 2311 """Marks interesting properties of AST nodes""" 2312 # ast_utils need to be imported after asttokens 2313 # is (custom-)imported 2314 try_load_modules_with_frontend_sys_path(["asttokens", "six", "astroid"]) 2315 from thonny import ast_utils 2316 2317 def add_tag(node, tag): 2318 if not hasattr(node, "tags"): 2319 node.tags = set() 2320 node.tags.add("class=" + node.__class__.__name__) 2321 node.tags.add(tag) 2322 2323 # ignore module docstring if it is before from __future__ import 2324 if ( 2325 isinstance(root.body[0], ast.Expr) 2326 and isinstance(root.body[0].value, ast.Str) 2327 and len(root.body) > 1 2328 and isinstance(root.body[1], ast.ImportFrom) 2329 and root.body[1].module == "__future__" 2330 ): 2331 add_tag(root.body[0], "ignore") 2332 add_tag(root.body[0].value, "ignore") 2333 add_tag(root.body[1], "ignore") 2334 2335 for node in ast.walk(root): 2336 if not isinstance(node, (ast.expr, ast.stmt)): 2337 if isinstance(node, ast.comprehension): 2338 for expr in node.ifs: 2339 add_tag(expr, "comprehension.if") 2340 2341 continue 2342 2343 # tag last children 2344 last_child = ast_utils.get_last_child(node) 2345 assert last_child in [True, False, None] or isinstance( 2346 last_child, (ast.expr, ast.stmt, type(None)) 2347 ), ("Bad last child " + str(last_child) + " of " + str(node)) 2348 if last_child is not None: 2349 add_tag(node, "has_children") 2350 2351 if isinstance(last_child, ast.AST): 2352 last_child.parent_node = node 2353 add_tag(last_child, "last_child") 2354 if isinstance(node, _ast.expr): 2355 add_tag(last_child, "child_of_expression") 2356 else: 2357 add_tag(last_child, "child_of_statement") 2358 2359 if isinstance(node, ast.Call): 2360 add_tag(last_child, "last_call_arg") 2361 2362 # other cases 2363 if isinstance(node, ast.Call): 2364 add_tag(node.func, "call_function") 2365 node.func.parent_node = node 2366 2367 if isinstance(node, ast.BoolOp) and node.op == ast.Or(): 2368 for child in node.values: 2369 add_tag(child, "or_arg") 2370 child.parent_node = node 2371 2372 if isinstance(node, ast.BoolOp) and node.op == ast.And(): 2373 for child in node.values: 2374 add_tag(child, "and_arg") 2375 child.parent_node = node 2376 2377 # TODO: assert (it doesn't evaluate msg when test == True) 2378 2379 if isinstance(node, ast.stmt): 2380 for child in ast.iter_child_nodes(node): 2381 child.parent_node = node 2382 child.parent_statement_focus = TextRange( 2383 node.lineno, node.col_offset, node.end_lineno, node.end_col_offset 2384 ) 2385 2386 if isinstance(node, ast.Str): 2387 add_tag(node, "skipexport") 2388 2389 if hasattr(ast, "JoinedStr") and isinstance(node, ast.JoinedStr): 2390 # can't present children normally without 2391 # ast giving correct locations for them 2392 # https://bugs.python.org/issue29051 2393 add_tag(node, "ignore_children") 2394 2395 elif isinstance(node, ast.Num): 2396 add_tag(node, "skipexport") 2397 2398 elif isinstance(node, ast.List): 2399 add_tag(node, "skipexport") 2400 2401 elif isinstance(node, ast.Tuple): 2402 add_tag(node, "skipexport") 2403 2404 elif isinstance(node, ast.Set): 2405 add_tag(node, "skipexport") 2406 2407 elif isinstance(node, ast.Dict): 2408 add_tag(node, "skipexport") 2409 2410 elif isinstance(node, ast.Name): 2411 add_tag(node, "skipexport") 2412 2413 elif isinstance(node, ast.NameConstant): 2414 add_tag(node, "skipexport") 2415 2416 elif hasattr(ast, "Constant") and isinstance(node, ast.Constant): 2417 add_tag(node, "skipexport") 2418 2419 elif isinstance(node, ast.Expr): 2420 if not isinstance(node.value, (ast.Yield, ast.YieldFrom)): 2421 add_tag(node, "skipexport") 2422 2423 elif isinstance(node, ast.If): 2424 add_tag(node, "skipexport") 2425 2426 elif isinstance(node, ast.Return): 2427 add_tag(node, "skipexport") 2428 2429 elif isinstance(node, ast.While): 2430 add_tag(node, "skipexport") 2431 2432 elif isinstance(node, ast.Continue): 2433 add_tag(node, "skipexport") 2434 2435 elif isinstance(node, ast.Break): 2436 add_tag(node, "skipexport") 2437 2438 elif isinstance(node, ast.Pass): 2439 add_tag(node, "skipexport") 2440 2441 elif isinstance(node, ast.For): 2442 add_tag(node, "skipexport") 2443 2444 elif isinstance(node, ast.Try): 2445 add_tag(node, "skipexport") 2446 2447 elif isinstance(node, ast.ListComp): 2448 add_tag(node.elt, "ListComp.elt") 2449 if len(node.generators) > 1: 2450 add_tag(node, "ignore_children") 2451 2452 elif isinstance(node, ast.SetComp): 2453 add_tag(node.elt, "SetComp.elt") 2454 if len(node.generators) > 1: 2455 add_tag(node, "ignore_children") 2456 2457 elif isinstance(node, ast.DictComp): 2458 add_tag(node.key, "DictComp.key") 2459 add_tag(node.value, "DictComp.value") 2460 if len(node.generators) > 1: 2461 add_tag(node, "ignore_children") 2462 2463 elif isinstance(node, ast.BinOp): 2464 # TODO: use static analysis to detect type of left child 2465 add_tag(node, "skipexport") 2466 2467 elif isinstance(node, ast.Attribute): 2468 # TODO: use static analysis to detect type of left child 2469 add_tag(node, "skipexport") 2470 2471 elif isinstance(node, ast.Subscript): 2472 # TODO: use static analysis to detect type of left child 2473 add_tag(node, "skipexport") 2474 2475 elif isinstance(node, ast.Compare): 2476 # TODO: use static analysis to detect type of left child 2477 add_tag(node, "skipexport") 2478 2479 if isinstance(node, (ast.Assign)): 2480 # value will be presented in assignment's before_statement_again 2481 add_tag(node.value, "skip_after_expression") 2482 2483 if isinstance(node, (ast.Expr, ast.While, ast.For, ast.If, ast.Try, ast.With)): 2484 add_tag(node, "skip_after_statement_again") 2485 2486 # make sure every node has this field 2487 if not hasattr(node, "tags"): 2488 node.tags = set() 2489 2490 def _should_instrument_as_expression(self, node): 2491 return ( 2492 isinstance(node, _ast.expr) 2493 and hasattr(node, "end_lineno") 2494 and hasattr(node, "end_col_offset") 2495 and not getattr(node, "incorrect_range", False) 2496 and "ignore" not in node.tags 2497 and (not hasattr(node, "ctx") or isinstance(node.ctx, ast.Load)) 2498 # TODO: repeatedly evaluated subexpressions of comprehensions 2499 # can be supported (but it requires some redesign both in backend and GUI) 2500 and "ListComp.elt" not in node.tags 2501 and "SetComp.elt" not in node.tags 2502 and "DictComp.key" not in node.tags 2503 and "DictComp.value" not in node.tags 2504 and "comprehension.if" not in node.tags 2505 ) 2506 2507 def _should_instrument_as_statement(self, node): 2508 return ( 2509 isinstance(node, _ast.stmt) 2510 and not getattr(node, "incorrect_range", False) 2511 and "ignore" not in node.tags 2512 # Shouldn't insert anything before from __future__ import 2513 # as this is not a normal statement 2514 # https://bitbucket.org/plas/thonny/issues/183/thonny-throws-false-positive-syntaxerror 2515 and (not isinstance(node, ast.ImportFrom) or node.module != "__future__") 2516 ) 2517 2518 def _insert_statement_markers(self, root): 2519 # find lists of statements and insert before/after markers for each statement 2520 for name, value in ast.iter_fields(root): 2521 if isinstance(root, ast.Try) and name == "handlers": 2522 # contains statements but is not statement itself 2523 for handler in value: 2524 self._insert_statement_markers(handler) 2525 elif isinstance(value, ast.AST): 2526 self._insert_statement_markers(value) 2527 elif isinstance(value, list): 2528 if len(value) > 0: 2529 new_list = [] 2530 for node in value: 2531 if self._should_instrument_as_statement(node): 2532 # self._debug("EBFOMA", node) 2533 # add before marker 2534 new_list.append( 2535 self._create_statement_marker(node, BEFORE_STATEMENT_MARKER) 2536 ) 2537 2538 # original statement 2539 if self._should_instrument_as_statement(node): 2540 self._insert_statement_markers(node) 2541 new_list.append(node) 2542 2543 if ( 2544 self._should_instrument_as_statement(node) 2545 and "skipexport" not in node.tags 2546 ): 2547 # add after marker 2548 new_list.append( 2549 self._create_statement_marker(node, AFTER_STATEMENT_MARKER) 2550 ) 2551 setattr(root, name, new_list) 2552 2553 def _create_statement_marker(self, node, function_name): 2554 call = self._create_simple_marker_call(node, function_name) 2555 stmt = ast.Expr(value=call) 2556 ast.copy_location(stmt, node) 2557 ast.fix_missing_locations(stmt) 2558 return stmt 2559 2560 def _insert_for_target_markers(self, root): 2561 """inserts markers which notify assignment to for-loop variables""" 2562 for node in ast.walk(root): 2563 if isinstance(node, ast.For): 2564 old_target = node.target 2565 # print(vars(old_target)) 2566 temp_name = "__for_loop_var" 2567 node.target = ast.Name(temp_name, ast.Store()) 2568 2569 name_load = ast.Name(temp_name, ast.Load()) 2570 # value will be visible in parent's before_statement_again event 2571 name_load.tags = {"skip_before_expression", "skip_after_expression", "last_child"} 2572 name_load.lineno, name_load.col_offset = (node.iter.lineno, node.iter.col_offset) 2573 name_load.end_lineno, name_load.end_col_offset = ( 2574 node.iter.end_lineno, 2575 node.iter.end_col_offset, 2576 ) 2577 2578 before_name_load = self._create_simple_marker_call( 2579 name_load, BEFORE_EXPRESSION_MARKER 2580 ) 2581 after_name_load = ast.Call( 2582 func=ast.Name(id=AFTER_EXPRESSION_MARKER, ctx=ast.Load()), 2583 args=[before_name_load, name_load], 2584 keywords=[], 2585 ) 2586 2587 ass = ast.Assign([old_target], after_name_load) 2588 ass.lineno, ass.col_offset = old_target.lineno, old_target.col_offset 2589 ass.end_lineno, ass.end_col_offset = ( 2590 node.iter.end_lineno, 2591 node.iter.end_col_offset, 2592 ) 2593 ass.tags = {"skip_before_statement"} # before_statement_again will be shown 2594 2595 name_load.parent_node = ass 2596 2597 ass_before = self._create_statement_marker(ass, BEFORE_STATEMENT_MARKER) 2598 node.body.insert(0, ass_before) 2599 node.body.insert(1, ass) 2600 node.body.insert(2, self._create_statement_marker(ass, AFTER_STATEMENT_MARKER)) 2601 2602 ast.fix_missing_locations(node) 2603 2604 def _insert_expression_markers(self, node): 2605 """ 2606 TODO: this docstring is outdated 2607 each expression e gets wrapped like this: 2608 _after(_before(_loc, _node_is_zoomable), e, _node_role, _parent_range) 2609 where 2610 _after is function that gives the resulting value 2611 _before is function that signals the beginning of evaluation of e 2612 _loc gives the code range of e 2613 _node_is_zoomable indicates whether this node has subexpressions 2614 _node_role is either 'last_call_arg', 'last_op_arg', 'first_or_arg', 2615 'first_and_arg', 'function' or None 2616 """ 2617 tracer = self 2618 2619 class ExpressionVisitor(ast.NodeTransformer): 2620 def generic_visit(self, node): 2621 if isinstance(node, _ast.expr): 2622 if isinstance(node, ast.Starred): 2623 # keep this node as is, but instrument its children 2624 return ast.NodeTransformer.generic_visit(self, node) 2625 elif tracer._should_instrument_as_expression(node): 2626 # before marker 2627 before_marker = tracer._create_simple_marker_call( 2628 node, BEFORE_EXPRESSION_MARKER 2629 ) 2630 ast.copy_location(before_marker, node) 2631 2632 if "ignore_children" in node.tags: 2633 transformed_node = node 2634 else: 2635 transformed_node = ast.NodeTransformer.generic_visit(self, node) 2636 2637 # after marker 2638 after_marker = ast.Call( 2639 func=ast.Name(id=AFTER_EXPRESSION_MARKER, ctx=ast.Load()), 2640 args=[before_marker, transformed_node], 2641 keywords=[], 2642 ) 2643 ast.copy_location(after_marker, node) 2644 ast.fix_missing_locations(after_marker) 2645 # further transformations may query original node location from after marker 2646 if hasattr(node, "end_lineno"): 2647 after_marker.end_lineno = node.end_lineno 2648 after_marker.end_col_offset = node.end_col_offset 2649 2650 return after_marker 2651 else: 2652 # This expression (and its children) should be ignored 2653 return node 2654 else: 2655 # Descend into statements 2656 return ast.NodeTransformer.generic_visit(self, node) 2657 2658 return ExpressionVisitor().visit(node) 2659 2660 def _create_simple_marker_call(self, node, fun_name, extra_args=[]): 2661 args = [self._export_node(node)] + extra_args 2662 2663 return ast.Call(func=ast.Name(id=fun_name, ctx=ast.Load()), args=args, keywords=[]) 2664 2665 def _export_node(self, node): 2666 assert isinstance(node, (ast.expr, ast.stmt)) 2667 node_id = id(node) 2668 self._nodes[node_id] = node 2669 return ast.Num(node_id) 2670 2671 def _debug(self, *args): 2672 logger.debug("TRACER: " + str(args)) 2673 2674 def _execute_prepared_user_code(self, statements, global_vars): 2675 try: 2676 return Tracer._execute_prepared_user_code(self, statements, global_vars) 2677 finally: 2678 """ 2679 from thonny.misc_utils import _win_get_used_memory 2680 print("Memory:", _win_get_used_memory() / 1024 / 1024) 2681 print("States:", len(self._saved_states)) 2682 print(self._fulltags.most_common()) 2683 """ 2684 2685 2686class CustomStackFrame: 2687 def __init__(self, frame, event, focus=None): 2688 self.system_frame = frame 2689 self.event = event 2690 self.focus = focus 2691 self.current_evaluations = [] 2692 self.current_statement = None 2693 self.current_root_expression = None 2694 self.node_tags = set() 2695 2696 2697class FancySourceFileLoader(SourceFileLoader): 2698 """Used for loading and instrumenting user modules during fancy tracing""" 2699 2700 def __init__(self, fullname, path, tracer): 2701 super().__init__(fullname, path) 2702 self._tracer = tracer 2703 2704 def source_to_code(self, data, path, *, _optimize=-1): 2705 old_tracer = sys.gettrace() 2706 sys.settrace(None) 2707 try: 2708 root = self._tracer._prepare_ast(data, path, "exec") 2709 return super().source_to_code(root, path) 2710 finally: 2711 sys.settrace(old_tracer) 2712 2713 2714def _get_frame_prefix(frame): 2715 return str(id(frame)) + " " + ">" * len(inspect.getouterframes(frame, 0)) + " " 2716 2717 2718def _fetch_frame_source_info(frame): 2719 if frame.f_code.co_filename is None or not os.path.exists(frame.f_code.co_filename): 2720 return None, None, True 2721 2722 is_libra = _is_library_file(frame.f_code.co_filename) 2723 2724 if frame.f_code.co_name == "<lambda>": 2725 source = inspect.getsource(frame.f_code) 2726 return source, frame.f_code.co_firstlineno, is_libra 2727 elif frame.f_code.co_name == "<module>": 2728 # inspect.getsource and getsourcelines don't help here 2729 with tokenize.open(frame.f_code.co_filename) as fp: 2730 return fp.read(), 1, is_libra 2731 else: 2732 # function or class 2733 try: 2734 source = inspect.getsource(frame.f_code) 2735 2736 # inspect.getsource is not reliable, see eg: 2737 # https://bugs.python.org/issue35101 2738 # If the code name is not present as definition 2739 # in the beginning of the source, 2740 # then play safe and return the whole script 2741 first_line = source.splitlines()[0] 2742 if re.search(r"\b(class|def)\b\s+\b%s\b" % frame.f_code.co_name, first_line) is None: 2743 with tokenize.open(frame.f_code.co_filename) as fp: 2744 return fp.read(), 1, is_libra 2745 2746 else: 2747 return source, frame.f_code.co_firstlineno, is_libra 2748 except OSError: 2749 logger.exception("Problem getting source") 2750 return None, None, True 2751 2752 2753def format_exception_with_frame_info(e_type, e_value, e_traceback, shorten_filenames=False): 2754 """Need to suppress thonny frames to avoid confusion""" 2755 2756 _traceback_message = "Traceback (most recent call last):\n" 2757 2758 _cause_message = getattr( 2759 traceback, 2760 "_cause_message", 2761 ("\nThe above exception was the direct cause " + "of the following exception:") + "\n\n", 2762 ) 2763 2764 _context_message = getattr( 2765 traceback, 2766 "_context_message", 2767 ("\nDuring handling of the above exception, " + "another exception occurred:") + "\n\n", 2768 ) 2769 2770 def rec_format_exception_with_frame_info(etype, value, tb, chain=True): 2771 # Based on 2772 # https://www.python.org/dev/peps/pep-3134/#enhanced-reporting 2773 # and traceback.format_exception 2774 2775 if etype is None: 2776 etype = type(value) 2777 2778 if tb is None: 2779 tb = value.__traceback__ 2780 2781 if chain: 2782 if value.__cause__ is not None: 2783 yield from rec_format_exception_with_frame_info(None, value.__cause__, None) 2784 yield (_cause_message, None, None, None) 2785 elif value.__context__ is not None and not value.__suppress_context__: 2786 yield from rec_format_exception_with_frame_info(None, value.__context__, None) 2787 yield (_context_message, None, None, None) 2788 2789 if tb is not None: 2790 yield (_traceback_message, None, None, None) 2791 2792 tb_temp = tb 2793 for entry in traceback.extract_tb(tb): 2794 assert tb_temp is not None # actual tb doesn't end before extract_tb 2795 if ( 2796 "cpython_backend" not in entry.filename 2797 and "thonny/backend" not in entry.filename.replace("\\", "/") 2798 and ( 2799 not entry.filename.endswith(os.sep + "ast.py") 2800 or entry.name != "parse" 2801 or etype is not SyntaxError 2802 ) 2803 ): 2804 fmt = ' File "{}", line {}, in {}\n'.format( 2805 entry.filename, entry.lineno, entry.name 2806 ) 2807 2808 if entry.line: 2809 fmt += " {}\n".format(entry.line.strip()) 2810 2811 yield (fmt, id(tb_temp.tb_frame), entry.filename, entry.lineno) 2812 2813 tb_temp = tb_temp.tb_next 2814 2815 assert tb_temp is None # tb was exhausted 2816 2817 for line in traceback.format_exception_only(etype, value): 2818 if etype is SyntaxError and line.endswith("^\n"): 2819 # for some reason it may add several empty lines before ^-line 2820 partlines = line.splitlines() 2821 while len(partlines) >= 2 and partlines[-2].strip() == "": 2822 del partlines[-2] 2823 line = "\n".join(partlines) + "\n" 2824 2825 yield (line, None, None, None) 2826 2827 items = rec_format_exception_with_frame_info(e_type, e_value, e_traceback) 2828 2829 return list(items) 2830 2831 2832def in_debug_mode(): 2833 return os.environ.get("THONNY_DEBUG", False) in [1, "1", True, "True", "true"] 2834 2835 2836def _is_library_file(filename): 2837 return ( 2838 filename is None 2839 or path_startswith(filename, sys.prefix) 2840 or hasattr(sys, "base_prefix") 2841 and path_startswith(filename, sys.base_prefix) 2842 or hasattr(sys, "real_prefix") 2843 and path_startswith(filename, getattr(sys, "real_prefix")) 2844 or site.ENABLE_USER_SITE 2845 and path_startswith(filename, site.getusersitepackages()) 2846 ) 2847 2848 2849def get_backend(): 2850 return _backend 2851