Last active
January 24, 2022 18:20
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 <[email protected]> | |
# 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