1import ast 2import logging 3import tkinter as tk 4from tkinter import ttk, messagebox 5 6import thonny.memory 7from thonny import get_runner, get_workbench, ui_utils 8from thonny.common import InlineCommand 9from thonny.languages import tr 10from thonny.memory import MemoryFrame 11from thonny.misc_utils import shorten_repr 12from thonny.tktextext import TextFrame 13from thonny.ui_utils import ems_to_pixels 14 15 16class ObjectInspector(ttk.Frame): 17 def __init__(self, master): 18 ttk.Frame.__init__(self, master, style="ViewBody.TFrame") 19 20 self.object_id = None 21 self.object_info = None 22 23 # self._create_general_page() 24 self._create_content_page() 25 self._create_attributes_page() 26 self.active_page = self.content_page 27 self.active_page.grid(row=1, column=0, sticky="nsew") 28 29 toolbar = self._create_toolbar() 30 toolbar.grid(row=0, column=0, sticky="nsew", pady=(0, 1)) 31 32 self.columnconfigure(0, weight=1) 33 self.rowconfigure(1, weight=1) 34 35 get_workbench().bind("ObjectSelect", self.show_object, True) 36 get_workbench().bind("get_object_info_response", self._handle_object_info_event, True) 37 get_workbench().bind("DebuggerResponse", self._handle_progress_event, True) 38 get_workbench().bind("ToplevelResponse", self._handle_progress_event, True) 39 get_workbench().bind("BackendRestart", self._on_backend_restart, True) 40 41 # self.demo() 42 43 def _create_toolbar(self): 44 toolbar = ttk.Frame(self, style="ViewToolbar.TFrame") 45 46 self.title_label = ttk.Label( 47 toolbar, 48 style="ViewToolbar.TLabel", 49 text="" 50 # borderwidth=1, 51 # background=ui_utils.get_main_background() 52 ) 53 self.title_label.grid(row=0, column=3, sticky="nsew", pady=5, padx=5) 54 toolbar.columnconfigure(3, weight=1) 55 56 self.tabs = [] 57 58 def create_tab(col, caption, page): 59 if page == self.active_page: 60 style = "Active.ViewTab.TLabel" 61 else: 62 style = "Inactive.ViewTab.TLabel" 63 tab = ttk.Label(toolbar, text=caption, style=style) 64 tab.grid(row=0, column=col, pady=5, padx=5, sticky="nsew") 65 self.tabs.append(tab) 66 page.tab = tab 67 68 def on_click(event): 69 if self.active_page == page: 70 return 71 else: 72 if self.active_page is not None: 73 self.active_page.grid_forget() 74 self.active_page.tab.configure(style="Inactive.ViewTab.TLabel") 75 76 self.active_page = page 77 page.grid(row=1, column=0, sticky="nsew", padx=0) 78 tab.configure(style="Active.ViewTab.TLabel") 79 if ( 80 self.active_page == self.attributes_page 81 and (self.object_info is None or not self.object_info.get("attributes")) 82 and self.object_id is not None 83 ): 84 self.request_object_info() 85 86 tab.bind("<1>", on_click) 87 88 # create_tab(1, "Overview", self.general_page) 89 create_tab(5, tr("Data"), self.content_page) 90 create_tab(6, tr("Attributes"), self.attributes_page) 91 92 def create_navigation_link(col, image_filename, action, tooltip, padx=0): 93 button = ttk.Button( 94 toolbar, 95 # command=handler, 96 image=get_workbench().get_image(image_filename), 97 style="ViewToolbar.Toolbutton", # TODO: does this cause problems in some Macs? 98 state=tk.NORMAL, 99 ) 100 ui_utils.create_tooltip(button, tooltip) 101 102 button.grid(row=0, column=col, sticky=tk.NE, padx=padx, pady=4) 103 button.bind("<Button-1>", action) 104 return button 105 106 def configure(event): 107 if event.width > 20: 108 self.title_label.configure(wraplength=event.width - 10) 109 110 self.title_label.bind("<Configure>", configure, True) 111 112 self.back_button = create_navigation_link( 113 1, "nav-backward", self.navigate_back, tr("Previous object"), (5, 0) 114 ) 115 self.forward_button = create_navigation_link( 116 2, "nav-forward", self.navigate_forward, tr("Next object") 117 ) 118 self.back_links = [] 119 self.forward_links = [] 120 121 return toolbar 122 123 def _create_content_page(self): 124 self.content_page = ttk.Frame(self, style="ViewBody.TFrame") 125 # type-specific inspectors 126 self.current_content_inspector = None 127 self.content_inspectors = [] 128 # load custom inspectors 129 for insp_class in get_workbench().content_inspector_classes: 130 self.content_inspectors.append(insp_class(self.content_page)) 131 132 # read standard inspectors 133 self.content_inspectors.extend( 134 [ 135 FileHandleInspector(self.content_page), 136 FunctionInspector(self.content_page), 137 StringInspector(self.content_page), 138 ElementsInspector(self.content_page), 139 DictInspector(self.content_page), 140 ImageInspector(self.content_page), 141 IntInspector(self.content_page), 142 FloatInspector(self.content_page), 143 ReprInspector(self.content_page), # fallback content inspector 144 ] 145 ) 146 147 self.content_page.columnconfigure(0, weight=1) 148 self.content_page.rowconfigure(0, weight=1) 149 150 def _create_attributes_page(self): 151 self.attributes_page = AttributesFrame(self) 152 153 def navigate_back(self, event): 154 if len(self.back_links) == 0: 155 return 156 157 self.forward_links.append(self.object_id) 158 self._show_object_by_id(self.back_links.pop(), True) 159 160 def navigate_forward(self, event): 161 if len(self.forward_links) == 0: 162 return 163 164 self.back_links.append(self.object_id) 165 self._show_object_by_id(self.forward_links.pop(), True) 166 167 def show_object(self, event): 168 self._show_object_by_id(event.object_id) 169 170 def _show_object_by_id(self, object_id, via_navigation=False): 171 assert object_id is not None 172 173 if self.winfo_ismapped() and self.object_id != object_id: 174 if not via_navigation and self.object_id is not None: 175 if self.object_id in self.back_links: 176 self.back_links.remove(self.object_id) 177 self.back_links.append(self.object_id) 178 del self.forward_links[:] 179 180 context_id = self.object_id 181 self.object_id = object_id 182 self.set_object_info(None) 183 self._set_title("object @ " + thonny.memory.format_object_id(object_id)) 184 self.request_object_info(context_id=context_id) 185 186 def _on_backend_restart(self, event=None): 187 self.set_object_info(None) 188 self.object_id = None 189 190 def _set_title(self, text): 191 self.title_label.configure(text=text) 192 193 def _handle_object_info_event(self, msg): 194 if self.winfo_ismapped(): 195 if msg.get("error") and not msg.get("info"): 196 self.set_object_info({"error": msg["error"]}) 197 return 198 199 if msg.info["id"] == self.object_id: 200 if hasattr(msg, "not_found") and msg.not_found: 201 self.object_id = None 202 self.set_object_info(None) 203 else: 204 self.set_object_info(msg.info) 205 206 def _handle_progress_event(self, event): 207 if self.object_id is not None: 208 # refresh 209 self.request_object_info() 210 211 def request_object_info(self, context_id=None): 212 # current width and height of the frame are required for 213 # some content providers 214 if self.active_page is not None: 215 frame_width = self.active_page.winfo_width() 216 frame_height = self.active_page.winfo_height() 217 218 # in some cases measures are inaccurate 219 if frame_width < 5 or frame_height < 5: 220 frame_width = None 221 frame_height = None 222 else: 223 frame_width = None 224 frame_height = None 225 226 get_runner().send_command( 227 InlineCommand( 228 "get_object_info", 229 object_id=self.object_id, 230 context_id=context_id, 231 back_links=self.back_links, 232 forward_links=self.forward_links, 233 include_attributes=self.active_page == self.attributes_page, 234 all_attributes=False, 235 frame_width=frame_width, 236 frame_height=frame_height, 237 ) 238 ) 239 240 def set_object_info(self, object_info): 241 self.object_info = object_info 242 if object_info is None or "error" in object_info: 243 if object_info is None: 244 self._set_title("") 245 else: 246 self._set_title(object_info["error"]) 247 if self.current_content_inspector is not None: 248 self.current_content_inspector.grid_remove() 249 self.current_content_inspector = None 250 self.attributes_page.clear() 251 else: 252 self._set_title( 253 object_info["full_type_name"] 254 + " @ " 255 + thonny.memory.format_object_id(object_info["id"]) 256 ) 257 self.attributes_page.update_variables(object_info["attributes"]) 258 self.attributes_page.context_id = object_info["id"] 259 self.update_type_specific_info(object_info) 260 261 # update layout 262 # self._expose(None) 263 # if not self.grid_frame.winfo_ismapped(): 264 # self.grid_frame.grid() 265 266 """ 267 if self.back_links == []: 268 self.back_label.config(foreground="lightgray", cursor="arrow") 269 else: 270 self.back_label.config(foreground="blue", cursor="hand2") 271 272 if self.forward_links == []: 273 self.forward_label.config(foreground="lightgray", cursor="arrow") 274 else: 275 self.forward_label.config(foreground="blue", cursor="hand2") 276 """ 277 278 def update_type_specific_info(self, object_info): 279 content_inspector = None 280 for insp in self.content_inspectors: 281 if insp.applies_to(object_info): 282 content_inspector = insp 283 break 284 285 if content_inspector != self.current_content_inspector: 286 if self.current_content_inspector is not None: 287 self.current_content_inspector.grid_remove() # TODO: or forget? 288 self.current_content_inspector = None 289 290 if content_inspector is not None: 291 content_inspector.grid(row=0, column=0, sticky=tk.NSEW, padx=(0, 0)) 292 293 self.current_content_inspector = content_inspector 294 295 if self.current_content_inspector is not None: 296 self.current_content_inspector.set_object_info(object_info) 297 298 299class ContentInspector: 300 def __init__(self, master): 301 pass 302 303 def set_object_info(self, object_info): 304 pass 305 306 def get_tab_text(self): 307 return "Data" 308 309 def applies_to(self, object_info): 310 return False 311 312 313class FileHandleInspector(TextFrame, ContentInspector): 314 def __init__(self, master): 315 ContentInspector.__init__(self, master) 316 TextFrame.__init__(self, master, read_only=True) 317 self.cache = {} # stores file contents for handle id-s 318 self.config(borderwidth=1) 319 self.text.configure(background="white") 320 self.text.tag_configure("read", foreground="lightgray") 321 322 def applies_to(self, object_info): 323 return "file_content" in object_info or "file_error" in object_info 324 325 def set_object_info(self, object_info): 326 327 if "file_content" not in object_info: 328 logging.exception("File error: " + object_info["file_error"]) 329 return 330 331 assert "file_content" in object_info 332 content = object_info["file_content"] 333 line_count_sep = len(content.split("\n")) 334 # line_count_term = len(content.splitlines()) 335 # char_count = len(content) 336 self.text.configure(height=min(line_count_sep, 10)) 337 self.text.set_content(content) 338 339 assert "file_tell" in object_info 340 # f.tell() gives num of bytes read (minus some magic with linebreaks) 341 342 file_bytes = content.encode(encoding=object_info["file_encoding"]) 343 bytes_read = file_bytes[0 : object_info["file_tell"]] 344 read_content = bytes_read.decode(encoding=object_info["file_encoding"]) 345 read_char_count = len(read_content) 346 # read_line_count_term = (len(content.splitlines()) 347 # - len(content[read_char_count:].splitlines())) 348 349 pos_index = "1.0+" + str(read_char_count) + "c" 350 self.text.tag_add("read", "1.0", pos_index) 351 self.text.see(pos_index) 352 353 # TODO: show this info somewhere 354 """ 355 label.configure(text="Read %d/%d %s, %d/%d %s" 356 % (read_char_count, 357 char_count, 358 "symbol" if char_count == 1 else "symbols", 359 read_line_count_term, 360 line_count_term, 361 "line" if line_count_term == 1 else "lines")) 362 """ 363 364 365class FunctionInspector(TextFrame, ContentInspector): 366 def __init__(self, master): 367 ContentInspector.__init__(self, master) 368 TextFrame.__init__(self, master, read_only=True) 369 self.text.configure(background="white") 370 371 def applies_to(self, object_info): 372 return "source" in object_info 373 374 def get_tab_text(self): 375 return "Code" 376 377 def set_object_info(self, object_info): 378 line_count = len(object_info["source"].split("\n")) 379 self.text.configure(height=min(line_count, 15)) 380 self.text.set_content(object_info["source"]) 381 382 383class StringInspector(TextFrame, ContentInspector): 384 def __init__(self, master): 385 ContentInspector.__init__(self, master) 386 TextFrame.__init__(self, master, read_only=True) 387 # self.config(borderwidth=1) 388 # self.text.configure(background="white") 389 390 def applies_to(self, object_info): 391 return object_info["type"] == repr(str) 392 393 def set_object_info(self, object_info): 394 # TODO: don't show too big string 395 try: 396 content = ast.literal_eval(object_info["repr"]) 397 except SyntaxError: 398 try: 399 # can be shortened 400 content = ast.literal_eval(object_info["repr"] + object_info["repr"][0:1]) 401 except SyntaxError: 402 content = "<can't show string content>" 403 404 line_count_sep = len(content.split("\n")) 405 # line_count_term = len(content.splitlines()) 406 self.text.configure(height=min(line_count_sep, 10)) 407 self.text.set_content(content) 408 """ TODO: 409 label.configure(text="%d %s, %d %s" 410 % (len(content), 411 "symbol" if len(content) == 1 else "symbols", 412 line_count_term, 413 "line" if line_count_term == 1 else "lines")) 414 """ 415 416 417class IntInspector(TextFrame, ContentInspector): 418 def __init__(self, master): 419 ContentInspector.__init__(self, master) 420 TextFrame.__init__( 421 self, master, read_only=True, horizontal_scrollbar=False, font="TkDefaultFont" 422 ) 423 424 def applies_to(self, object_info): 425 return object_info["type"] == repr(int) 426 427 def set_object_info(self, object_info): 428 content = ast.literal_eval(object_info["repr"]) 429 self.text.set_content( 430 object_info["repr"] 431 + "\n\n" 432 + "bin: " 433 + bin(content) 434 + "\n" 435 + "oct: " 436 + oct(content) 437 + "\n" 438 + "hex: " 439 + hex(content) 440 + "\n" 441 ) 442 443 444class FloatInspector(TextFrame, ContentInspector): 445 def __init__(self, master): 446 ContentInspector.__init__(self, master) 447 TextFrame.__init__( 448 self, 449 master, 450 read_only=True, 451 horizontal_scrollbar=False, 452 wrap="word", 453 font="TkDefaultFont", 454 ) 455 456 def applies_to(self, object_info): 457 return object_info["type"] == repr(float) 458 459 def set_object_info(self, object_info): 460 content = object_info["repr"] + "\n\n\n" 461 462 if "as_integer_ratio" in object_info: 463 ratio = object_info["as_integer_ratio"] 464 from decimal import Decimal 465 466 ratio_dec_str = str(Decimal(ratio[0]) / Decimal(ratio[1])) 467 468 if ratio_dec_str != object_info["repr"]: 469 explanation = tr( 470 "The representation above is an approximate value of this float. " 471 "The exact stored value is %s which is about %s" 472 ) 473 474 content += explanation % ( 475 "\n\n %d / %d\n\n" % ratio, 476 "\n\n %s\n\n" % ratio_dec_str, 477 ) 478 479 self.text.set_content(content) 480 481 482class ReprInspector(TextFrame, ContentInspector): 483 def __init__(self, master): 484 ContentInspector.__init__(self, master) 485 TextFrame.__init__(self, master, read_only=True) 486 # self.config(borderwidth=1) 487 # self.text.configure(background="white") 488 489 def applies_to(self, object_info): 490 return True 491 492 def set_object_info(self, object_info): 493 # TODO: don't show too big string 494 content = object_info["repr"] 495 self.text.set_content(content) 496 """ 497 line_count_sep = len(content.split("\n")) 498 line_count_term = len(content.splitlines()) 499 self.text.configure(height=min(line_count_sep, 10)) 500 label.configure(text="%d %s, %d %s" 501 % (len(content), 502 "symbol" if len(content) == 1 else "symbols", 503 line_count_term, 504 "line" if line_count_term == 1 else "lines")) 505 """ 506 507 508class ElementsInspector(thonny.memory.MemoryFrame, ContentInspector): 509 def __init__(self, master): 510 ContentInspector.__init__(self, master) 511 thonny.memory.MemoryFrame.__init__( 512 self, master, ("index", "id", "value"), show_statusbar=True 513 ) 514 515 # self.vert_scrollbar.grid_remove() 516 self.tree.column("index", width=ems_to_pixels(4), anchor=tk.W, stretch=False) 517 self.tree.column("id", width=750, anchor=tk.W, stretch=True) 518 self.tree.column("value", width=750, anchor=tk.W, stretch=True) 519 520 self.tree.heading("index", text=tr("Index"), anchor=tk.W) 521 self.tree.heading("id", text=tr("Value ID"), anchor=tk.W) 522 self.tree.heading("value", text=tr("Value"), anchor=tk.W) 523 524 self.len_label = ttk.Label(self.statusbar, text="", anchor="w") 525 self.len_label.grid(row=0, column=0, sticky="w") 526 self.statusbar.columnconfigure(0, weight=1) 527 528 self.elements_have_indices = None 529 self.update_memory_model() 530 531 get_workbench().bind("ShowView", self.update_memory_model, True) 532 get_workbench().bind("HideView", self.update_memory_model, True) 533 534 def update_memory_model(self, event=None): 535 self._update_columns() 536 537 def _update_columns(self): 538 if get_workbench().in_heap_mode(): 539 if self.elements_have_indices: 540 self.tree.configure(displaycolumns=("index", "id")) 541 else: 542 self.tree.configure(displaycolumns=("id",)) 543 else: 544 if self.elements_have_indices: 545 self.tree.configure(displaycolumns=("index", "value")) 546 else: 547 self.tree.configure(displaycolumns=("value")) 548 549 def applies_to(self, object_info): 550 return "elements" in object_info 551 552 def on_select(self, event): 553 pass 554 555 def on_double_click(self, event): 556 self.show_selected_object_info() 557 558 def set_object_info(self, object_info): 559 assert "elements" in object_info 560 561 self.elements_have_indices = object_info["type"] in (repr(tuple), repr(list)) 562 self._update_columns() 563 self.context_id = object_info["id"] 564 565 self._clear_tree() 566 index = 0 567 # TODO: don't show too big number of elements 568 for element in object_info["elements"]: 569 node_id = self.tree.insert("", "end") 570 if self.elements_have_indices: 571 self.tree.set(node_id, "index", index) 572 else: 573 self.tree.set(node_id, "index", "") 574 575 self.tree.set(node_id, "id", thonny.memory.format_object_id(element.id)) 576 self.tree.set( 577 node_id, "value", shorten_repr(element.repr, thonny.memory.MAX_REPR_LENGTH_IN_GRID) 578 ) 579 index += 1 580 581 count = len(object_info["elements"]) 582 self.len_label.configure(text=" len: %d" % count) 583 584 585class DictInspector(thonny.memory.MemoryFrame, ContentInspector): 586 def __init__(self, master): 587 ContentInspector.__init__(self, master) 588 thonny.memory.MemoryFrame.__init__( 589 self, master, ("key_id", "id", "key", "value"), show_statusbar=True 590 ) 591 # self.configure(border=1) 592 # self.vert_scrollbar.grid_remove() 593 self.tree.column("key_id", width=ems_to_pixels(7), anchor=tk.W, stretch=False) 594 self.tree.column("key", width=100, anchor=tk.W, stretch=False) 595 self.tree.column("id", width=750, anchor=tk.W, stretch=True) 596 self.tree.column("value", width=750, anchor=tk.W, stretch=True) 597 598 self.tree.heading("key_id", text=tr("Key ID"), anchor=tk.W) 599 self.tree.heading("key", text=tr("Key"), anchor=tk.W) 600 self.tree.heading("id", text=tr("Value ID"), anchor=tk.W) 601 self.tree.heading("value", text=tr("Value"), anchor=tk.W) 602 603 self.len_label = ttk.Label(self.statusbar, text="", anchor="w") 604 self.len_label.grid(row=0, column=0, sticky="w") 605 self.statusbar.columnconfigure(0, weight=1) 606 607 self.update_memory_model() 608 609 def update_memory_model(self, event=None): 610 if get_workbench().in_heap_mode(): 611 self.tree.configure(displaycolumns=("key_id", "id")) 612 else: 613 self.tree.configure(displaycolumns=("key", "value")) 614 615 def applies_to(self, object_info): 616 return "entries" in object_info 617 618 def on_select(self, event): 619 pass 620 621 def on_double_click(self, event): 622 # NB! this selects value 623 self.show_selected_object_info() 624 625 def set_object_info(self, object_info): 626 assert "entries" in object_info 627 self.context_id = object_info["id"] 628 629 self._clear_tree() 630 # TODO: don't show too big number of elements 631 for key, value in object_info["entries"]: 632 node_id = self.tree.insert("", "end") 633 self.tree.set(node_id, "key_id", thonny.memory.format_object_id(key.id)) 634 self.tree.set( 635 node_id, "key", shorten_repr(key.repr, thonny.memory.MAX_REPR_LENGTH_IN_GRID) 636 ) 637 self.tree.set(node_id, "id", thonny.memory.format_object_id(value.id)) 638 self.tree.set( 639 node_id, "value", shorten_repr(value.repr, thonny.memory.MAX_REPR_LENGTH_IN_GRID) 640 ) 641 642 count = len(object_info["entries"]) 643 self.len_label.configure(text=" len: %d" % count) 644 self.update_memory_model() 645 646 647class ImageInspector(ContentInspector, tk.Frame): 648 def __init__(self, master): 649 tk.Frame.__init__(self, master) 650 ContentInspector.__init__(self, master) 651 self.label = tk.Label(self, anchor="nw") 652 self.label.grid(row=0, column=0, sticky="nsew") 653 self.rowconfigure(0, weight=1) 654 self.columnconfigure(0, weight=1) 655 656 def set_object_info(self, object_info): 657 if isinstance(object_info["image_data"], bytes): 658 import base64 659 660 data = base64.b64encode(object_info["image_data"]) 661 elif isinstance(object_info["image_data"], str): 662 data = object_info["image_data"] 663 else: 664 self.label.configure( 665 image=None, text="Unsupported image data (%s)" % type(object_info["image_data"]) 666 ) 667 return 668 669 try: 670 self.image = tk.PhotoImage(data=data) 671 self.label.configure(image=self.image) 672 except Exception as e: 673 self.label.configure(image=None, text="Unsupported image data (%s)" % e) 674 675 def applies_to(self, object_info): 676 return "image_data" in object_info 677 678 679class AttributesFrame(thonny.memory.VariablesFrame): 680 def __init__(self, master): 681 thonny.memory.VariablesFrame.__init__(self, master) 682 self.configure(border=0) 683 684 def on_select(self, event): 685 pass 686 687 def on_double_click(self, event): 688 self.show_selected_object_info() 689 690 def show_selected_object_info(self): 691 object_id = self.get_object_id() 692 if object_id is None: 693 return 694 695 iid = self.tree.focus() 696 if not iid: 697 return 698 repr_str = self.tree.item(iid)["values"][2] 699 700 if repr_str == "<bound_method>": 701 from thonny.plugins.micropython import MicroPythonProxy 702 703 if isinstance(get_runner().get_backend_proxy(), MicroPythonProxy): 704 messagebox.showinfo( 705 "Not supported", 706 "Inspecting bound methods is not supported with MicroPython", 707 master=self, 708 ) 709 return 710 711 get_workbench().event_generate("ObjectSelect", object_id=object_id) 712 713 714def load_plugin() -> None: 715 get_workbench().add_view(ObjectInspector, tr("Object inspector"), "se") 716