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