Added a pretty-printing debug logger
A new feature has been added to the codebase, which is a pretty-printing debug logger. This logger colorizes printed data based on datatype and handles nested data structures such as Arrays and Dictionaries. It also prefixes logs with the callsite's source file for easy tracking. The logger supports opt-in to pretty printing via duck-typing by implementing a `to_printable()` method on the object.
This commit is contained in:
parent
3fa6393643
commit
052dc00039
3 changed files with 501 additions and 0 deletions
484
addons/log/log.gd
Normal file
484
addons/log/log.gd
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
@tool
|
||||
extends Object
|
||||
class_name Log
|
||||
|
||||
## helpers ####################################
|
||||
|
||||
static func assoc(opts: Dictionary, key: String, val):
|
||||
var _opts = opts.duplicate(true)
|
||||
_opts[key] = val
|
||||
return _opts
|
||||
|
||||
## config ####################################
|
||||
|
||||
static var config = {
|
||||
max_array_size=20,
|
||||
dictionary_skip_keys=["layer_0/tile_data"],
|
||||
}
|
||||
|
||||
static func get_max_array_size():
|
||||
return Log.config.get("max_array_size", 20)
|
||||
|
||||
static func get_dictionary_skip_keys():
|
||||
return Log.config.get("dictionary_skip_keys", [])
|
||||
|
||||
static func set_color_scheme(scheme):
|
||||
Log.config["color_scheme"] = scheme
|
||||
|
||||
static func get_config_color_scheme():
|
||||
return Log.config.get("color_scheme", {})
|
||||
|
||||
## colors ###########################################################################
|
||||
|
||||
# terminal safe colors:
|
||||
# - black
|
||||
# - red
|
||||
# - green
|
||||
# - yellow
|
||||
# - blue
|
||||
# - magenta
|
||||
# - pink
|
||||
# - purple
|
||||
# - cyan
|
||||
# - white
|
||||
# - orange
|
||||
# - gray
|
||||
|
||||
static var COLORS_TERMINAL_SAFE = {
|
||||
"SRC": "cyan",
|
||||
"ADDONS": "red",
|
||||
"TEST": "green",
|
||||
",": "red",
|
||||
"(": "red",
|
||||
")": "red",
|
||||
"[": "red",
|
||||
"]": "red",
|
||||
"{": "red",
|
||||
"}": "red",
|
||||
"&": "orange",
|
||||
"^": "orange",
|
||||
"dict_key": "magenta",
|
||||
"vector_value": "green",
|
||||
"class_name": "magenta",
|
||||
TYPE_NIL: "pink",
|
||||
TYPE_BOOL: "pink",
|
||||
TYPE_INT: "green",
|
||||
TYPE_FLOAT: "green",
|
||||
TYPE_STRING: "pink",
|
||||
TYPE_VECTOR2: "green",
|
||||
TYPE_VECTOR2I: "green",
|
||||
TYPE_RECT2: "green",
|
||||
TYPE_RECT2I: "green",
|
||||
TYPE_VECTOR3: "green",
|
||||
TYPE_VECTOR3I: "green",
|
||||
TYPE_TRANSFORM2D: "pink",
|
||||
TYPE_VECTOR4: "green",
|
||||
TYPE_VECTOR4I: "green",
|
||||
TYPE_PLANE: "pink",
|
||||
TYPE_QUATERNION: "pink",
|
||||
TYPE_AABB: "pink",
|
||||
TYPE_BASIS: "pink",
|
||||
TYPE_TRANSFORM3D: "pink",
|
||||
TYPE_PROJECTION: "pink",
|
||||
TYPE_COLOR: "pink",
|
||||
TYPE_STRING_NAME: "pink",
|
||||
TYPE_NODE_PATH: "pink",
|
||||
TYPE_RID: "pink",
|
||||
TYPE_OBJECT: "pink",
|
||||
TYPE_CALLABLE: "pink",
|
||||
TYPE_SIGNAL: "pink",
|
||||
TYPE_DICTIONARY: "pink",
|
||||
TYPE_ARRAY: "pink",
|
||||
TYPE_PACKED_BYTE_ARRAY: "pink",
|
||||
TYPE_PACKED_INT32_ARRAY: "pink",
|
||||
TYPE_PACKED_INT64_ARRAY: "pink",
|
||||
TYPE_PACKED_FLOAT32_ARRAY: "pink",
|
||||
TYPE_PACKED_FLOAT64_ARRAY: "pink",
|
||||
TYPE_PACKED_STRING_ARRAY: "pink",
|
||||
TYPE_PACKED_VECTOR2_ARRAY: "pink",
|
||||
TYPE_PACKED_VECTOR3_ARRAY: "pink",
|
||||
TYPE_PACKED_COLOR_ARRAY: "pink",
|
||||
TYPE_MAX: "pink",
|
||||
}
|
||||
|
||||
static var COLORS_PRETTY_V1 = {
|
||||
"SRC": "aquamarine",
|
||||
"ADDONS": "peru",
|
||||
"TEST": "green_yellow",
|
||||
",": "crimson",
|
||||
"(": "crimson",
|
||||
")": "crimson",
|
||||
"[": "crimson",
|
||||
"]": "crimson",
|
||||
"{": "crimson",
|
||||
"}": "crimson",
|
||||
"&": "coral",
|
||||
"^": "coral",
|
||||
"dict_key": "cadet_blue",
|
||||
"vector_value": "cornflower_blue",
|
||||
"class_name": "cadet_blue",
|
||||
TYPE_NIL: "coral",
|
||||
TYPE_BOOL: "pink",
|
||||
TYPE_INT: "cornflower_blue",
|
||||
TYPE_FLOAT: "cornflower_blue",
|
||||
TYPE_STRING: "dark_gray",
|
||||
TYPE_VECTOR2: "cornflower_blue",
|
||||
TYPE_VECTOR2I: "cornflower_blue",
|
||||
TYPE_RECT2: "cornflower_blue",
|
||||
TYPE_RECT2I: "cornflower_blue",
|
||||
TYPE_VECTOR3: "cornflower_blue",
|
||||
TYPE_VECTOR3I: "cornflower_blue",
|
||||
TYPE_TRANSFORM2D: "pink",
|
||||
TYPE_VECTOR4: "cornflower_blue",
|
||||
TYPE_VECTOR4I: "cornflower_blue",
|
||||
TYPE_PLANE: "pink",
|
||||
TYPE_QUATERNION: "pink",
|
||||
TYPE_AABB: "pink",
|
||||
TYPE_BASIS: "pink",
|
||||
TYPE_TRANSFORM3D: "pink",
|
||||
TYPE_PROJECTION: "pink",
|
||||
TYPE_COLOR: "pink",
|
||||
TYPE_STRING_NAME: "pink",
|
||||
TYPE_NODE_PATH: "pink",
|
||||
TYPE_RID: "pink",
|
||||
TYPE_OBJECT: "pink",
|
||||
TYPE_CALLABLE: "pink",
|
||||
TYPE_SIGNAL: "pink",
|
||||
TYPE_DICTIONARY: "pink",
|
||||
TYPE_ARRAY: "pink",
|
||||
TYPE_PACKED_BYTE_ARRAY: "pink",
|
||||
TYPE_PACKED_INT32_ARRAY: "pink",
|
||||
TYPE_PACKED_INT64_ARRAY: "pink",
|
||||
TYPE_PACKED_FLOAT32_ARRAY: "pink",
|
||||
TYPE_PACKED_FLOAT64_ARRAY: "pink",
|
||||
TYPE_PACKED_STRING_ARRAY: "pink",
|
||||
TYPE_PACKED_VECTOR2_ARRAY: "pink",
|
||||
TYPE_PACKED_VECTOR3_ARRAY: "pink",
|
||||
TYPE_PACKED_COLOR_ARRAY: "pink",
|
||||
TYPE_MAX: "pink",
|
||||
}
|
||||
|
||||
## set color scheme ####################################
|
||||
|
||||
static func set_colors_termsafe():
|
||||
set_color_scheme(Log.COLORS_TERMINAL_SAFE)
|
||||
|
||||
static func set_colors_pretty():
|
||||
set_color_scheme(Log.COLORS_PRETTY_V1)
|
||||
|
||||
static func color_scheme(opts={}):
|
||||
var scheme = opts.get("color_scheme", {})
|
||||
# fill in any missing vals with the set scheme, then the term-safe fallbacks
|
||||
scheme.merge(Log.get_config_color_scheme())
|
||||
scheme.merge(Log.COLORS_TERMINAL_SAFE)
|
||||
return scheme
|
||||
|
||||
static func color_wrap(s, opts={}):
|
||||
var use_color = opts.get("use_color", true)
|
||||
# don't rebuild the color scheme every time
|
||||
var colors = opts.get("built_color_scheme", color_scheme(opts))
|
||||
|
||||
if use_color:
|
||||
var color = opts.get("color")
|
||||
if not color:
|
||||
var s_type = opts.get("typeof", typeof(s))
|
||||
if s_type is String:
|
||||
# type overwrites
|
||||
color = colors.get(s_type)
|
||||
elif s_type is int and s_type == TYPE_STRING:
|
||||
# specific strings/punctuation
|
||||
var s_trimmed = s.strip_edges()
|
||||
if s_trimmed in colors:
|
||||
color = colors.get(s_trimmed)
|
||||
else:
|
||||
# fallback string color
|
||||
color = colors.get(s_type)
|
||||
else:
|
||||
# all other types
|
||||
color = colors.get(s_type)
|
||||
|
||||
if color == null:
|
||||
print("Log.gd could not determine color for object: %s type: (%s)" % [str(s), typeof(s)])
|
||||
|
||||
return "[color=%s]%s[/color]" % [color, s]
|
||||
else:
|
||||
return s
|
||||
|
||||
## overwrites ###########################################################################
|
||||
|
||||
static var log_overwrites = {
|
||||
"Vector2": func(msg, opts):
|
||||
if opts.get("use_color", true):
|
||||
return '%s%s%s%s%s' % [
|
||||
Log.color_wrap("(", opts),
|
||||
Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")),
|
||||
Log.color_wrap(",", opts),
|
||||
Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")),
|
||||
Log.color_wrap(")", opts),
|
||||
]
|
||||
else:
|
||||
return '(%s,%s)' % [msg.x, msg.y],
|
||||
}
|
||||
|
||||
static func register_overwrite(key, handler):
|
||||
# TODO warning on key exists?
|
||||
# support multiple handlers?
|
||||
# return success/fail?
|
||||
# validate the key/handler somehow?
|
||||
log_overwrites[key] = handler
|
||||
|
||||
## to_pretty ###########################################################################
|
||||
|
||||
# returns the passed object as a decorated string
|
||||
static func to_pretty(msg, opts={}):
|
||||
var newlines = opts.get("newlines", false)
|
||||
var use_color = opts.get("use_color", true)
|
||||
var indent_level = opts.get("indent_level", 0)
|
||||
if not "indent_level" in opts:
|
||||
opts["indent_level"] = indent_level
|
||||
|
||||
var color_scheme = opts.get("built_color_scheme", color_scheme(opts))
|
||||
if not "built_color_scheme" in opts:
|
||||
opts["built_color_scheme"] = color_scheme
|
||||
|
||||
if not is_instance_valid(msg) and typeof(msg) == TYPE_OBJECT:
|
||||
return str("invalid instance: ", msg)
|
||||
|
||||
if msg == null:
|
||||
return Log.color_wrap(msg, opts)
|
||||
|
||||
if msg is Object and msg.get_class() in log_overwrites:
|
||||
return log_overwrites.get(msg.get_class()).call(msg, opts)
|
||||
elif typeof(msg) in log_overwrites:
|
||||
return log_overwrites.get(typeof(msg)).call(msg, opts)
|
||||
|
||||
# objects
|
||||
if msg is Object and msg.has_method("to_pretty"):
|
||||
return Log.to_pretty(msg.to_pretty(), opts)
|
||||
if msg is Object and msg.has_method("data"):
|
||||
return Log.to_pretty(msg.data(), opts)
|
||||
if msg is Object and msg.has_method("to_printable"):
|
||||
return Log.to_pretty(msg.to_printable(), opts)
|
||||
|
||||
# arrays
|
||||
if msg is Array or msg is PackedStringArray:
|
||||
if len(msg) > Log.get_max_array_size():
|
||||
pr("[DEBUG]: truncating large array. total:", len(msg))
|
||||
msg = msg.slice(0, Log.get_max_array_size() - 1)
|
||||
if newlines:
|
||||
msg.append("...")
|
||||
|
||||
var tmp = Log.color_wrap("[ ", opts)
|
||||
var last = len(msg) - 1
|
||||
for i in range(len(msg)):
|
||||
if newlines and last > 1:
|
||||
tmp += "\n\t"
|
||||
tmp += Log.to_pretty(msg[i],
|
||||
# duplicate here to prevent indenting-per-msg
|
||||
# e.g. when printing an array of dictionaries
|
||||
opts.duplicate(true))
|
||||
if i != last:
|
||||
tmp += Log.color_wrap(", ", opts)
|
||||
tmp += Log.color_wrap(" ]", opts)
|
||||
return tmp
|
||||
|
||||
# dictionary
|
||||
elif msg is Dictionary:
|
||||
var tmp = Log.color_wrap("{ ", opts)
|
||||
var ct = len(msg)
|
||||
var last
|
||||
if len(msg) > 0:
|
||||
last = msg.keys()[-1]
|
||||
for k in msg.keys():
|
||||
var val
|
||||
if k in Log.get_dictionary_skip_keys():
|
||||
val = "..."
|
||||
else:
|
||||
opts.indent_level += 1
|
||||
val = Log.to_pretty(msg[k], opts)
|
||||
if newlines and ct > 1:
|
||||
tmp += "\n\t" \
|
||||
+ range(indent_level)\
|
||||
.map(func(_i): return "\t")\
|
||||
.reduce(func(a, b): return str(a, b), "")
|
||||
if use_color:
|
||||
var key = Log.color_wrap('"%s"' % k, Log.assoc(opts, "typeof", "dict_key"))
|
||||
tmp += "%s: %s" % [key, val]
|
||||
else:
|
||||
tmp += '"%s": %s' % [k, val]
|
||||
if last and str(k) != str(last):
|
||||
tmp += Log.color_wrap(", ", opts)
|
||||
tmp += Log.color_wrap(" }", opts)
|
||||
return tmp
|
||||
|
||||
# strings
|
||||
elif msg is String:
|
||||
if msg == "":
|
||||
return '""'
|
||||
if "[color=" in msg and "[/color]" in msg:
|
||||
# assumes the string is already colorized
|
||||
# NOT PERFECT! could use a regex for something more robust
|
||||
return msg
|
||||
return Log.color_wrap(msg, opts)
|
||||
elif msg is StringName:
|
||||
return str(Log.color_wrap("&", opts), '"%s"' % msg)
|
||||
elif msg is NodePath:
|
||||
return str(Log.color_wrap("^", opts), '"%s"' % msg)
|
||||
|
||||
# vectors
|
||||
elif msg is Vector2 or msg is Vector2i:
|
||||
return log_overwrites.get("Vector2").call(msg, opts)
|
||||
|
||||
elif msg is Vector3 or msg is Vector3i:
|
||||
if use_color:
|
||||
return '%s%s%s%s%s%s%s' % [
|
||||
Log.color_wrap("(", opts),
|
||||
Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")),
|
||||
Log.color_wrap(",", opts),
|
||||
Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")),
|
||||
Log.color_wrap(",", opts),
|
||||
Log.color_wrap(msg.z, Log.assoc(opts, "typeof", "vector_value")),
|
||||
Log.color_wrap(")", opts),
|
||||
]
|
||||
else:
|
||||
return '(%s,%s,%s)' % [msg.x, msg.y, msg.z]
|
||||
elif msg is Vector4 or msg is Vector4i:
|
||||
if use_color:
|
||||
return '%s%s%s%s%s%s%s%s%s' % [
|
||||
Log.color_wrap("(", opts),
|
||||
Log.color_wrap(msg.x, Log.assoc(opts, "typeof", "vector_value")),
|
||||
Log.color_wrap(",", opts),
|
||||
Log.color_wrap(msg.y, Log.assoc(opts, "typeof", "vector_value")),
|
||||
Log.color_wrap(",", opts),
|
||||
Log.color_wrap(msg.z, Log.assoc(opts, "typeof", "vector_value")),
|
||||
Log.color_wrap(",", opts),
|
||||
Log.color_wrap(msg.w, Log.assoc(opts, "typeof", "vector_value")),
|
||||
Log.color_wrap(")", opts),
|
||||
]
|
||||
else:
|
||||
return '(%s,%s,%s,%s)' % [msg.x, msg.y, msg.z, msg.w]
|
||||
|
||||
# packed scene
|
||||
elif msg is PackedScene:
|
||||
if msg.resource_path != "":
|
||||
return str(Log.color_wrap("PackedScene:", opts), '%s' % msg.resource_path.get_file())
|
||||
elif msg.get_script() != null and msg.get_script().resource_path != "":
|
||||
return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name"))
|
||||
else:
|
||||
return Log.color_wrap(msg, opts)
|
||||
|
||||
# resource
|
||||
elif msg is Resource:
|
||||
if msg.get_script() != null and msg.get_script().resource_path != "":
|
||||
return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name"))
|
||||
elif msg.resource_path != "":
|
||||
return str(Log.color_wrap("Resource:", opts), '%s' % msg.resource_path.get_file())
|
||||
else:
|
||||
return Log.color_wrap(msg, opts)
|
||||
|
||||
# refcounted
|
||||
elif msg is RefCounted:
|
||||
if msg.get_script() != null and msg.get_script().resource_path != "":
|
||||
return Log.color_wrap(msg.get_script().resource_path.get_file(), Log.assoc(opts, "typeof", "class_name"))
|
||||
else:
|
||||
return Log.color_wrap(msg.get_class(), Log.assoc(opts, "typeof", "class_name"))
|
||||
|
||||
# fallback to primitive-type lookup
|
||||
else:
|
||||
return Log.color_wrap(msg, opts)
|
||||
|
||||
## to_printable ###########################################################################
|
||||
|
||||
static func log_prefix(stack):
|
||||
if len(stack) > 1:
|
||||
var call_site = stack[1]
|
||||
var basename = call_site["source"].get_file().get_basename()
|
||||
var line_num = str(call_site.get("line", 0))
|
||||
if call_site["source"].match("*/test/*"):
|
||||
return "{" + basename + ":" + line_num + "}: "
|
||||
elif call_site["source"].match("*/addons/*"):
|
||||
return "<" + basename + ":" + line_num + ">: "
|
||||
else:
|
||||
return "[" + basename + ":" + line_num + "]: "
|
||||
|
||||
static func to_printable(msgs, opts={}):
|
||||
var stack = opts.get("stack", [])
|
||||
var pretty = opts.get("pretty", true)
|
||||
var newlines = opts.get("newlines", false)
|
||||
var m = ""
|
||||
if len(stack) > 0:
|
||||
var prefix = Log.log_prefix(stack)
|
||||
var prefix_type
|
||||
if prefix != null and prefix[0] == "[":
|
||||
prefix_type = "SRC"
|
||||
elif prefix != null and prefix[0] == "{":
|
||||
prefix_type = "TEST"
|
||||
elif prefix != null and prefix[0] == "<":
|
||||
prefix_type = "ADDONS"
|
||||
if pretty:
|
||||
m += Log.color_wrap(prefix, Log.assoc(opts, "typeof", prefix_type))
|
||||
else:
|
||||
m += prefix
|
||||
for msg in msgs:
|
||||
# add a space between msgs
|
||||
if pretty:
|
||||
m += "%s " % Log.to_pretty(msg, opts)
|
||||
else:
|
||||
m += "%s " % str(msg)
|
||||
return m.trim_suffix(" ")
|
||||
|
||||
## public print fns ###########################################################################
|
||||
|
||||
static func is_not_default(v):
|
||||
return not v is String or (v is String and v != "ZZZDEF")
|
||||
|
||||
static func pr(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
|
||||
var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
|
||||
msgs = msgs.filter(Log.is_not_default)
|
||||
var m = Log.to_printable(msgs, {stack=get_stack()})
|
||||
print_rich(m)
|
||||
|
||||
static func info(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
|
||||
var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
|
||||
msgs = msgs.filter(Log.is_not_default)
|
||||
var m = Log.to_printable(msgs, {stack=get_stack()})
|
||||
print_rich(m)
|
||||
|
||||
static func log(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
|
||||
var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
|
||||
msgs = msgs.filter(Log.is_not_default)
|
||||
var m = Log.to_printable(msgs, {stack=get_stack()})
|
||||
print_rich(m)
|
||||
|
||||
static func prn(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
|
||||
var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
|
||||
msgs = msgs.filter(Log.is_not_default)
|
||||
var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true})
|
||||
print_rich(m)
|
||||
|
||||
static func warn(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
|
||||
var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
|
||||
msgs = msgs.filter(Log.is_not_default)
|
||||
var rich_msgs = msgs.duplicate()
|
||||
rich_msgs.push_front("[color=yellow][WARN][/color]")
|
||||
print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true}))
|
||||
var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false})
|
||||
push_warning(m)
|
||||
|
||||
static func err(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
|
||||
var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
|
||||
msgs = msgs.filter(Log.is_not_default)
|
||||
var rich_msgs = msgs.duplicate()
|
||||
rich_msgs.push_front("[color=red][ERR][/color]")
|
||||
print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true}))
|
||||
var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false})
|
||||
push_error(m)
|
||||
|
||||
static func error(msg, msg2="ZZZDEF", msg3="ZZZDEF", msg4="ZZZDEF", msg5="ZZZDEF", msg6="ZZZDEF", msg7="ZZZDEF"):
|
||||
var msgs = [msg, msg2, msg3, msg4, msg5, msg6, msg7]
|
||||
msgs = msgs.filter(Log.is_not_default)
|
||||
var rich_msgs = msgs.duplicate()
|
||||
rich_msgs.push_front("[color=red][ERR][/color]")
|
||||
print_rich(Log.to_printable(rich_msgs, {stack=get_stack(), newlines=true}))
|
||||
var m = Log.to_printable(msgs, {stack=get_stack(), newlines=true, pretty=false})
|
||||
push_error(m)
|
||||
Loading…
Add table
Add a link
Reference in a new issue