# (C) Steve Stagg
# -*- coding: utf-8 -*-
import collections
import io
import functools
import pprint
import sys
import textwrap
import types
import fin.terminal
import fin.color
import fin.duplex
THEMES = {
"plain": {
"OK": lambda C: C.green.bold("OK"),
"FAIL": lambda C: C.red.bold("FAIL"),
"CHILD_PADD": lambda C: "| ",
"LAST_LINE": lambda C: "`- ",
"OUTPUT_PREFIX": lambda C: "+",
"START": lambda C, l: l
},
"aa": {
"OK": lambda C: C.green.bold("OK"),
"FAIL": lambda C: C.red.bold("FAIL"),
"CHILD_PADD": lambda C: C.purple("│ "),
"LAST_LINE": lambda C: C.purple("└ "),
"OUTPUT_PREFIX": lambda C: C.purple("▻"),
"START": lambda C, l: l
},
"mac": {
"OK": lambda C: C.green.bold(u"✓"),
"FAIL": lambda C: C.red.bold(u"✗"),
"CHILD_PADD": lambda C: C.purple(u"│ "),
"LAST_LINE": lambda C: C.purple(u"╰ "),
"OUTPUT_PREFIX": lambda C: C.purple.bold(u"▻"),
"START": lambda C, l: C.purple.bold(l)
},
}
class ColorFakeDict(object):
def __init__(self, color, items):
self.color = color
self.items = items
def __getitem__(self, name):
return self.color.blue.bold(str(self.items[name]))
class LeaveLogException(BaseException):
def __init__(self, msg):
self.exit_msg = msg
def find_open_log(cls):
for stack in cls.LOGS.values():
if len(stack) > 0:
return stack[-1]
raise ValueError("Cannot find a suitable context log to output to")
DEFAULT_STREAM = sys.stderr
if hasattr(sys.stderr, "buffer"):
DEFAULT_STREAM = sys.stderr.buffer.raw
[docs]class Log(object):
"""A logging context manager that provides easy to understand, and useful console output.
Multiple logs may be nested (provided they use the same output stream) and
the output reflects this, allowing for complex processing to be reflected simply to the user
:Example:
>>> from fin.contextlog import Log
>>> def do_stuff():
>>> with Log("Doing stuff"):
>>> pass
:param message: A string to be output when the context manager is entered
:param ok_msg: Defaults to the theme-specific 'OK' message.
The string that is printed if the Context manager exits without error
:param fail_msg: Defaults to the theme-specific 'Fail' message.
The string that is printed if the Context manager detects an exception
:param theme: contextlib has several themes that control how the output is displayed, common ones are 'default', 'aa', and 'mac'
Note, for performance reasons, themes cannot be mixed on the same stream.
:param stream: A file-like object (default is stderr) that the context output is written to.
"""
LOGS = collections.defaultdict(list)
def __init__(self, message,
ok_msg=None,
fail_msg=None,
theme="mac",
stream=DEFAULT_STREAM):
self.message = message
self.theme = theme
self.open = False
self.stream = stream
if isinstance(self.stream, io.TextIOBase):
try:
unicode
except NameError:
self.stream_encoder = lambda x: x
else:
self.stream_encoder = lambda x: unicode(x)
else:
self.stream_encoder = lambda x: x.encode('utf-8', errors='replace')
self.color = fin.color.auto_color(stream)
self.ok_msg = self._theme_item("OK") if ok_msg is None else ok_msg
self.fail_msg = (self._theme_item("FAIL") if fail_msg is None else fail_msg)
self.has_child = False
self.level = None
def _theme_item(self, item, color=None, *args):
if color is None:
color = self.color
return THEMES[self.theme][item](color, *args)
@property
def stack(self):
return self.LOGS[self.stream]
def enter_message(self, suffix=""):
prefix = self._theme_item("CHILD_PADD") * self.level
message = self._theme_item("START", self.color, self.message)
return u"%s%s: %s" % (prefix, message, suffix)
def _write(self, data):
self.stream.write(self.stream_encoder(data))
def child_added(self, child):
if not self.has_child:
self._write("\n")
self.has_child = True
def on_enter(self):
self.open = True
self._write(self.enter_message())
self.stream.flush()
def on_exit(self, failed, msg=None):
if self.has_child:
line = (self._theme_item("CHILD_PADD") * self.level
+ self._theme_item("LAST_LINE"))
self._write(line)
if msg is not None:
self._write(msg+ "\n")
elif failed:
self._write(self.fail_msg + "\n")
else:
self._write(self.ok_msg + "\n")
self.open = False
def exit(self, msg):
raise LeaveLogException(msg)
def __enter__(self):
self.level = len(self.stack)
for item in self.stack:
item.child_added(self)
self.on_enter()
self.stack.append(self)
return self
def __exit__(self, exc_type, exc_value, tb):
rv = None
msg = None
while self.stack and self.stack[-1] != self:
self.stack[-1].__exit__(None, None, None)
self.stack.remove(self)
if exc_type is not None and issubclass(exc_type, LeaveLogException):
rv = True
msg = exc_value.exit_msg
self.on_exit(exc_type is not None, msg)
return rv
@fin.duplex.method(inst_lookup_fun=find_open_log)
[docs] def output(self, msg):
"""
Output `msg` to the stream, but correctly indented
to fit nicely within the current contextlog output.
:param msg: String to be output to the stream
"""
if isinstance(self, type) and issubclass(self, Log):
for stack in self.LOGS.viewvalues():
if len(stack) > 0:
self = stack[-1]
break
else:
raise ValueError("Cannot find a suitable context log to output to")
if not self.open:
raise ValueError("Cannot log output from outside log context.")
self.child_added(None)
for line in msg.splitlines():
line = line.rstrip()
full = "%s%s %s\n" % (
self._theme_item("CHILD_PADD") * (self.level + 1),
self._theme_item("OUTPUT_PREFIX"),
line)
self._write(full)
self.stream.flush()
@fin.duplex.method(inst_lookup_fun=find_open_log)
[docs]class CLog(Log):
"""A logging context manager, similar to :class:`fin.contextlog.Log`, that only produces output if an exception occurs
or log.output()/log.format() is called.
This means that an app/script that uses CLog could run silently if there are no errors,
but show all the context if/when an error does occur
"""
def on_enter(self):
self.open = True
def child_added(self, child):
if self.has_child or isinstance(child, CLog):
return
self._write(self.enter_message("\n"))
self.has_child = True
def on_exit(self, failed, msg=None):
if not failed and not self.has_child:
return
if failed:
for log in self.stack:
log.child_added(None)
self.child_added(None)
return super(CLog, self).on_exit(failed, msg)
def logger(**kwargs):
return functools.partial(Log, **kwargs)