Skip to content

Instantly share code, notes, and snippets.

@polyvertex
Last active January 24, 2022 18:20
Show Gist options
  • Save polyvertex/6b48cd51c1635a3e10c3 to your computer and use it in GitHub Desktop.
Save polyvertex/6b48cd51c1635a3e10c3 to your computer and use it in GitHub Desktop.
A Python3 pretty-printer that also does introspection to detect the original name of the passed variables
#!/usr/bin/env python3
#
# pydump
# A Python3 pretty-printer that also does introspection to detect the original
# name of the passed variables
#
# Jean-Charles Lefebvre <polyvertex@gmail.com>
# Latest version at: http://gist.github.com/polyvertex (pydump)
import sys
import pprint
import inspect
import ast
def dbg_dump(
*args,
dumpopt_stream=sys.stderr,
dumpopt_forcename=True,
dumpopt_pformat={'indent': 2},
dumpopt_srcinfo=1,
**kwargs):
"""
Pretty-format every passed positional and named parameters, in that order,
prefixed by their **original** name (i.e.: the one used by the caller), or
by their type name for literals.
Depends on the ``pprint``, ``inspect`` and ``ast`` standard modules.
Note that the names of the keyword arguments you want to dump must not begin
with ``dumpopt_`` since this prefix is used internally to differentiate
options over values to dump.
Also, the introspection code won't behave as expected if you make recursive
calls to this function.
Options can be passed as keyword arguments to tweak behavior and output
format:
* ``dumpopt_stream``:
May you wish to print() the result directly, you can pass a stream object
(e.g.: ``sys.stdout``) through this option, that will be given to
``print()``'s ``file`` keyword argument.
You can also specify None in case you just want the output string to be
returned without further ado.
* ``dumpopt_forcename``:
A boolean value to indicate wether you want every dumped value to be
prepended by its name (i.e.: its name or its type).
If ``False``, only non-literal values will be named.
* ``dumpopt_pformat``:
The dictionary of keyword arguments to pass to ``pprint.pformat()``
* ``dumpopt_srcinfo``:
Specify a false value (``None``, ``False``, zero) to skip caller's info.
Specify ``1`` to output caller's line number only.
Specify ``2`` to output caller's file name and line number.
Specify ``3`` or greater to output caller's file path and line number.
Example:
``dbg_dump(my_var, None, True, 123, "Bar", (4, 5, 6), fcall(), hello="world")``
Result:
::
DUMP(202):
my_var: 'Foo'
None: None
Bool: True
Num: 123
Str: 'Bar'
Tuple: (4, 5, 6)
fcall(): "Function's Result"
hello: 'world'
"""
try:
def _find_caller_node(root_node, func_name, last_lineno):
# find caller's node by walking down the ast, searching for an
# ast.Call object named func_name of which the last source line is
# last_lineno
found_node = None
lineno = 0
def _luke_astwalker(parent):
nonlocal found_node
nonlocal lineno
for child in ast.iter_child_nodes(parent):
# break if we passed the last line
if hasattr(child, "lineno") and child.lineno:
lineno = child.lineno
if lineno > last_lineno:
break
# is it our candidate?
if (isinstance(child, ast.Name)
and isinstance(parent, ast.Call)
and child.id == func_name):
found_node = parent
break
_luke_astwalker(child)
_luke_astwalker(root_node)
return found_node
frame = inspect.currentframe()
backf = frame.f_back
this_func_name = frame.f_code.co_name
#this_func = backf.f_locals.get(
# this_func_name, backf.f_globals.get(this_func_name))
# get the source code of caller's module
# note that we have to reload the entire module file since the
# inspect.getsource() function doesn't work in some cases (i.e.:
# returned source content was incomplete... Why?!).
# --> is inspect.getsource broken???
# source = inspect.getsource(backf.f_code)
#source = inspect.getsource(backf.f_code)
with open(backf.f_code.co_filename, "r") as f:
source = f.read()
# get the ast node of caller's module
# we don't need to use ast.increment_lineno() since we've loaded the
# whole module
ast_root = ast.parse(source, backf.f_code.co_filename)
#ast.increment_lineno(ast_root, backf.f_code.co_firstlineno - 1)
# find caller's ast node
caller_node = _find_caller_node(ast_root, this_func_name, backf.f_lineno)
if not caller_node:
raise Exception("caller's AST node not found")
# keep some useful info for later
src_info = {
'file': backf.f_code.co_filename,
'name': (
backf.f_code.co_filename.replace("\\", "/").rpartition("/")[2]),
'lineno': caller_node.lineno}
# if caller's node has been found, we now have the AST of our parameters
args_names = []
for arg_node in caller_node.args:
if isinstance(arg_node, ast.Name):
args_names.append(arg_node.id)
elif isinstance(arg_node, ast.Attribute):
if hasattr(arg_node, "value") and hasattr(arg_node.value, "id"):
args_names.append(arg_node.value.id + "." + arg_node.attr)
else:
args_names.append(arg_node.attr)
elif isinstance(arg_node, ast.Subscript):
args_names.append(arg_node.value.id + "[]")
elif (isinstance(arg_node, ast.Call)
and hasattr(arg_node, "func")
and hasattr(arg_node.func, "id")):
args_names.append(arg_node.func.id + "()")
elif dumpopt_forcename:
if (isinstance(arg_node, ast.NameConstant)
and arg_node.value is None):
args_names.append("None")
elif (isinstance(arg_node, ast.NameConstant)
and arg_node.value in (False, True)):
args_names.append("Bool")
else:
args_names.append(arg_node.__class__.__name__)
else:
args_names.append(None)
except:
#import traceback
#traceback.print_exc()
src_info = None
args_names = [None] * len(args)
args_count = len(args) + len(kwargs)
output = ""
if dumpopt_srcinfo:
if not src_info:
output += "DUMP(<unknown>):"
else:
if dumpopt_srcinfo <= 1:
fmt = "DUMP({2}):"
elif dumpopt_srcinfo == 2:
fmt = "{1}({2}):"
else:
fmt = "{0}({2}):"
output += fmt.format(
src_info['file'], src_info['name'], src_info['lineno'])
output += "\n" if args_count > 1 else " "
else:
src_info = None
for name, obj in zip(
args_names + list(kwargs.keys()),
list(args) + list(kwargs.values())):
if name and name.startswith("dumpopt_"):
continue
if dumpopt_srcinfo and args_count > 1:
output += " "
if name:
output += name + ": "
output += pprint.pformat(obj, **dumpopt_pformat) + "\n"
if dumpopt_stream:
print(output, end="", file=dumpopt_stream)
return None # explicit is better than implicit
else:
return output.rstrip()
if __name__ == "__main__":
def fcall():
return "Function's Result"
my_var = "Foo"
dbg_dump(my_var)
dbg_dump(
my_var, None, True, 123, "Bar", (4, 5, 6), fcall(),
dbg_dump(1, dumpopt_stream=None), hello="world")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment