[Autoscaler] CLI Logger docs (#9690)

Co-authored-by: Richard Liaw <rliaw@berkeley.edu>
This commit is contained in:
Maksim Smolin
2020-07-24 16:59:25 -07:00
committed by GitHub
parent 56d2cf6479
commit d364afbd31
+263 -18
View File
@@ -1,3 +1,13 @@
"""Logger implementing the Command Line Interface.
A replacement for the standard Python `logging` API
designed for implementing a better CLI UX for the cluster launcher.
Supports color, bold text, italics, underlines, etc.
(depending on TTY features)
as well as indentation and other structured output.
"""
import sys
import click
@@ -8,18 +18,53 @@ import colorful as cf
colorama.init()
def _strip_codes(msg):
return msg # todo
# we could bold "{}" strings automatically but do we want that?
# todo:
def _format_msg(msg,
*args,
_tags=None,
_numbered=None,
_no_format=None,
**kwargs):
"""Formats a message for printing.
Renders `msg` using the built-in `str.format` and the passed-in
`*args` and `**kwargs`.
Args:
*args (Any): `.format` arguments for `msg`.
_tags (Dict[str, Any]):
key-value pairs to display at the end of
the message in square brackets.
If a tag is set to `True`, it is printed without the value,
the presence of the tag treated as a "flag".
E.g. `_format_msg("hello", _tags=dict(from=mom, signed=True))`
`hello [from=Mom, signed]`
_numbered (Tuple[str, int, int]):
`(brackets, i, n)`
The `brackets` string is composed of two "bracket" characters,
`i` is the index, `n` is the total.
The string `{i}/{n}` surrounded by the "brackets" is
prepended to the message.
This is used to number steps in a procedure, with different
brackets specifying different major tasks.
E.g. `_format_msg("hello", _numbered=("[]", 0, 5))`
`[0/5] hello`
_no_format (bool):
If `_no_format` is `True`,
`.format` will not be called on the message.
Useful if the output is user-provided or may otherwise
contain an unexpected formatting string (e.g. "{}").
Returns:
The formatted message.
"""
if isinstance(msg, str) or isinstance(msg, ColorfulString):
tags_str = ""
if _tags is not None:
@@ -59,6 +104,41 @@ def _format_msg(msg,
class _CliLogger():
"""Singleton class for CLI logging.
Attributes:
strip (bool):
If `strip` is `True`, all TTY control sequences will be
removed from the output.
old_style (bool):
If `old_style` is `True`, the old logging calls are used instead
of the new CLI UX. This is enabled by default and remains for
backwards compatibility.
color_mode (str):
Can be "true", "false", or "auto".
Determines the value of `strip` using a human-readable string
that can be set from command line arguments.
Also affects the `colorful` settings.
If `color_mode` is "auto", `strip` is set to `not stdout.isatty()`
indent_level (int):
The current indentation level.
All messages will be indented by prepending `" " * indent_level`
vebosity (int):
Output verbosity.
Low verbosity will disable `verbose` and `very_verbose` messages.
dump_command_output (bool):
Determines whether the old behavior of dumping command output
to console will be used, or the new behavior of redirecting to
a file.
! Currently unused.
"""
def __init__(self):
self.strip = False
self.old_style = True
@@ -68,31 +148,53 @@ class _CliLogger():
self.dump_command_output = False
self.info = {}
# store whatever colorful has detected for future use if
# the color ouput is toggled (colorful detects # of supported colors,
# so it has some non-trivial logic to determine this)
self._autodetected_cf_colormode = cf.colormode
def detect_colors(self):
"""Update color output settings.
Parse the `color_mode` string and optionally disable or force-enable
color output
(8-color ANSI if no terminal detected to be safe) in colorful.
"""
if self.color_mode == "true":
self.strip = False
if self._autodetected_cf_colormode != cf.NO_COLORS:
cf.colormode = self._autodetected_cf_colormode
else:
cf.colormode = cf.ANSI_8_COLORS
return
if self.color_mode == "false":
self.strip = True
cf.disable()
return
if self.color_mode == "auto":
self.strip = sys.stdout.isatty()
self.strip = not sys.stdout.isatty()
# colorful autodetects tty settings
return
raise ValueError("Invalid log color setting: " + self.color_mode)
def newline(self):
"""Print a line feed.
"""
self._print("")
def _print(self, msg, linefeed=True):
"""Proxy for printing messages.
Args:
msg (str): Message to print.
linefeed (bool):
If `linefeed` is `False` no linefeed is printed at the
end of the message.
"""
if self.old_style:
return
if self.strip:
msg = _strip_codes(msg)
rendered_message = " " * self.indent_level + msg
if not linefeed:
@@ -102,7 +204,9 @@ class _CliLogger():
print(rendered_message)
def indented(self, cls=False):
def indented(self):
"""Context manager that starts an indented block of output.
"""
cli_logger = self
class IndentedContextManager():
@@ -112,20 +216,38 @@ class _CliLogger():
def __exit__(self, type, value, tb):
cli_logger.indent_level -= 1
if cls:
# fixme: this does not work :()
return IndentedContextManager
return IndentedContextManager()
def timed(self, msg, *args, **kwargs):
"""
TODO: Unimplemented special type of output grouping that displays
a timer for its execution. The method was not removed so we
can mark places where this might be useful in case we ever
implement this.
For arguments, see `_format_msg`.
"""
return self.group(msg, *args, **kwargs)
def group(self, msg, *args, **kwargs):
"""Print a group title in a special color and start an indented block.
For arguments, see `_format_msg`.
"""
self._print(_format_msg(cf.cornflowerBlue(msg), *args, **kwargs))
return self.indented()
def verbatim_error_ctx(self, msg, *args, **kwargs):
"""Context manager for printing multi-line error messages.
Displays a start sequence "!!! {optional message}"
and a matching end sequence "!!!".
The string "!!!" can be used as a "tombstone" for searching.
For arguments, see `_format_msg`.
"""
cli_logger = self
class VerbatimErorContextManager():
@@ -138,40 +260,87 @@ class _CliLogger():
return VerbatimErorContextManager()
def labeled_value(self, key, msg, *args, **kwargs):
"""Displays a key-value pair with special formatting.
Args:
key (str): Label that is prepended to the message.
For other arguments, see `_format_msg`.
"""
self._print(
cf.cyan(key) + ": " + _format_msg(cf.bold(msg), *args, **kwargs))
def verbose(self, msg, *args, **kwargs):
"""Prints a message if verbosity is not 0.
For arguments, see `_format_msg`.
"""
if self.verbosity > 0:
self.print(msg, *args, **kwargs)
def verbose_error(self, msg, *args, **kwargs):
"""Logs an error if verbosity is not 0.
For arguments, see `_format_msg`.
"""
if self.verbosity > 0:
self.error(msg, *args, **kwargs)
def very_verbose(self, msg, *args, **kwargs):
"""Prints if verbosity is > 1.
For arguments, see `_format_msg`.
"""
if self.verbosity > 1:
self.print(msg, *args, **kwargs)
def success(self, msg, *args, **kwargs):
"""Prints a formatted success message.
For arguments, see `_format_msg`.
"""
self._print(_format_msg(cf.green(msg), *args, **kwargs))
def warning(self, msg, *args, **kwargs):
"""Prints a formatted warning message.
For arguments, see `_format_msg`.
"""
self._print(_format_msg(cf.yellow(msg), *args, **kwargs))
def error(self, msg, *args, **kwargs):
"""Prints a formatted error message.
For arguments, see `_format_msg`.
"""
self._print(_format_msg(cf.red(msg), *args, **kwargs))
def print(self, msg, *args, **kwargs):
"""Prints a message.
For arguments, see `_format_msg`.
"""
self._print(_format_msg(msg, *args, **kwargs))
def abort(self, msg=None, *args, **kwargs):
"""Prints an error and aborts execution.
Print an error and throw an exception to terminate the program
(the exception will not print a message).
"""
if msg is not None:
self.error(msg, *args, **kwargs)
raise SilentClickException("Exiting due to cli_logger.abort()")
def doassert(self, val, msg, *args, **kwargs):
"""Handle assertion without throwing a scary exception.
Args:
val (bool): Value to check.
For other arguments, see `_format_msg`.
"""
if self.old_style:
return
@@ -179,34 +348,105 @@ class _CliLogger():
self.abort(msg, *args, **kwargs)
def old_debug(self, logger, msg, *args, **kwargs):
"""Old debug logging proxy.
Pass along an old debug log iff new logging is disabled.
Supports the new formatting features.
Args:
logger (logging.Logger):
Logger to use if old logging behavior is selected.
For other arguments, see `_format_msg`.
"""
if self.old_style:
logger.debug(_format_msg(msg, *args, **kwargs))
return
def old_info(self, logger, msg, *args, **kwargs):
"""Old info logging proxy.
Pass along an old info log iff new logging is disabled.
Supports the new formatting features.
Args:
logger (logging.Logger):
Logger to use if old logging behavior is selected.
For other arguments, see `_format_msg`.
"""
if self.old_style:
logger.info(_format_msg(msg, *args, **kwargs))
return
def old_warning(self, logger, msg, *args, **kwargs):
"""Old warning logging proxy.
Pass along an old warning log iff new logging is disabled.
Supports the new formatting features.
Args:
logger (logging.Logger):
Logger to use if old logging behavior is selected.
For other arguments, see `_format_msg`.
"""
if self.old_style:
logger.warning(_format_msg(msg, *args, **kwargs))
return
def old_error(self, logger, msg, *args, **kwargs):
"""Old error logging proxy.
Pass along an old error log iff new logging is disabled.
Supports the new formatting features.
Args:
logger (logging.Logger):
Logger to use if old logging behavior is selected.
For other arguments, see `_format_msg`.
"""
if self.old_style:
logger.error(_format_msg(msg, *args, **kwargs))
return
def old_exception(self, logger, msg, *args, **kwargs):
"""Old exception logging proxy.
Pass along an old exception log iff new logging is disabled.
Supports the new formatting features.
Args:
logger (logging.Logger):
Logger to use if old logging behavior is selected.
For other arguments, see `_format_msg`.
"""
if self.old_style:
logger.exception(_format_msg(msg, *args, **kwargs))
return
def render_list(self, xs, separator=cf.reset(", ")):
"""Render a list of bolded values using a non-bolded separator.
"""
return separator.join([str(cf.bold(x)) for x in xs])
def confirm(self, yes, msg, *args, _abort=False, _default=False, **kwargs):
"""Display a confirmation dialog.
Valid answers are "y/yes/true/1" and "n/no/false/0".
Args:
yes (bool): If `yes` is `True` the dialog will default to "yes"
and continue without waiting for user input.
_abort (bool):
If `_abort` is `True`,
"no" means aborting the program.
_default (bool):
The default action to take if the user just presses enter
with no input.
"""
if self.old_style:
return
@@ -274,6 +514,10 @@ class _CliLogger():
return res
def old_confirm(self, msg, yes):
"""Old confirm dialog proxy.
Let `click` display a confirm dialog iff new logging is disabled.
"""
if not self.old_style:
return
@@ -281,7 +525,8 @@ class _CliLogger():
class SilentClickException(click.ClickException):
"""
"""`ClickException` that does not print a message.
Some of our tooling relies on catching ClickException in particular.
However the default prints a message, which is undesirable since we expect