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