""" This module decorates methods of Logger class. It's quite harmless, unless you use some weird non-tty stdout/stderr while DEBUG-ing. This hack should be auto-loaded by it's app. If you don't use the app, just import this file to apply the hack. """ import sys import logging import itertools import random # Control sequences (just google VT52 to see the compatibility burden straight from 1975) color_set = "\x1b[{}m{}" color_reset = "\x1b[0m" # useless stuff displayed before applying patches... yes, I felt very bored intro_words = ["magic", "MaGiC", "voodoo", "sorcery", "wizardry", "witchery", "foobar", "rainbow"] intro_adjectives = ["colourful", "evil", "fairy", "random"] intro_colors = [34, 36, 32, 33, 35] decorate_info = { # method names and their desired highlight colors "debug": "1;90", "info": "1;32", "warning": "1;33", "error": "1;91", "critical": "1;95", "exception": "1;95", } if settings.DEBUG: # probably PyCharm or some other IDE with own stream handling # useless foo intro_seq = zip(itertools.cycle(intro_colors), random.choice(intro_words)) print( "{adj} {word}{reset}:".format( adj=random.choice(intro_adjectives).capitalize(), word=str().join(color_set.format(*symbol) for symbol in intro_seq), reset=color_reset, ), end=" ") # Patching handlers: Python logging.StreamHandler targets stderr by default, # therefore Django example config does, therefore our logging config also does, # so PyCharm believes every line of log is error. Well... Fixing it now. # Existing loggers first. patched = list() for logger in logging.Logger.manager.loggerDict.values(): for handler in getattr(logger, "handlers", list()): if isinstance(handler, logging.StreamHandler): if handler.stream == sys.stderr: handler.stream = sys.stdout patched.append(handler) # Future loggers (they should be initially created with the proper stream). original_constructor = logging.StreamHandler.__init__ # must grab a reference outside of the lambda logging.StreamHandler.__init__ = lambda zelf, stream=None: original_constructor(zelf, stream or sys.stdout) # the isatty() stuff below solved some problems in the past, I'm not sure anymore which exactly try: stdout_tty = sys.stdout.isatty() except Exception: stdout_tty = False if not stdout_tty: patched.append(sys.stdout.isatty) sys.stdout.isatty = lambda: True # stdout is now a TTY no matter what # reporting print("applied {n} logging patches.".format(n=len(patched))) logging.getLogger().debug("Logging patches: {}.".format(", ".join(str(e) for e in patched) or "none")) def _apply_color_to_msg(msg, color, extras): return "".join([color_set.format(color, extras), str(msg), color_reset]) def decorate_for_tty(method, color="1;39", extras=""): """ it also knows how to decorate bound methods, but it is not used anymore """ method_is_bound = bool(getattr(method, "__self__", None)) # existing loggers cause bound methods msg_index = 0 if method_is_bound else 1 # message argument is located there def wrapper(*args, **kwargs): args = list(args) # it's an immutable tuple when received args[msg_index] = _apply_color_to_msg(args[msg_index], color, extras) return method(*args, **kwargs) return wrapper for entity in [logging.Logger]: for method_name, color_str in decorate_info.items(): if isinstance(entity, logging.Logger) or entity is logging.Logger: new_method = decorate_for_tty(getattr(entity, method_name), color_str) setattr(entity, method_name, new_method) else: pass # that should be a placeholder object (non-public API), ignoring it