• 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
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