Skip to content

Instantly share code, notes, and snippets.

@JanChec
Last active December 12, 2018 15:51
Show Gist options
  • Save JanChec/cc27c540ca532e049ae50c9fbab5c570 to your computer and use it in GitHub Desktop.
Save JanChec/cc27c540ca532e049ae50c9fbab5c570 to your computer and use it in GitHub Desktop.
Python - print function calls - decorator and metaclass
# Python textual debugging tool
# A function wrapper to display PID, stack-size indented function name and arguments when it's called.
# Also display similar message when function ends.
# Example output for three wrapped class methods (skips `self` when displaying class method arguments):
21906 P300GuiPeer.send_blink
(blink_id=-1, blink_time=1544627502.9847314)
21906 P300GuiPeer.create_message
(blink_id=-1, blink_time=1544627502.9847314)
21906 P300GuiPeer.create_message ended!
21906 P300GuiPeer.send
(message=<Message object, (...)>)
21906 P300GuiPeer.send ended!
21906 P300GuiPeer.send_blink ended!
# It is also distinctly colored, which is not visible here.
# Works with standalone functions and class methods.
### Requirements ###
termcolor (==1.1.0 tested)
### Usage ###
# Whole class - wrap every method of the class:
class Calibrator(metaclass=PrintMethodCallsMetaclass):
...
# Single function
@print_calls()
async def _dependencies_are_ready(self):
...
# ^ needs the () after print_usage because it accepts parameters, as follows:
@print_calls(show_arguments=False, display_end=False)
def __init__(self, *args, **kwargs):
...
# Set 'show_arguments' to False if they are not important but big.
# Set 'display_end' to False if you don't want to see the message about ending the function call.
# You can also set arguments in the meta class:
attribute = print_calls(show_arguments=False, display_end=False)(attribute)
### Code ###
class PrintMethodCallsMetaclass(type):
def __new__(meta, classname, bases, class_dict):
import types
excluded_names = []
new_class_dict = {}
for attribute_name, attribute in class_dict.items():
if isinstance(attribute, types.FunctionType) and attribute_name not in excluded_names:
attribute = print_calls()(attribute)
new_class_dict[attribute_name] = attribute
return type.__new__(meta, classname, bases, new_class_dict)
def print_calls(show_arguments=True, display_end=True): # noqa: C901
def wrap(function):
import collections
import functools
import inspect
import termcolor
import os
@functools.wraps(function)
def wrapped(*args, **kwargs):
def _run():
reference = _build_reference()
stack_indent = _build_indent()
entry_info = "{}{}{}".format(os.getpid(), stack_indent, reference)
if show_arguments:
nonself_arguments = _get_nonself_arguments(args, kwargs)
if nonself_arguments:
formatted_arguments = ['{}={}'.format(termcolor.colored(key, 'cyan'), value)
for key, value in nonself_arguments.items()]
pid_placeholder = ' ' * len(str(os.getpid()))
flat_arguments = ', '.join(formatted_arguments)
arguments_info = "{}{}({})".format(pid_placeholder,
stack_indent,
flat_arguments)
entry_info += '\n{}'.format(arguments_info)
print(entry_info, flush=True)
output = function(*args, **kwargs)
if display_end:
return_info = "{}{}{} ended!".format(os.getpid(), stack_indent, reference)
print(return_info, flush=True)
return output
def _build_reference():
if _is_class_function():
self = args[0]
class_name = termcolor.colored(self.__class__.__name__, 'green')
function_name = termcolor.colored(function.__name__, 'yellow')
reference = '{}.{}'.format(class_name, function_name)
else:
reference = termcolor.colored(function.__name__, 'magenta')
return reference
def _build_indent():
this_wrapper_stack_elements_now = 3
target_function_stack_elements = 1
return ' ' * (len(inspect.stack())
- this_wrapper_stack_elements_now
+ target_function_stack_elements)
def _get_nonself_arguments(args, kwargs):
args_names = _get_args_names()
if _is_class_function():
arguments = args[1:]
arguments_names = args_names[1:]
else:
arguments = args
arguments_names = args_names
items = list(zip(arguments_names, arguments)) + list(kwargs.items())
return collections.OrderedDict(items)
def _is_class_function():
args_names = _get_args_names()
return len(args) > 0 and args_names[0] == 'self'
def _get_args_names():
spec = inspect.getfullargspec(function)
args_names = spec.args
if spec.varargs:
args_names.append(spec.varargs)
return args_names
return _run()
return wrapped
return wrap
### TODO/wishlist ###
# distinct coloring of PIDs - unique per PID
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment