Skip to content

Instantly share code, notes, and snippets.

@joe-sullivan
Last active March 1, 2024 00:13
Show Gist options
  • Save joe-sullivan/3ad7f000dfeefcd32b6d1f6c7c8c0a1b to your computer and use it in GitHub Desktop.
Save joe-sullivan/3ad7f000dfeefcd32b6d1f6c7c8c0a1b to your computer and use it in GitHub Desktop.
Simple logging decorator for python
#!/usr/bin/env python3
# +------------------------------------------------------------------------------+
# | HOW TO USE: from verbose import Log |
# +=================================== Basics ===================================+
# | <Inputs> |
# | msg : used as the main message to output (this is the only required field) |
# | tag : used to conceptually group messages or add string identifier |
# | |
# | <Functions> |
# | note() : use instead of print() |
# | wrap() : decorator for methods (the message will be shown for each call) |
# | write() : writes messages to a log file (with timestamps) |
# | hack() : display the source of wrapped function as if it were being typed |
# | (this is a novelty wrapper to look like a "movie hacker") |
# | |
# | <Levels (colors)> |
# | DEFAULT : standard (default terminal text color) |
# | INFO : grey, use to show general notes |
# | DEBUG : blue, use to show debugging info |
# | ERROR : red, use to indicate errors |
# | SUCCESS : green, use to indicate success |
# | WARNING : yellow, use to indicate warnings |
# | IMPORTANT : magenta, use to bring attention to but not for errors |
# | HEADER : bblack, use to display function wrappers |
# | |
# +================================== Advanced ==================================+
# | <Inputs> |
# | force : always print the log message (default=False) |
# | pad : character used to pad message to 80 characters (default=None) |
# | fmt : python format string (the following variables can be used) |
# | {tag} - the tag field |
# | {msg} - the msg field |
# | {args} - required function arguments |
# | {kwargs} - optional function arguments |
# | {pad} - character used to pad to 80 bytes |
# | |
# | <Temporarily disable logging> |
# | Log.ENABLED = False |
# | |
# | <Print the first argument to a function> |
# | @Log.wrap('argument1', fmt='{msg}: {args[0]}') |
# | def foo(x): |
# | pass # the value of x will now be logged when foo() is called |
# | |
# +------------------------------------------------------------------------------+
import os, sys
import time
from functools import wraps
# for movie hacker wrapper
import inspect
try: # pip install Pygments
from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
except ModuleNotFoundError:
COLOR = False
else:
COLOR = True
class Log:
ENABLED = True
FILE_PATH = os.path.curdir # set to None to disable file log
FILE_LINE_LIMIT = 1000 # set to 0 for no limit
def __make_color(code):
return '\033[' + str(code) + 'm'
DEFAULT = {'name': 'DEFAULT', 'color': __make_color( 0)} # (terminal text)
INFO = {'name': 'INFO', 'color': __make_color( 1)} # grey
ERROR = {'name': 'ERROR', 'color': __make_color(31)} # red
SUCCESS = {'name': 'SUCCESS', 'color': __make_color(32)} # green
WARN = {'name': 'WARN', 'color': __make_color(33)} # yellow
DEBUG = {'name': 'DEBUG', 'color': __make_color(34)} # blue
IMPORTANT = {'name': 'IMPORTANT', 'color': __make_color(35)} # magenta
HEADER = {'name': 'HEADER', 'color': __make_color('30;1')} # bright black (requires 16 color support)
@staticmethod
def note(msg, tag='>', fmt='{tag} {msg}', args=[], kwargs={}, level=INFO, pad=None, force=False):
def color(msg):
"""Add color to strings"""
if os.name == 'posix':
return level['color'] + msg + Log.DEFAULT['color']
else:
return msg
if Log.ENABLED or force:
formatted_msg = fmt.format(tag=tag, msg=msg, args=args, kwargs=kwargs)
if pad:
formatted_msg += ' '
formatted_msg = formatted_msg.ljust(80, pad)
print(color(formatted_msg))
@staticmethod
def wrap(msg, tag=None, fmt='[{tag}] {msg}: {args}{kwargs}', level=HEADER):
"""Decorator used to display log as '[tag] msg'"""
def print_msg(func):
@wraps(func)
def wrapper(*args, **kwargs):
Log.note(msg, tag=tag or func.__name__, fmt=fmt, args=args, kwargs=kwargs, level=level)
return func(*args, **kwargs)
return wrapper
return print_msg
@staticmethod
def write(msg, tag='*', fmt='[{tag}] {msg}', level=INFO, args=[]):
"""Write log to file"""
if not Log.FILE_PATH: return
timestamp = time.strftime('%Y-%m-%d_%H:%M:%S')
log_msg = ' - '.join([timestamp, level['name'], fmt.format(tag=tag, msg=msg)])
filename = os.path.join(Log.FILE_PATH, 'log')
if not os.path.isfile(filename):
Log.note('creating log file...', tag='+')
with open(filename, 'w'): pass
with open(filename, 'r+') as f:
data = f.readlines()
data.append(log_msg + os.linesep)
f.seek(0)
f.truncate()
f.writelines(data[-Log.FILE_LINE_LIMIT:])
@staticmethod
def hack(func):
def print_type(s, speed=0.0003, force=False):
if not Log.ENABLED and not force: return
if COLOR:
s = highlight(s, PythonLexer(), TerminalFormatter(linenos=True))
else: # green text
print('\033[32m', end='')
line = lambda c: Log.note('', tag='---', fmt='', pad=c)
line('~')
for c in s:
print(c, end='', flush=True)
time.sleep(speed)
line('~')
@wraps(func)
def wrapper(*args, **kwargs):
src = inspect.getsource(func)
# TODO: remove @Log.hack from src
try:
print_type(src)
except KeyboardInterrupt:
pass
finally:
print('\033[0m') # reset terminal color
return func(*args, **kwargs)
return wrapper
if __name__ == '__main__':
# sample usage
Log.ENABLED = True # comment this line to turn off logging
@Log.hack
@Log.wrap('this is a messages for the method')
def hello_world(an_arg, opt=None):
Log.note('hello world')
Log.note('a padded note', pad='*', level=Log.IMPORTANT)
Log.note('this will always be printed', level=Log.DEBUG, force=True)
Log.write('done', tag='verbose test', level=Log.SUCCESS)
hello_world(5, opt=1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment