1# Copyright 2016 The TensorFlow Authors. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14# ============================================================================== 15"""Building Blocks of TensorFlow Debugger Command-Line Interface.""" 16import copy 17import os 18import re 19import sre_constants 20import traceback 21 22import numpy as np 23 24from tensorflow.python.client import pywrap_tf_session 25from tensorflow.python.platform import gfile 26 27HELP_INDENT = " " 28 29EXPLICIT_USER_EXIT = "explicit_user_exit" 30REGEX_MATCH_LINES_KEY = "regex_match_lines" 31INIT_SCROLL_POS_KEY = "init_scroll_pos" 32 33MAIN_MENU_KEY = "mm:" 34 35 36class CommandLineExit(Exception): 37 38 def __init__(self, exit_token=None): 39 Exception.__init__(self) 40 self._exit_token = exit_token 41 42 @property 43 def exit_token(self): 44 return self._exit_token 45 46 47class RichLine: 48 """Rich single-line text. 49 50 Attributes: 51 text: A plain string, the raw text represented by this object. Should not 52 contain newlines. 53 font_attr_segs: A list of (start, end, font attribute) triples, representing 54 richness information applied to substrings of text. 55 """ 56 57 def __init__(self, text="", font_attr=None): 58 """Construct a RichLine with no rich attributes or a single attribute. 59 60 Args: 61 text: Raw text string 62 font_attr: If specified, a single font attribute to be applied to the 63 entire text. Extending this object via concatenation allows creation 64 of text with varying attributes. 65 """ 66 # TODO(ebreck) Make .text and .font_attr protected members when we no 67 # longer need public access. 68 self.text = text 69 if font_attr: 70 self.font_attr_segs = [(0, len(text), font_attr)] 71 else: 72 self.font_attr_segs = [] 73 74 def __add__(self, other): 75 """Concatenate two chunks of maybe rich text to make a longer rich line. 76 77 Does not modify self. 78 79 Args: 80 other: Another piece of text to concatenate with this one. 81 If it is a plain str, it will be appended to this string with no 82 attributes. If it is a RichLine, it will be appended to this string 83 with its attributes preserved. 84 85 Returns: 86 A new RichLine comprising both chunks of text, with appropriate 87 attributes applied to the corresponding substrings. 88 """ 89 ret = RichLine() 90 if isinstance(other, str): 91 ret.text = self.text + other 92 ret.font_attr_segs = self.font_attr_segs[:] 93 return ret 94 elif isinstance(other, RichLine): 95 ret.text = self.text + other.text 96 ret.font_attr_segs = self.font_attr_segs[:] 97 old_len = len(self.text) 98 for start, end, font_attr in other.font_attr_segs: 99 ret.font_attr_segs.append((old_len + start, old_len + end, font_attr)) 100 return ret 101 else: 102 raise TypeError("%r cannot be concatenated with a RichLine" % other) 103 104 def __len__(self): 105 return len(self.text) 106 107 108def rich_text_lines_from_rich_line_list(rich_text_list, annotations=None): 109 """Convert a list of RichLine objects or strings to a RichTextLines object. 110 111 Args: 112 rich_text_list: a list of RichLine objects or strings 113 annotations: annotations for the resultant RichTextLines object. 114 115 Returns: 116 A corresponding RichTextLines object. 117 """ 118 lines = [] 119 font_attr_segs = {} 120 for i, rl in enumerate(rich_text_list): 121 if isinstance(rl, RichLine): 122 lines.append(rl.text) 123 if rl.font_attr_segs: 124 font_attr_segs[i] = rl.font_attr_segs 125 else: 126 lines.append(rl) 127 return RichTextLines(lines, font_attr_segs, annotations=annotations) 128 129 130def get_tensorflow_version_lines(include_dependency_versions=False): 131 """Generate RichTextLines with TensorFlow version info. 132 133 Args: 134 include_dependency_versions: Include the version of TensorFlow's key 135 dependencies, such as numpy. 136 137 Returns: 138 A formatted, multi-line `RichTextLines` object. 139 """ 140 lines = ["TensorFlow version: %s" % pywrap_tf_session.__version__] 141 lines.append("") 142 if include_dependency_versions: 143 lines.append("Dependency version(s):") 144 lines.append(" numpy: %s" % np.__version__) 145 lines.append("") 146 return RichTextLines(lines) 147 148 149class RichTextLines: 150 """Rich multi-line text. 151 152 Line-by-line text output, with font attributes (e.g., color) and annotations 153 (e.g., indices in a multi-dimensional tensor). Used as the text output of CLI 154 commands. Can be rendered on terminal environments such as curses. 155 156 This is not to be confused with Rich Text Format (RTF). This class is for text 157 lines only. 158 """ 159 160 def __init__(self, lines, font_attr_segs=None, annotations=None): 161 """Constructor of RichTextLines. 162 163 Args: 164 lines: A list of str or a single str, representing text output to 165 screen. The latter case is for convenience when the text output is 166 single-line. 167 font_attr_segs: A map from 0-based row index to a list of 3-tuples. 168 It lists segments in each row that have special font attributes, such 169 as colors, that are not the default attribute. For example: 170 {1: [(0, 3, "red"), (4, 7, "green")], 2: [(10, 20, "yellow")]} 171 172 In each tuple, the 1st element is the start index of the segment. The 173 2nd element is the end index, in an "open interval" fashion. The 3rd 174 element is an object or a list of objects that represents the font 175 attribute. Colors are represented as strings as in the examples above. 176 annotations: A map from 0-based row index to any object for annotating 177 the row. A typical use example is annotating rows of the output as 178 indices in a multi-dimensional tensor. For example, consider the 179 following text representation of a 3x2x2 tensor: 180 [[[0, 0], [0, 0]], 181 [[0, 0], [0, 0]], 182 [[0, 0], [0, 0]]] 183 The annotation can indicate the indices of the first element shown in 184 each row, i.e., 185 {0: [0, 0, 0], 1: [1, 0, 0], 2: [2, 0, 0]} 186 This information can make display of tensors on screen clearer and can 187 help the user navigate (scroll) to the desired location in a large 188 tensor. 189 190 Raises: 191 ValueError: If lines is of invalid type. 192 """ 193 if isinstance(lines, list): 194 self._lines = lines 195 elif isinstance(lines, str): 196 self._lines = [lines] 197 else: 198 raise ValueError("Unexpected type in lines: %s" % type(lines)) 199 200 self._font_attr_segs = font_attr_segs 201 if not self._font_attr_segs: 202 self._font_attr_segs = {} 203 # TODO(cais): Refactor to collections.defaultdict(list) to simplify code. 204 205 self._annotations = annotations 206 if not self._annotations: 207 self._annotations = {} 208 # TODO(cais): Refactor to collections.defaultdict(list) to simplify code. 209 210 @property 211 def lines(self): 212 return self._lines 213 214 @property 215 def font_attr_segs(self): 216 return self._font_attr_segs 217 218 @property 219 def annotations(self): 220 return self._annotations 221 222 def num_lines(self): 223 return len(self._lines) 224 225 def slice(self, begin, end): 226 """Slice a RichTextLines object. 227 228 The object itself is not changed. A sliced instance is returned. 229 230 Args: 231 begin: (int) Beginning line index (inclusive). Must be >= 0. 232 end: (int) Ending line index (exclusive). Must be >= 0. 233 234 Returns: 235 (RichTextLines) Sliced output instance of RichTextLines. 236 237 Raises: 238 ValueError: If begin or end is negative. 239 """ 240 241 if begin < 0 or end < 0: 242 raise ValueError("Encountered negative index.") 243 244 # Copy lines. 245 lines = self.lines[begin:end] 246 247 # Slice font attribute segments. 248 font_attr_segs = {} 249 for key in self.font_attr_segs: 250 if key >= begin and key < end: 251 font_attr_segs[key - begin] = self.font_attr_segs[key] 252 253 # Slice annotations. 254 annotations = {} 255 for key in self.annotations: 256 if not isinstance(key, int): 257 # Annotations can contain keys that are not line numbers. 258 annotations[key] = self.annotations[key] 259 elif key >= begin and key < end: 260 annotations[key - begin] = self.annotations[key] 261 262 return RichTextLines( 263 lines, font_attr_segs=font_attr_segs, annotations=annotations) 264 265 def extend(self, other): 266 """Extend this instance of RichTextLines with another instance. 267 268 The extension takes effect on the text lines, the font attribute segments, 269 as well as the annotations. The line indices in the font attribute 270 segments and the annotations are adjusted to account for the existing 271 lines. If there are duplicate, non-line-index fields in the annotations, 272 the value from the input argument "other" will override that in this 273 instance. 274 275 Args: 276 other: (RichTextLines) The other RichTextLines instance to be appended at 277 the end of this instance. 278 """ 279 280 orig_num_lines = self.num_lines() # Record original number of lines. 281 282 # Merge the lines. 283 self._lines.extend(other.lines) 284 285 # Merge the font_attr_segs. 286 for line_index in other.font_attr_segs: 287 self._font_attr_segs[orig_num_lines + line_index] = ( 288 other.font_attr_segs[line_index]) 289 290 # Merge the annotations. 291 for key in other.annotations: 292 if isinstance(key, int): 293 self._annotations[orig_num_lines + key] = (other.annotations[key]) 294 else: 295 self._annotations[key] = other.annotations[key] 296 297 def _extend_before(self, other): 298 """Add another RichTextLines object to the front. 299 300 Args: 301 other: (RichTextLines) The other object to add to the front to this 302 object. 303 """ 304 305 other_num_lines = other.num_lines() # Record original number of lines. 306 307 # Merge the lines. 308 self._lines = other.lines + self._lines 309 310 # Merge the font_attr_segs. 311 new_font_attr_segs = {} 312 for line_index in self.font_attr_segs: 313 new_font_attr_segs[other_num_lines + line_index] = ( 314 self.font_attr_segs[line_index]) 315 new_font_attr_segs.update(other.font_attr_segs) 316 self._font_attr_segs = new_font_attr_segs 317 318 # Merge the annotations. 319 new_annotations = {} 320 for key in self._annotations: 321 if isinstance(key, int): 322 new_annotations[other_num_lines + key] = (self.annotations[key]) 323 else: 324 new_annotations[key] = other.annotations[key] 325 326 new_annotations.update(other.annotations) 327 self._annotations = new_annotations 328 329 def append(self, line, font_attr_segs=None): 330 """Append a single line of text. 331 332 Args: 333 line: (str) The text to be added to the end. 334 font_attr_segs: (list of tuples) Font attribute segments of the appended 335 line. 336 """ 337 338 self._lines.append(line) 339 if font_attr_segs: 340 self._font_attr_segs[len(self._lines) - 1] = font_attr_segs 341 342 def append_rich_line(self, rich_line): 343 self.append(rich_line.text, rich_line.font_attr_segs) 344 345 def prepend(self, line, font_attr_segs=None): 346 """Prepend (i.e., add to the front) a single line of text. 347 348 Args: 349 line: (str) The text to be added to the front. 350 font_attr_segs: (list of tuples) Font attribute segments of the appended 351 line. 352 """ 353 354 other = RichTextLines(line) 355 if font_attr_segs: 356 other.font_attr_segs[0] = font_attr_segs 357 self._extend_before(other) 358 359 def write_to_file(self, file_path): 360 """Write the object itself to file, in a plain format. 361 362 The font_attr_segs and annotations are ignored. 363 364 Args: 365 file_path: (str) path of the file to write to. 366 """ 367 368 with gfile.Open(file_path, "w") as f: 369 for line in self._lines: 370 f.write(line + "\n") 371 372 # TODO(cais): Add a method to allow appending to a line in RichTextLines with 373 # both text and font_attr_segs. 374 375 376def regex_find(orig_screen_output, regex, font_attr): 377 """Perform regex match in rich text lines. 378 379 Produces a new RichTextLines object with font_attr_segs containing highlighted 380 regex matches. 381 382 Example use cases include: 383 1) search for specific items in a large list of items, and 384 2) search for specific numerical values in a large tensor. 385 386 Args: 387 orig_screen_output: The original RichTextLines, in which the regex find 388 is to be performed. 389 regex: The regex used for matching. 390 font_attr: Font attribute used for highlighting the found result. 391 392 Returns: 393 A modified copy of orig_screen_output. 394 395 Raises: 396 ValueError: If input str regex is not a valid regular expression. 397 """ 398 new_screen_output = RichTextLines( 399 orig_screen_output.lines, 400 font_attr_segs=copy.deepcopy(orig_screen_output.font_attr_segs), 401 annotations=orig_screen_output.annotations) 402 403 try: 404 re_prog = re.compile(regex) 405 except sre_constants.error: 406 raise ValueError("Invalid regular expression: \"%s\"" % regex) 407 408 regex_match_lines = [] 409 for i, line in enumerate(new_screen_output.lines): 410 find_it = re_prog.finditer(line) 411 412 match_segs = [] 413 for match in find_it: 414 match_segs.append((match.start(), match.end(), font_attr)) 415 416 if match_segs: 417 if i not in new_screen_output.font_attr_segs: 418 new_screen_output.font_attr_segs[i] = match_segs 419 else: 420 new_screen_output.font_attr_segs[i].extend(match_segs) 421 new_screen_output.font_attr_segs[i] = sorted( 422 new_screen_output.font_attr_segs[i], key=lambda x: x[0]) 423 regex_match_lines.append(i) 424 425 new_screen_output.annotations[REGEX_MATCH_LINES_KEY] = regex_match_lines 426 return new_screen_output 427 428 429def wrap_rich_text_lines(inp, cols): 430 """Wrap RichTextLines according to maximum number of columns. 431 432 Produces a new RichTextLines object with the text lines, font_attr_segs and 433 annotations properly wrapped. This ought to be used sparingly, as in most 434 cases, command handlers producing RichTextLines outputs should know the 435 screen/panel width via the screen_info kwarg and should produce properly 436 length-limited lines in the output accordingly. 437 438 Args: 439 inp: Input RichTextLines object. 440 cols: Number of columns, as an int. 441 442 Returns: 443 1) A new instance of RichTextLines, with line lengths limited to cols. 444 2) A list of new (wrapped) line index. For example, if the original input 445 consists of three lines and only the second line is wrapped, and it's 446 wrapped into two lines, this return value will be: [0, 1, 3]. 447 Raises: 448 ValueError: If inputs have invalid types. 449 """ 450 451 new_line_indices = [] 452 453 if not isinstance(inp, RichTextLines): 454 raise ValueError("Invalid type of input screen_output") 455 456 if not isinstance(cols, int): 457 raise ValueError("Invalid type of input cols") 458 459 out = RichTextLines([]) 460 461 row_counter = 0 # Counter for new row index 462 for i, line in enumerate(inp.lines): 463 new_line_indices.append(out.num_lines()) 464 465 if i in inp.annotations: 466 out.annotations[row_counter] = inp.annotations[i] 467 468 if len(line) <= cols: 469 # No wrapping. 470 out.lines.append(line) 471 if i in inp.font_attr_segs: 472 out.font_attr_segs[row_counter] = inp.font_attr_segs[i] 473 474 row_counter += 1 475 else: 476 # Wrap. 477 wlines = [] # Wrapped lines. 478 479 osegs = [] 480 if i in inp.font_attr_segs: 481 osegs = inp.font_attr_segs[i] 482 483 idx = 0 484 while idx < len(line): 485 if idx + cols > len(line): 486 rlim = len(line) 487 else: 488 rlim = idx + cols 489 490 wlines.append(line[idx:rlim]) 491 for seg in osegs: 492 if (seg[0] < rlim) and (seg[1] >= idx): 493 # Calculate left bound within wrapped line. 494 if seg[0] >= idx: 495 lb = seg[0] - idx 496 else: 497 lb = 0 498 499 # Calculate right bound within wrapped line. 500 if seg[1] < rlim: 501 rb = seg[1] - idx 502 else: 503 rb = rlim - idx 504 505 if rb > lb: # Omit zero-length segments. 506 wseg = (lb, rb, seg[2]) 507 if row_counter not in out.font_attr_segs: 508 out.font_attr_segs[row_counter] = [wseg] 509 else: 510 out.font_attr_segs[row_counter].append(wseg) 511 512 idx += cols 513 row_counter += 1 514 515 out.lines.extend(wlines) 516 517 # Copy over keys of annotation that are not row indices. 518 for key in inp.annotations: 519 if not isinstance(key, int): 520 out.annotations[key] = inp.annotations[key] 521 522 return out, new_line_indices 523 524 525class CommandHandlerRegistry: 526 """Registry of command handlers for CLI. 527 528 Handler methods (callables) for user commands can be registered with this 529 class, which then is able to dispatch commands to the correct handlers and 530 retrieve the RichTextLines output. 531 532 For example, suppose you have the following handler defined: 533 def echo(argv, screen_info=None): 534 return RichTextLines(["arguments = %s" % " ".join(argv), 535 "screen_info = " + repr(screen_info)]) 536 537 you can register the handler with the command prefix "echo" and alias "e": 538 registry = CommandHandlerRegistry() 539 registry.register_command_handler("echo", echo, 540 "Echo arguments, along with screen info", prefix_aliases=["e"]) 541 542 then to invoke this command handler with some arguments and screen_info, do: 543 registry.dispatch_command("echo", ["foo", "bar"], screen_info={"cols": 80}) 544 545 or with the prefix alias: 546 registry.dispatch_command("e", ["foo", "bar"], screen_info={"cols": 80}) 547 548 The call will return a RichTextLines object which can be rendered by a CLI. 549 """ 550 551 HELP_COMMAND = "help" 552 HELP_COMMAND_ALIASES = ["h"] 553 VERSION_COMMAND = "version" 554 VERSION_COMMAND_ALIASES = ["ver"] 555 556 def __init__(self): 557 # A dictionary from command prefix to handler. 558 self._handlers = {} 559 560 # A dictionary from prefix alias to prefix. 561 self._alias_to_prefix = {} 562 563 # A dictionary from prefix to aliases. 564 self._prefix_to_aliases = {} 565 566 # A dictionary from command prefix to help string. 567 self._prefix_to_help = {} 568 569 # Introductory text to help information. 570 self._help_intro = None 571 572 # Register a default handler for the command "help". 573 self.register_command_handler( 574 self.HELP_COMMAND, 575 self._help_handler, 576 "Print this help message.", 577 prefix_aliases=self.HELP_COMMAND_ALIASES) 578 579 # Register a default handler for the command "version". 580 self.register_command_handler( 581 self.VERSION_COMMAND, 582 self._version_handler, 583 "Print the versions of TensorFlow and its key dependencies.", 584 prefix_aliases=self.VERSION_COMMAND_ALIASES) 585 586 def register_command_handler(self, 587 prefix, 588 handler, 589 help_info, 590 prefix_aliases=None): 591 """Register a callable as a command handler. 592 593 Args: 594 prefix: Command prefix, i.e., the first word in a command, e.g., 595 "print" as in "print tensor_1". 596 handler: A callable of the following signature: 597 foo_handler(argv, screen_info=None), 598 where argv is the argument vector (excluding the command prefix) and 599 screen_info is a dictionary containing information about the screen, 600 such as number of columns, e.g., {"cols": 100}. 601 The callable should return: 602 1) a RichTextLines object representing the screen output. 603 604 The callable can also raise an exception of the type CommandLineExit, 605 which if caught by the command-line interface, will lead to its exit. 606 The exception can optionally carry an exit token of arbitrary type. 607 help_info: A help string. 608 prefix_aliases: Aliases for the command prefix, as a list of str. E.g., 609 shorthands for the command prefix: ["p", "pr"] 610 611 Raises: 612 ValueError: If 613 1) the prefix is empty, or 614 2) handler is not callable, or 615 3) a handler is already registered for the prefix, or 616 4) elements in prefix_aliases clash with existing aliases. 617 5) help_info is not a str. 618 """ 619 620 if not prefix: 621 raise ValueError("Empty command prefix") 622 623 if prefix in self._handlers: 624 raise ValueError( 625 "A handler is already registered for command prefix \"%s\"" % prefix) 626 627 # Make sure handler is callable. 628 if not callable(handler): 629 raise ValueError("handler is not callable") 630 631 # Make sure that help info is a string. 632 if not isinstance(help_info, str): 633 raise ValueError("help_info is not a str") 634 635 # Process prefix aliases. 636 if prefix_aliases: 637 for alias in prefix_aliases: 638 if self._resolve_prefix(alias): 639 raise ValueError( 640 "The prefix alias \"%s\" clashes with existing prefixes or " 641 "aliases." % alias) 642 self._alias_to_prefix[alias] = prefix 643 644 self._prefix_to_aliases[prefix] = prefix_aliases 645 646 # Store handler. 647 self._handlers[prefix] = handler 648 649 # Store help info. 650 self._prefix_to_help[prefix] = help_info 651 652 def dispatch_command(self, prefix, argv, screen_info=None): 653 """Handles a command by dispatching it to a registered command handler. 654 655 Args: 656 prefix: Command prefix, as a str, e.g., "print". 657 argv: Command argument vector, excluding the command prefix, represented 658 as a list of str, e.g., 659 ["tensor_1"] 660 screen_info: A dictionary containing screen info, e.g., {"cols": 100}. 661 662 Returns: 663 An instance of RichTextLines or None. If any exception is caught during 664 the invocation of the command handler, the RichTextLines will wrap the 665 error type and message. 666 667 Raises: 668 ValueError: If 669 1) prefix is empty, or 670 2) no command handler is registered for the command prefix, or 671 3) the handler is found for the prefix, but it fails to return a 672 RichTextLines or raise any exception. 673 CommandLineExit: 674 If the command handler raises this type of exception, this method will 675 simply pass it along. 676 """ 677 if not prefix: 678 raise ValueError("Prefix is empty") 679 680 resolved_prefix = self._resolve_prefix(prefix) 681 if not resolved_prefix: 682 raise ValueError("No handler is registered for command prefix \"%s\"" % 683 prefix) 684 685 handler = self._handlers[resolved_prefix] 686 try: 687 output = handler(argv, screen_info=screen_info) 688 except CommandLineExit as e: 689 raise e 690 except SystemExit as e: 691 # Special case for syntax errors caught by argparse. 692 lines = ["Syntax error for command: %s" % prefix, 693 "For help, do \"help %s\"" % prefix] 694 output = RichTextLines(lines) 695 696 except BaseException as e: # pylint: disable=broad-except 697 lines = ["Error occurred during handling of command: %s %s:" % 698 (resolved_prefix, " ".join(argv)), "%s: %s" % (type(e), str(e))] 699 700 # Include traceback of the exception. 701 lines.append("") 702 lines.extend(traceback.format_exc().split("\n")) 703 704 output = RichTextLines(lines) 705 706 if not isinstance(output, RichTextLines) and output is not None: 707 raise ValueError( 708 "Return value from command handler %s is not None or a RichTextLines " 709 "instance" % str(handler)) 710 711 return output 712 713 def is_registered(self, prefix): 714 """Test if a command prefix or its alias is has a registered handler. 715 716 Args: 717 prefix: A prefix or its alias, as a str. 718 719 Returns: 720 True iff a handler is registered for prefix. 721 """ 722 return self._resolve_prefix(prefix) is not None 723 724 def get_help(self, cmd_prefix=None): 725 """Compile help information into a RichTextLines object. 726 727 Args: 728 cmd_prefix: Optional command prefix. As the prefix itself or one of its 729 aliases. 730 731 Returns: 732 A RichTextLines object containing the help information. If cmd_prefix 733 is None, the return value will be the full command-line help. Otherwise, 734 it will be the help information for the specified command. 735 """ 736 if not cmd_prefix: 737 # Print full help information, in sorted order of the command prefixes. 738 help_info = RichTextLines([]) 739 if self._help_intro: 740 # If help intro is available, show it at the beginning. 741 help_info.extend(self._help_intro) 742 743 sorted_prefixes = sorted(self._handlers) 744 for cmd_prefix in sorted_prefixes: 745 lines = self._get_help_for_command_prefix(cmd_prefix) 746 lines.append("") 747 lines.append("") 748 help_info.extend(RichTextLines(lines)) 749 750 return help_info 751 else: 752 return RichTextLines(self._get_help_for_command_prefix(cmd_prefix)) 753 754 def set_help_intro(self, help_intro): 755 """Set an introductory message to help output. 756 757 Args: 758 help_intro: (RichTextLines) Rich text lines appended to the 759 beginning of the output of the command "help", as introductory 760 information. 761 """ 762 self._help_intro = help_intro 763 764 def _help_handler(self, args, screen_info=None): 765 """Command handler for "help". 766 767 "help" is a common command that merits built-in support from this class. 768 769 Args: 770 args: Command line arguments to "help" (not including "help" itself). 771 screen_info: (dict) Information regarding the screen, e.g., the screen 772 width in characters: {"cols": 80} 773 774 Returns: 775 (RichTextLines) Screen text output. 776 """ 777 778 _ = screen_info # Unused currently. 779 780 if not args: 781 return self.get_help() 782 elif len(args) == 1: 783 return self.get_help(args[0]) 784 else: 785 return RichTextLines(["ERROR: help takes only 0 or 1 input argument."]) 786 787 def _version_handler(self, args, screen_info=None): 788 del args # Unused currently. 789 del screen_info # Unused currently. 790 return get_tensorflow_version_lines(include_dependency_versions=True) 791 792 def _resolve_prefix(self, token): 793 """Resolve command prefix from the prefix itself or its alias. 794 795 Args: 796 token: a str to be resolved. 797 798 Returns: 799 If resolvable, the resolved command prefix. 800 If not resolvable, None. 801 """ 802 if token in self._handlers: 803 return token 804 elif token in self._alias_to_prefix: 805 return self._alias_to_prefix[token] 806 else: 807 return None 808 809 def _get_help_for_command_prefix(self, cmd_prefix): 810 """Compile the help information for a given command prefix. 811 812 Args: 813 cmd_prefix: Command prefix, as the prefix itself or one of its 814 aliases. 815 816 Returns: 817 A list of str as the help information fo cmd_prefix. If the cmd_prefix 818 does not exist, the returned list of str will indicate that. 819 """ 820 lines = [] 821 822 resolved_prefix = self._resolve_prefix(cmd_prefix) 823 if not resolved_prefix: 824 lines.append("Invalid command prefix: \"%s\"" % cmd_prefix) 825 return lines 826 827 lines.append(resolved_prefix) 828 829 if resolved_prefix in self._prefix_to_aliases: 830 lines.append(HELP_INDENT + "Aliases: " + ", ".join( 831 self._prefix_to_aliases[resolved_prefix])) 832 833 lines.append("") 834 help_lines = self._prefix_to_help[resolved_prefix].split("\n") 835 for line in help_lines: 836 lines.append(HELP_INDENT + line) 837 838 return lines 839 840 841class TabCompletionRegistry: 842 """Registry for tab completion responses.""" 843 844 def __init__(self): 845 self._comp_dict = {} 846 847 # TODO(cais): Rename method names with "comp" to "*completion*" to avoid 848 # confusion. 849 850 def register_tab_comp_context(self, context_words, comp_items): 851 """Register a tab-completion context. 852 853 Register that, for each word in context_words, the potential tab-completions 854 are the words in comp_items. 855 856 A context word is a pre-existing, completed word in the command line that 857 determines how tab-completion works for another, incomplete word in the same 858 command line. 859 Completion items consist of potential candidates for the incomplete word. 860 861 To give a general example, a context word can be "drink", and the completion 862 items can be ["coffee", "tea", "water"] 863 864 Note: A context word can be empty, in which case the context is for the 865 top-level commands. 866 867 Args: 868 context_words: A list of context words belonging to the context being 869 registered. It is a list of str, instead of a single string, to support 870 synonym words triggering the same tab-completion context, e.g., 871 both "drink" and the short-hand "dr" can trigger the same context. 872 comp_items: A list of completion items, as a list of str. 873 874 Raises: 875 TypeError: if the input arguments are not all of the correct types. 876 """ 877 878 if not isinstance(context_words, list): 879 raise TypeError("Incorrect type in context_list: Expected list, got %s" % 880 type(context_words)) 881 882 if not isinstance(comp_items, list): 883 raise TypeError("Incorrect type in comp_items: Expected list, got %s" % 884 type(comp_items)) 885 886 # Sort the completion items on registration, so that later during 887 # get_completions calls, no sorting will be necessary. 888 sorted_comp_items = sorted(comp_items) 889 890 for context_word in context_words: 891 self._comp_dict[context_word] = sorted_comp_items 892 893 def deregister_context(self, context_words): 894 """Deregister a list of context words. 895 896 Args: 897 context_words: A list of context words to deregister, as a list of str. 898 899 Raises: 900 KeyError: if there are word(s) in context_words that do not correspond 901 to any registered contexts. 902 """ 903 904 for context_word in context_words: 905 if context_word not in self._comp_dict: 906 raise KeyError("Cannot deregister unregistered context word \"%s\"" % 907 context_word) 908 909 for context_word in context_words: 910 del self._comp_dict[context_word] 911 912 def extend_comp_items(self, context_word, new_comp_items): 913 """Add a list of completion items to a completion context. 914 915 Args: 916 context_word: A single completion word as a string. The extension will 917 also apply to all other context words of the same context. 918 new_comp_items: (list of str) New completion items to add. 919 920 Raises: 921 KeyError: if the context word has not been registered. 922 """ 923 924 if context_word not in self._comp_dict: 925 raise KeyError("Context word \"%s\" has not been registered" % 926 context_word) 927 928 self._comp_dict[context_word].extend(new_comp_items) 929 self._comp_dict[context_word] = sorted(self._comp_dict[context_word]) 930 931 def remove_comp_items(self, context_word, comp_items): 932 """Remove a list of completion items from a completion context. 933 934 Args: 935 context_word: A single completion word as a string. The removal will 936 also apply to all other context words of the same context. 937 comp_items: Completion items to remove. 938 939 Raises: 940 KeyError: if the context word has not been registered. 941 """ 942 943 if context_word not in self._comp_dict: 944 raise KeyError("Context word \"%s\" has not been registered" % 945 context_word) 946 947 for item in comp_items: 948 self._comp_dict[context_word].remove(item) 949 950 def get_completions(self, context_word, prefix): 951 """Get the tab completions given a context word and a prefix. 952 953 Args: 954 context_word: The context word. 955 prefix: The prefix of the incomplete word. 956 957 Returns: 958 (1) None if no registered context matches the context_word. 959 A list of str for the matching completion items. Can be an empty list 960 of a matching context exists, but no completion item matches the 961 prefix. 962 (2) Common prefix of all the words in the first return value. If the 963 first return value is None, this return value will be None, too. If 964 the first return value is not None, i.e., a list, this return value 965 will be a str, which can be an empty str if there is no common 966 prefix among the items of the list. 967 """ 968 969 if context_word not in self._comp_dict: 970 return None, None 971 972 comp_items = self._comp_dict[context_word] 973 comp_items = sorted( 974 [item for item in comp_items if item.startswith(prefix)]) 975 976 return comp_items, self._common_prefix(comp_items) 977 978 def _common_prefix(self, m): 979 """Given a list of str, returns the longest common prefix. 980 981 Args: 982 m: (list of str) A list of strings. 983 984 Returns: 985 (str) The longest common prefix. 986 """ 987 if not m: 988 return "" 989 990 s1 = min(m) 991 s2 = max(m) 992 for i, c in enumerate(s1): 993 if c != s2[i]: 994 return s1[:i] 995 996 return s1 997 998 999class CommandHistory: 1000 """Keeps command history and supports lookup.""" 1001 1002 _HISTORY_FILE_NAME = ".tfdbg_history" 1003 1004 def __init__(self, limit=100, history_file_path=None): 1005 """CommandHistory constructor. 1006 1007 Args: 1008 limit: Maximum number of the most recent commands that this instance 1009 keeps track of, as an int. 1010 history_file_path: (str) Manually specified path to history file. Used in 1011 testing. 1012 """ 1013 1014 self._commands = [] 1015 self._limit = limit 1016 self._history_file_path = ( 1017 history_file_path or self._get_default_history_file_path()) 1018 self._load_history_from_file() 1019 1020 def _load_history_from_file(self): 1021 if os.path.isfile(self._history_file_path): 1022 try: 1023 with open(self._history_file_path, "rt") as history_file: 1024 commands = history_file.readlines() 1025 self._commands = [command.strip() for command in commands 1026 if command.strip()] 1027 1028 # Limit the size of the history file. 1029 if len(self._commands) > self._limit: 1030 self._commands = self._commands[-self._limit:] 1031 with open(self._history_file_path, "wt") as history_file: 1032 for command in self._commands: 1033 history_file.write(command + "\n") 1034 except IOError: 1035 print("WARNING: writing history file failed.") 1036 1037 def _add_command_to_history_file(self, command): 1038 try: 1039 with open(self._history_file_path, "at") as history_file: 1040 history_file.write(command + "\n") 1041 except IOError: 1042 pass 1043 1044 @classmethod 1045 def _get_default_history_file_path(cls): 1046 return os.path.join(os.path.expanduser("~"), cls._HISTORY_FILE_NAME) 1047 1048 def add_command(self, command): 1049 """Add a command to the command history. 1050 1051 Args: 1052 command: The history command, as a str. 1053 1054 Raises: 1055 TypeError: if command is not a str. 1056 """ 1057 1058 if self._commands and command == self._commands[-1]: 1059 # Ignore repeating commands in a row. 1060 return 1061 1062 if not isinstance(command, str): 1063 raise TypeError("Attempt to enter non-str entry to command history") 1064 1065 self._commands.append(command) 1066 1067 if len(self._commands) > self._limit: 1068 self._commands = self._commands[-self._limit:] 1069 1070 self._add_command_to_history_file(command) 1071 1072 def most_recent_n(self, n): 1073 """Look up the n most recent commands. 1074 1075 Args: 1076 n: Number of most recent commands to look up. 1077 1078 Returns: 1079 A list of n most recent commands, or all available most recent commands, 1080 if n exceeds size of the command history, in chronological order. 1081 """ 1082 1083 return self._commands[-n:] 1084 1085 def lookup_prefix(self, prefix, n): 1086 """Look up the n most recent commands that starts with prefix. 1087 1088 Args: 1089 prefix: The prefix to lookup. 1090 n: Number of most recent commands to look up. 1091 1092 Returns: 1093 A list of n most recent commands that have the specified prefix, or all 1094 available most recent commands that have the prefix, if n exceeds the 1095 number of history commands with the prefix. 1096 """ 1097 1098 commands = [cmd for cmd in self._commands if cmd.startswith(prefix)] 1099 1100 return commands[-n:] 1101 1102 # TODO(cais): Lookup by regex. 1103 1104 1105class MenuItem: 1106 """A class for an item in a text-based menu.""" 1107 1108 def __init__(self, caption, content, enabled=True): 1109 """Menu constructor. 1110 1111 TODO(cais): Nested menu is currently not supported. Support it. 1112 1113 Args: 1114 caption: (str) caption of the menu item. 1115 content: Content of the menu item. For a menu item that triggers 1116 a command, for example, content is the command string. 1117 enabled: (bool) whether this menu item is enabled. 1118 """ 1119 1120 self._caption = caption 1121 self._content = content 1122 self._enabled = enabled 1123 1124 @property 1125 def caption(self): 1126 return self._caption 1127 1128 @property 1129 def type(self): 1130 return self._node_type 1131 1132 @property 1133 def content(self): 1134 return self._content 1135 1136 def is_enabled(self): 1137 return self._enabled 1138 1139 def disable(self): 1140 self._enabled = False 1141 1142 def enable(self): 1143 self._enabled = True 1144 1145 1146class Menu: 1147 """A class for text-based menu.""" 1148 1149 def __init__(self, name=None): 1150 """Menu constructor. 1151 1152 Args: 1153 name: (str or None) name of this menu. 1154 """ 1155 1156 self._name = name 1157 self._items = [] 1158 1159 def append(self, item): 1160 """Append an item to the Menu. 1161 1162 Args: 1163 item: (MenuItem) the item to be appended. 1164 """ 1165 self._items.append(item) 1166 1167 def insert(self, index, item): 1168 self._items.insert(index, item) 1169 1170 def num_items(self): 1171 return len(self._items) 1172 1173 def captions(self): 1174 return [item.caption for item in self._items] 1175 1176 def caption_to_item(self, caption): 1177 """Get a MenuItem from the caption. 1178 1179 Args: 1180 caption: (str) The caption to look up. 1181 1182 Returns: 1183 (MenuItem) The first-match menu item with the caption, if any. 1184 1185 Raises: 1186 LookupError: If a menu item with the caption does not exist. 1187 """ 1188 1189 captions = self.captions() 1190 if caption not in captions: 1191 raise LookupError("There is no menu item with the caption \"%s\"" % 1192 caption) 1193 1194 return self._items[captions.index(caption)] 1195 1196 def format_as_single_line(self, 1197 prefix=None, 1198 divider=" | ", 1199 enabled_item_attrs=None, 1200 disabled_item_attrs=None): 1201 """Format the menu as a single-line RichTextLines object. 1202 1203 Args: 1204 prefix: (str) String added to the beginning of the line. 1205 divider: (str) The dividing string between the menu items. 1206 enabled_item_attrs: (list or str) Attributes applied to each enabled 1207 menu item, e.g., ["bold", "underline"]. 1208 disabled_item_attrs: (list or str) Attributes applied to each 1209 disabled menu item, e.g., ["red"]. 1210 1211 Returns: 1212 (RichTextLines) A single-line output representing the menu, with 1213 font_attr_segs marking the individual menu items. 1214 """ 1215 1216 if (enabled_item_attrs is not None and 1217 not isinstance(enabled_item_attrs, list)): 1218 enabled_item_attrs = [enabled_item_attrs] 1219 1220 if (disabled_item_attrs is not None and 1221 not isinstance(disabled_item_attrs, list)): 1222 disabled_item_attrs = [disabled_item_attrs] 1223 1224 menu_line = prefix if prefix is not None else "" 1225 attr_segs = [] 1226 1227 for item in self._items: 1228 menu_line += item.caption 1229 item_name_begin = len(menu_line) - len(item.caption) 1230 1231 if item.is_enabled(): 1232 final_attrs = [item] 1233 if enabled_item_attrs: 1234 final_attrs.extend(enabled_item_attrs) 1235 attr_segs.append((item_name_begin, len(menu_line), final_attrs)) 1236 else: 1237 if disabled_item_attrs: 1238 attr_segs.append( 1239 (item_name_begin, len(menu_line), disabled_item_attrs)) 1240 1241 menu_line += divider 1242 1243 return RichTextLines(menu_line, font_attr_segs={0: attr_segs}) 1244