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