loguru/loguru/_colorizer.py

473 lines
14 KiB
Python

import re
from string import Formatter
class Style:
RESET_ALL = 0
BOLD = 1
DIM = 2
ITALIC = 3
UNDERLINE = 4
BLINK = 5
REVERSE = 7
HIDE = 8
STRIKE = 9
NORMAL = 22
class Fore:
BLACK = 30
RED = 31
GREEN = 32
YELLOW = 33
BLUE = 34
MAGENTA = 35
CYAN = 36
WHITE = 37
RESET = 39
LIGHTBLACK_EX = 90
LIGHTRED_EX = 91
LIGHTGREEN_EX = 92
LIGHTYELLOW_EX = 93
LIGHTBLUE_EX = 94
LIGHTMAGENTA_EX = 95
LIGHTCYAN_EX = 96
LIGHTWHITE_EX = 97
class Back:
BLACK = 40
RED = 41
GREEN = 42
YELLOW = 43
BLUE = 44
MAGENTA = 45
CYAN = 46
WHITE = 47
RESET = 49
LIGHTBLACK_EX = 100
LIGHTRED_EX = 101
LIGHTGREEN_EX = 102
LIGHTYELLOW_EX = 103
LIGHTBLUE_EX = 104
LIGHTMAGENTA_EX = 105
LIGHTCYAN_EX = 106
LIGHTWHITE_EX = 107
def ansi_escape(codes):
return {name: "\033[%dm" % code for name, code in codes.items()}
class TokenType:
TEXT = 1
ANSI = 2
LEVEL = 3
CLOSING = 4
class AnsiParser:
_style = ansi_escape(
{
"b": Style.BOLD,
"d": Style.DIM,
"n": Style.NORMAL,
"h": Style.HIDE,
"i": Style.ITALIC,
"l": Style.BLINK,
"s": Style.STRIKE,
"u": Style.UNDERLINE,
"v": Style.REVERSE,
"bold": Style.BOLD,
"dim": Style.DIM,
"normal": Style.NORMAL,
"hide": Style.HIDE,
"italic": Style.ITALIC,
"blink": Style.BLINK,
"strike": Style.STRIKE,
"underline": Style.UNDERLINE,
"reverse": Style.REVERSE,
}
)
_foreground = ansi_escape(
{
"k": Fore.BLACK,
"r": Fore.RED,
"g": Fore.GREEN,
"y": Fore.YELLOW,
"e": Fore.BLUE,
"m": Fore.MAGENTA,
"c": Fore.CYAN,
"w": Fore.WHITE,
"lk": Fore.LIGHTBLACK_EX,
"lr": Fore.LIGHTRED_EX,
"lg": Fore.LIGHTGREEN_EX,
"ly": Fore.LIGHTYELLOW_EX,
"le": Fore.LIGHTBLUE_EX,
"lm": Fore.LIGHTMAGENTA_EX,
"lc": Fore.LIGHTCYAN_EX,
"lw": Fore.LIGHTWHITE_EX,
"black": Fore.BLACK,
"red": Fore.RED,
"green": Fore.GREEN,
"yellow": Fore.YELLOW,
"blue": Fore.BLUE,
"magenta": Fore.MAGENTA,
"cyan": Fore.CYAN,
"white": Fore.WHITE,
"light-black": Fore.LIGHTBLACK_EX,
"light-red": Fore.LIGHTRED_EX,
"light-green": Fore.LIGHTGREEN_EX,
"light-yellow": Fore.LIGHTYELLOW_EX,
"light-blue": Fore.LIGHTBLUE_EX,
"light-magenta": Fore.LIGHTMAGENTA_EX,
"light-cyan": Fore.LIGHTCYAN_EX,
"light-white": Fore.LIGHTWHITE_EX,
}
)
_background = ansi_escape(
{
"K": Back.BLACK,
"R": Back.RED,
"G": Back.GREEN,
"Y": Back.YELLOW,
"E": Back.BLUE,
"M": Back.MAGENTA,
"C": Back.CYAN,
"W": Back.WHITE,
"LK": Back.LIGHTBLACK_EX,
"LR": Back.LIGHTRED_EX,
"LG": Back.LIGHTGREEN_EX,
"LY": Back.LIGHTYELLOW_EX,
"LE": Back.LIGHTBLUE_EX,
"LM": Back.LIGHTMAGENTA_EX,
"LC": Back.LIGHTCYAN_EX,
"LW": Back.LIGHTWHITE_EX,
"BLACK": Back.BLACK,
"RED": Back.RED,
"GREEN": Back.GREEN,
"YELLOW": Back.YELLOW,
"BLUE": Back.BLUE,
"MAGENTA": Back.MAGENTA,
"CYAN": Back.CYAN,
"WHITE": Back.WHITE,
"LIGHT-BLACK": Back.LIGHTBLACK_EX,
"LIGHT-RED": Back.LIGHTRED_EX,
"LIGHT-GREEN": Back.LIGHTGREEN_EX,
"LIGHT-YELLOW": Back.LIGHTYELLOW_EX,
"LIGHT-BLUE": Back.LIGHTBLUE_EX,
"LIGHT-MAGENTA": Back.LIGHTMAGENTA_EX,
"LIGHT-CYAN": Back.LIGHTCYAN_EX,
"LIGHT-WHITE": Back.LIGHTWHITE_EX,
}
)
_regex_tag = re.compile(r"\\?</?((?:[fb]g\s)?[^<>\s]*)>")
def __init__(self):
self._tokens = []
self._tags = []
self._color_tokens = []
@staticmethod
def strip(tokens):
output = ""
for type_, value in tokens:
if type_ == TokenType.TEXT:
output += value
return output
@staticmethod
def colorize(tokens, ansi_level):
output = ""
for type_, value in tokens:
if type_ == TokenType.LEVEL:
if ansi_level is None:
raise ValueError(
"The '<level>' color tag is not allowed in this context, "
"it has not yet been associated to any color value."
)
value = ansi_level
output += value
return output
@staticmethod
def wrap(tokens, *, ansi_level, color_tokens):
output = ""
for type_, value in tokens:
if type_ == TokenType.LEVEL:
value = ansi_level
output += value
if type_ == TokenType.CLOSING:
for subtype, subvalue in color_tokens:
if subtype == TokenType.LEVEL:
subvalue = ansi_level
output += subvalue
return output
def feed(self, text, *, raw=False):
if raw:
self._tokens.append((TokenType.TEXT, text))
return
position = 0
for match in self._regex_tag.finditer(text):
markup, tag = match.group(0), match.group(1)
self._tokens.append((TokenType.TEXT, text[position : match.start()]))
position = match.end()
if markup[0] == "\\":
self._tokens.append((TokenType.TEXT, markup[1:]))
continue
if markup[1] == "/":
if self._tags and (tag == "" or tag == self._tags[-1]):
self._tags.pop()
self._color_tokens.pop()
self._tokens.append((TokenType.CLOSING, "\033[0m"))
self._tokens.extend(self._color_tokens)
continue
if tag in self._tags:
raise ValueError('Closing tag "%s" violates nesting rules' % markup)
raise ValueError('Closing tag "%s" has no corresponding opening tag' % markup)
if tag in {"lvl", "level"}:
token = (TokenType.LEVEL, None)
else:
ansi = self._get_ansicode(tag)
if ansi is None:
raise ValueError(
'Tag "%s" does not correspond to any known color directive, '
"make sure you did not misspelled it (or prepend '\\' to escape it)"
% markup
)
token = (TokenType.ANSI, ansi)
self._tags.append(tag)
self._color_tokens.append(token)
self._tokens.append(token)
self._tokens.append((TokenType.TEXT, text[position:]))
def done(self, *, strict=True):
if strict and self._tags:
faulty_tag = self._tags.pop(0)
raise ValueError('Opening tag "<%s>" has no corresponding closing tag' % faulty_tag)
return self._tokens
def current_color_tokens(self):
return list(self._color_tokens)
def _get_ansicode(self, tag):
style = self._style
foreground = self._foreground
background = self._background
# Substitute on a direct match.
if tag in style:
return style[tag]
if tag in foreground:
return foreground[tag]
if tag in background:
return background[tag]
# An alternative syntax for setting the color (e.g. <fg red>, <bg red>).
if tag.startswith("fg ") or tag.startswith("bg "):
st, color = tag[:2], tag[3:]
code = "38" if st == "fg" else "48"
if st == "fg" and color.lower() in foreground:
return foreground[color.lower()]
if st == "bg" and color.upper() in background:
return background[color.upper()]
if color.isdigit() and int(color) <= 255:
return "\033[%s;5;%sm" % (code, color)
if re.match(r"#(?:[a-fA-F0-9]{3}){1,2}$", color):
hex_color = color[1:]
if len(hex_color) == 3:
hex_color *= 2
rgb = tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
return "\033[%s;2;%s;%s;%sm" % ((code,) + rgb)
if color.count(",") == 2:
colors = tuple(color.split(","))
if all(x.isdigit() and int(x) <= 255 for x in colors):
return "\033[%s;2;%s;%s;%sm" % ((code,) + colors)
return None
class ColoringMessage(str):
__fields__ = ("_messages",)
def __format__(self, spec):
return next(self._messages).__format__(spec)
class ColoredMessage:
def __init__(self, tokens):
self.tokens = tokens
self.stripped = AnsiParser.strip(tokens)
def colorize(self, ansi_level):
return AnsiParser.colorize(self.tokens, ansi_level)
class ColoredFormat:
def __init__(self, tokens, messages_color_tokens):
self._tokens = tokens
self._messages_color_tokens = messages_color_tokens
def strip(self):
return AnsiParser.strip(self._tokens)
def colorize(self, ansi_level):
return AnsiParser.colorize(self._tokens, ansi_level)
def make_coloring_message(self, message, *, ansi_level, colored_message):
messages = [
(
message
if color_tokens is None
else AnsiParser.wrap(
colored_message.tokens, ansi_level=ansi_level, color_tokens=color_tokens
)
)
for color_tokens in self._messages_color_tokens
]
coloring = ColoringMessage(message)
coloring._messages = iter(messages)
return coloring
class Colorizer:
@staticmethod
def prepare_format(string):
tokens, messages_color_tokens = Colorizer._parse_without_formatting(string)
return ColoredFormat(tokens, messages_color_tokens)
@staticmethod
def prepare_message(string, args=(), kwargs={}): # noqa: B006
tokens = Colorizer._parse_with_formatting(string, args, kwargs)
return ColoredMessage(tokens)
@staticmethod
def prepare_simple_message(string):
parser = AnsiParser()
parser.feed(string)
tokens = parser.done()
return ColoredMessage(tokens)
@staticmethod
def ansify(text):
parser = AnsiParser()
parser.feed(text.strip())
tokens = parser.done(strict=False)
return AnsiParser.colorize(tokens, None)
@staticmethod
def _parse_with_formatting(
string, args, kwargs, *, recursion_depth=2, auto_arg_index=0, recursive=False
):
# This function re-implements Formatter._vformat()
if recursion_depth < 0:
raise ValueError("Max string recursion exceeded")
formatter = Formatter()
parser = AnsiParser()
for literal_text, field_name, format_spec, conversion in formatter.parse(string):
parser.feed(literal_text, raw=recursive)
if field_name is not None:
if field_name == "":
if auto_arg_index is False:
raise ValueError(
"cannot switch from manual field "
"specification to automatic field "
"numbering"
)
field_name = str(auto_arg_index)
auto_arg_index += 1
elif field_name.isdigit():
if auto_arg_index:
raise ValueError(
"cannot switch from manual field "
"specification to automatic field "
"numbering"
)
auto_arg_index = False
obj, _ = formatter.get_field(field_name, args, kwargs)
obj = formatter.convert_field(obj, conversion)
format_spec, auto_arg_index = Colorizer._parse_with_formatting(
format_spec,
args,
kwargs,
recursion_depth=recursion_depth - 1,
auto_arg_index=auto_arg_index,
recursive=True,
)
formatted = formatter.format_field(obj, format_spec)
parser.feed(formatted, raw=True)
tokens = parser.done()
if recursive:
return AnsiParser.strip(tokens), auto_arg_index
return tokens
@staticmethod
def _parse_without_formatting(string, *, recursion_depth=2, recursive=False):
if recursion_depth < 0:
raise ValueError("Max string recursion exceeded")
formatter = Formatter()
parser = AnsiParser()
messages_color_tokens = []
for literal_text, field_name, format_spec, conversion in formatter.parse(string):
if literal_text and literal_text[-1] in "{}":
literal_text += literal_text[-1]
parser.feed(literal_text, raw=recursive)
if field_name is not None:
if field_name == "message":
if recursive:
messages_color_tokens.append(None)
else:
color_tokens = parser.current_color_tokens()
messages_color_tokens.append(color_tokens)
field = "{%s" % field_name
if conversion:
field += "!%s" % conversion
if format_spec:
field += ":%s" % format_spec
field += "}"
parser.feed(field, raw=True)
_, color_tokens = Colorizer._parse_without_formatting(
format_spec, recursion_depth=recursion_depth - 1, recursive=True
)
messages_color_tokens.extend(color_tokens)
return parser.done(), messages_color_tokens