1"""Manages logger for dvc repo.""" 2 3from __future__ import unicode_literals 4 5from dvc.exceptions import DvcException 6from dvc.utils.compat import str 7from dvc.progress import progress_aware 8 9import re 10import sys 11import logging 12import traceback 13 14from contextlib import contextmanager 15 16import colorama 17 18 19@progress_aware 20def info(message): 21 """Prints an info message.""" 22 logger.info(message) 23 24 25def debug(message): 26 """Prints a debug message.""" 27 prefix = colorize("Debug", color="blue") 28 29 out = "{prefix}: {message}".format(prefix=prefix, message=message) 30 31 logger.debug(out) 32 33 34@progress_aware 35def warning(message, parse_exception=False): 36 """Prints a warning message.""" 37 prefix = colorize("Warning", color="yellow") 38 39 exception, stack_trace = None, "" 40 if parse_exception: 41 exception, stack_trace = _parse_exc() 42 43 out = "{prefix}: {description}".format( 44 prefix=prefix, description=_description(message, exception) 45 ) 46 47 if stack_trace: 48 out += "\n{stack_trace}".format(stack_trace=stack_trace) 49 50 logger.warning(out) 51 52 53@progress_aware 54def error(message=None): 55 """Prints an error message.""" 56 prefix = colorize("Error", color="red") 57 58 exception, stack_trace = _parse_exc() 59 60 out = ( 61 "{prefix}: {description}" 62 "\n" 63 "{stack_trace}" 64 "\n" 65 "{footer}".format( 66 prefix=prefix, 67 description=_description(message, exception), 68 stack_trace=stack_trace, 69 footer=_footer(), 70 ) 71 ) 72 73 logger.error(out) 74 75 76def box(message, border_color=None): 77 """Prints a message in a box. 78 79 Args: 80 message (unicode): message to print. 81 border_color (unicode): name of a color to outline the box with. 82 """ 83 lines = message.split("\n") 84 max_width = max(_visual_width(line) for line in lines) 85 86 padding_horizontal = 5 87 padding_vertical = 1 88 89 box_size_horizontal = max_width + (padding_horizontal * 2) 90 91 chars = {"corner": "+", "horizontal": "-", "vertical": "|", "empty": " "} 92 93 margin = "{corner}{line}{corner}\n".format( 94 corner=chars["corner"], line=chars["horizontal"] * box_size_horizontal 95 ) 96 97 padding_lines = [ 98 "{border}{space}{border}\n".format( 99 border=colorize(chars["vertical"], color=border_color), 100 space=chars["empty"] * box_size_horizontal, 101 ) 102 * padding_vertical 103 ] 104 105 content_lines = [ 106 "{border}{space}{content}{space}{border}\n".format( 107 border=colorize(chars["vertical"], color=border_color), 108 space=chars["empty"] * padding_horizontal, 109 content=_visual_center(line, max_width), 110 ) 111 for line in lines 112 ] 113 114 box_str = "{margin}{padding}{content}{padding}{margin}".format( 115 margin=colorize(margin, color=border_color), 116 padding="".join(padding_lines), 117 content="".join(content_lines), 118 ) 119 120 logger.info(box_str) 121 122 123def level(): 124 """Returns current log level.""" 125 return logger.getEffectiveLevel() 126 127 128def set_level(level_name): 129 """Sets log level. 130 131 Args: 132 level_name (str): log level name. E.g. info, debug, warning, error, 133 critical. 134 """ 135 if not level_name: 136 return 137 138 levels = { 139 "info": logging.INFO, 140 "debug": logging.DEBUG, 141 "warning": logging.WARNING, 142 "error": logging.ERROR, 143 "critical": logging.CRITICAL, 144 } 145 146 logger.setLevel(levels.get(level_name)) 147 148 149def be_quiet(): 150 """Disables all messages except critical ones.""" 151 logger.setLevel(logging.CRITICAL) 152 153 154def be_verbose(): 155 """Enables all messages.""" 156 logger.setLevel(logging.DEBUG) 157 158 159@contextmanager 160def verbose(): 161 """Enables verbose mode for the context.""" 162 previous_level = level() 163 be_verbose() 164 yield 165 logger.setLevel(previous_level) 166 167 168@contextmanager 169def quiet(): 170 """Enables quiet mode for the context.""" 171 previous_level = level() 172 be_quiet() 173 yield 174 logger.setLevel(previous_level) 175 176 177def is_quiet(): 178 """Returns whether or not all messages except critical ones are 179 disabled. 180 """ 181 return level() == logging.CRITICAL 182 183 184def is_verbose(): 185 """Returns whether or not all messages are enabled.""" 186 return level() == logging.DEBUG 187 188 189def colorize(message, color=None): 190 """Returns a message in a specified color.""" 191 if not color: 192 return message 193 194 colors = { 195 "green": colorama.Fore.GREEN, 196 "yellow": colorama.Fore.YELLOW, 197 "blue": colorama.Fore.BLUE, 198 "red": colorama.Fore.RED, 199 } 200 201 return "{color}{message}{nc}".format( 202 color=colors.get(color, ""), message=message, nc=colorama.Fore.RESET 203 ) 204 205 206def _init_colorama(): 207 colorama.init() 208 209 210def set_default_level(): 211 """Sets default log level.""" 212 logger.setLevel(logging.INFO) 213 214 215def _add_handlers(): 216 formatter = "%(message)s" 217 218 class _LogLevelFilter(logging.Filter): 219 # pylint: disable=too-few-public-methods 220 def filter(self, record): 221 return record.levelno <= logging.WARNING 222 223 sh_out = logging.StreamHandler(sys.stdout) 224 sh_out.setFormatter(logging.Formatter(formatter)) 225 sh_out.setLevel(logging.DEBUG) 226 sh_out.addFilter(_LogLevelFilter()) 227 228 sh_err = logging.StreamHandler(sys.stderr) 229 sh_err.setFormatter(logging.Formatter(formatter)) 230 sh_err.setLevel(logging.ERROR) 231 232 logger.addHandler(sh_out) 233 logger.addHandler(sh_err) 234 235 236def _walk_exc(exc): 237 exc_list = [str(exc)] 238 tb_list = [traceback.format_exc()] 239 240 # NOTE: parsing chained exceptions. See dvc/exceptions.py for more info. 241 while hasattr(exc, "cause") and exc.cause is not None: 242 exc_list.append(str(exc.cause)) 243 if hasattr(exc, "cause_tb") and exc.cause_tb is not None: 244 tb_list.insert(0, str(exc.cause_tb)) 245 exc = exc.cause 246 247 return exc_list, tb_list 248 249 250def _parse_exc(): 251 exc = sys.exc_info()[1] 252 if not exc: 253 return (None, "") 254 255 exc_list, tb_list = _walk_exc(exc) 256 257 exception = ": ".join(exc_list) 258 259 if is_verbose(): 260 stack_trace = "{line}\n{stack_trace}{line}\n".format( 261 line=colorize("-" * 60, color="red"), 262 stack_trace="\n".join(tb_list), 263 ) 264 else: 265 stack_trace = "" 266 267 return (exception, stack_trace) 268 269 270def _description(message, exception): 271 if exception and message: 272 description = "{message} - {exception}" 273 elif exception: 274 description = "{exception}" 275 elif message: 276 description = "{message}" 277 else: 278 raise DvcException( 279 "Unexpected error - either exception or message must be provided" 280 ) 281 282 return description.format(message=message, exception=exception) 283 284 285def _footer(): 286 return "{phrase} Hit us up at {url}, we are always happy to help!".format( 287 phrase=colorize("Having any troubles?", "yellow"), 288 url=colorize("https://dvc.org/support", "blue"), 289 ) 290 291 292def _visual_width(line): 293 """Get the the number of columns required to display a string""" 294 295 return len(re.sub(colorama.ansitowin32.AnsiToWin32.ANSI_CSI_RE, "", line)) 296 297 298def _visual_center(line, width): 299 """Center align string according to it's visual width""" 300 301 spaces = max(width - _visual_width(line), 0) 302 left_padding = int(spaces / 2) 303 right_padding = spaces - left_padding 304 305 return (left_padding * " ") + line + (right_padding * " ") 306 307 308logger = logging.getLogger("dvc") # pylint: disable=invalid-name 309 310set_default_level() 311_add_handlers() 312_init_colorama() 313