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