Last active
December 12, 2018 15:51
-
-
Save JanChec/cc27c540ca532e049ae50c9fbab5c570 to your computer and use it in GitHub Desktop.
Python - print function calls - decorator and metaclass
This file contains 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
# 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