• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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