Skip to content

Instantly share code, notes, and snippets.

@fonic
Last active July 15, 2023 11:38
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fonic/dfcaa350d5fb57835cf6c1689d251912 to your computer and use it in GitHub Desktop.
Save fonic/dfcaa350d5fb57835cf6c1689d251912 to your computer and use it in GitHub Desktop.
Python pretty print arbitrary objects
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Recursively generate pretty print of arbitrary objects
def generate_pprint(obj, level_indent=" ", max_depth=None, verbose_output=True,
justify_output=True, prevent_loops=True, prevent_revisit=False,
explore_objects=True, excluded_ids=[], visited_ids=[],
path_ids=[], current_depth=0):
"""Recursively generates pretty print of arbitrary objects.
Recursively generates pretty print of contents of arbitrary objects. Contents
are represented as lines containing key-value pairs. Recursion may be affected
by various parameters (see below).
Arguments:
max_depth: Maximum allowed depth of recursion. If depth is exceeded,
recursion is stopped.
verbose_output: Produce verbose output. Adds some additional details for
certain data types
justify_output: Justify output. Produces output in block-like appearance
with equal spacing between keys and values.
prevent_loops: Detect and prevent recursion loops by keeping track of
already visited objects within current recursion path.
prevent_revisit: Detect and prevent revisiting of already visited objects.
While 'prevent_loops' prevents revisiting objects only
within one single recursion path, this prevents revisiting
objects globally across all recursion paths.
explore_objects: Explore (i.e. recurse into) arbitrary objects. If enabled,
arbitrary objects not matching base types are explored.
If disabled, only certain types of objects are explored
(tuple, list, dict, set/frozenset). Note that this does
not affect the initially provided object (which is always
explored).
excluded_ids: List of object IDs to exclude from exploration (i.e. re-
cursion). Recursion is stopped if object with matching
ID is encountered.
visited_ids, Internal variables used to control recursion flow, loop
path_ids, detection and revisit detection. Never provide or modify
current_depth: these!
Returns:
Generated pretty print output as list of lines (strings).
Raises:
TypeError: Object or value has unsupported type (should never occur)
AssertionError: Assertion failed, most likely exposing a bug (should never
occur)
"""
output = []
indent = level_indent * current_depth
# Check if object has already been visited within current recursion path.
# If so, we encoutered a loop and thus need to break off recursion. If
# not, continue and add object to list of visited objects within current
# recursion path
if (prevent_loops == True):
if (id(obj) in path_ids):
output.append(indent + "<recursion loop detected>")
return output
path_ids.append(id(obj))
# Check if object has already been visited. If so, we're not going to visit
# it again and break off recursion. If not, continue and add current object
# to list of visited objects
if (prevent_revisit == True):
if (id(obj) in visited_ids):
output.append(indent + "<item already visited>")
return output
visited_ids.append(id(obj))
# Check if maximum allowed depth of recursion has been exceeded. If so, break
# off recursion
if (max_depth != None and current_depth > max_depth):
output.append(indent + "<recursion limit reached>")
return output
# Check if object is supposed to be excluded. If so, break of recursion
if (id(obj) in excluded_ids):
output.append(indent + "<item is excluded>")
return output
# Determine keys and associated values
if (isinstance(obj, dict)):
keys = obj.keys()
values = obj
elif (isinstance(obj, tuple) or isinstance(obj, list)):
keys = range(0, len(obj))
values = obj
elif (isinstance(obj, set) or isinstance(obj, frozenset)):
keys = range(0, len(obj))
values = [ item for item in obj ]
elif (isinstance(obj, object)):
keys = [ item for item in dir(obj) if (not item.startswith("_")) ]
values = { key: getattr(obj, key) for key in keys }
else: # should never occur as everything in Python is an 'object' and should be caught above, but better be safe than sorry
raise TypeError("unsupported object type: '%s'" % type(obj))
# Define key string templates. If output is to be justified, determine maximum
# length of key string and adjust templates accordingly
kstmp1 = kstmp2 = "%s"
if (justify_output == True):
maxlen = 0
for key in keys:
klen = len(str(key))
if (klen > maxlen):
maxlen = klen
kstmp1 = "%-" + str(maxlen+3) + "s" # maxlen+3: surrounding single quotes + trailing colon
kstmp2 = "%-" + str(maxlen+1) + "s" # maxlen+1: trailing colon
# Process keys and associated values
for key in keys:
value = values[key]
# Generate key string
keystr = kstmp1 % ("'" + str(key) + "':") if (isinstance(obj, dict) and isinstance(key, str)) else kstmp2 % (str(key) + ":")
# Generate value string
valstr = ""
exp_obj = False
if (isinstance(value, dict)):
valstr = "<dict, %d items, class '%s'>" % (len(value), type(value).__name__) if (verbose_output == True) else "<dict, %d items>" % len(value)
elif (isinstance(value, tuple)):
valstr = "<tuple, %d items, class '%s'>" % (len(value), type(value).__name__) if (verbose_output == True) else "<tuple, %d items>" % len(value)
elif (isinstance(value, list)):
valstr = "<list, %d items, class '%s'>" % (len(value), type(value).__name__) if (verbose_output == True) else "<list, %d items>" % len(value)
elif (isinstance(value, set)): # set and frozenset are distinct
valstr = "<set, %d items, class '%s'>" % (len(value), type(value).__name__) if (verbose_output == True) else "<set, %d items>" % len(value)
elif (isinstance(value, frozenset)): # set and frozenset are distinct
valstr = "<frozenset, %d items, class '%s'>" % (len(value), type(value).__name__) if (verbose_output == True) else "<frozenset, %d items>" % len(value)
elif (isinstance(value, range)):
valstr = "<range, start %d, stop %d, step %d>" % (value.start, value.stop, value.step) if (verbose_output == True) else "<range(%d,%d,%d)>" % (value.start, value.stop, value.step)
elif (isinstance(value, bytes)):
valstr = "<bytes, %d bytes>" % len(value)
elif (isinstance(value, bytearray)):
valstr = "<bytearray, %d bytes>" % len(value)
elif (isinstance(value, memoryview)):
valstr = "<memoryview, %d bytes, object %s>" % (len(value), type(value.obj).__name__) if (verbose_output == True) else "<memoryview, %d bytes>" % len(value)
elif (isinstance(value, bool)): # needs to be above int as 'bool' also registers as 'int'
valstr = "%s" % value
elif (isinstance(value, int)):
valstr = "%d (0x%x)" % (value, value)
elif (isinstance(value, float)):
valstr = "%s" % str(value) # str(value) provides best representation; alternatives: '%e|%E|%f|%F|%g|%G' % value
elif (isinstance(value, complex)):
valstr = "%s" % str(value)
elif (isinstance(value, str)):
#valstr = "%s" % repr(value) # using repr(value) to encode escape sequences (e.g. '\n' instead of actual newline); repr() adds surrounding quotes for strings (style based on contents)
#valstr = "%r" % value # alternative, seems to be the same as repr(value)
valstr = "'%s'" % repr(value)[1:-1] # this seems to be the only way to always get a single-quoted string
elif (value == None):
valstr = "None"
elif isinstance(value, type): # checks if object is 'class' (https://stackoverflow.com/a/10123520/1976617); needs to be above 'callable' as 'class' also registers as 'callable'
#valstr = "<class '%s'>" % type(value).__name__ if (verbose_output == True) else "<class>"
valstr = "<class '%s.%s'>" % (value.__module__, value.__name__) if (verbose_output == True) else "<class>"
elif (callable(value)): # catches everything callable, i.e. functions, methods, classes (due to constructor), etc.
#valstr = "<callable, %s>" % repr(value)[1:-1] if (verbose_output == True) else "<callable>"
#valstr = "<callable, class '%s'>" % type(value).__name__ if (verbose_output == True) else "<callable>"
valstr = "<callable, class '%s.%s'>" % (value.__class__.__module__, value.__class__.__name__) if (verbose_output == True) else "<callable>"
elif (isinstance(value, object)): # this has to be last in line as *everything* above also registers as 'object'
#valstr = "<object, class '%s'>" % type(value).__name__ if (verbose_output == True) else "<object>"
valstr = "<object, class '%s.%s'>" % (value.__class__.__module__, value.__class__.__name__) if (verbose_output == True) else "<object>"
if (explore_objects == True):
exp_obj = True # this ensures we only explore objects that do not represent a base type (i.e. everything listed above)
else: # should never occur as everything in Python is an 'object' and should be caught above, but better be safe than sorry
#valstr = "'%s'" % str(value) if (verbose_output == True) else str(value)
raise TypeError("unsupported value type: '%s'" % type(value))
# Generate key-value line from key/value strings and add to output
output.append(indent + keystr + " " + valstr)
# Explore value object recursively if it meets certain criteria
if (isinstance(value, dict) or isinstance(value, tuple) or isinstance(value, list) or isinstance(value, set) or
isinstance(value, frozenset) or (isinstance(value, object) and exp_obj == True)):
# These may be used to prevent recursion beforehand, i.e. before calling this
# function again, as an alternative to checks at beginning of function. Leaving
# this here for future reference
#if (prevent_loops == True and id(value) in path_ids):
# #output[-1] += " <recursion loop detected>"
# output[-1] += " <recursion loop>"
# continue
#if (prevent_revisit == True and id(value) in visited_ids):
# #output[-1] += " <item already visited>"
# output[-1] += " <already visited>"
# continue
#if (max_depth != None and current_depth+1 > max_depth):
# #output[-1] += " <recusion limit reached>"
# output[-1] += " <recursion limit>"
# continue
#if (id(value) in excluded_ids):
# #output[-1] += " <item is excluded>"
# output[-1] += " <item excluded>"
# continue
output += generate_pprint(value, level_indent=level_indent, max_depth=max_depth, verbose_output=verbose_output,
justify_output=justify_output, prevent_loops=prevent_loops, prevent_revisit=prevent_revisit,
explore_objects=explore_objects, excluded_ids=excluded_ids, visited_ids=visited_ids,
path_ids=path_ids, current_depth=current_depth + 1)
# Remove object from list of visited objects within current recursion path
# (part of recursion loop detection; this 'rolls back' the laid out path)
if (prevent_loops == True):
assert len(path_ids) > 0 and path_ids[-1] == id(obj), "last item in list of path objects not existing or not matching object"
path_ids.pop()
# Return generated output
return output
# Convenience wrapper for generate_pprint()
def print_pprint(*args, **kwargs):
output = generate_pprint(*args, **kwargs)
for line in output:
print(line)
# Demonstration (executed if run directly)
if (__name__ == "__main__"):
import sys
print_pprint(sys)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment