1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
|
from abc import ABCMeta
import logging
from .trace_call import trace_call
class AbstractTraceMeta(type):
"""
An abstract metaclass for tracing classes
"""
@classmethod
def _filter_attribute(mcs, attribute_name, attribute_value):
"""
decides whether the given attribute should be excluded from tracing or not
"""
if attribute_name == '__module__':
return True
elif hasattr(attribute_value, '_trace_disable'):
return True
return False
def __new__(mcs, name, bases, attrs, **kwargs):
# *magic*: an educated guess is made on how the module that the
# processed class is created in would get its logger.
# It is assumed that the popular convention recommended by the
# developers of standard library (`logger = logging.getLogger(__name__)`)
# is used.
target_logger = logging.getLogger(attrs['__module__'])
for attribute_name in attrs:
attribute_value = attrs[attribute_name]
if mcs._filter_attribute(attribute_name, attribute_value):
continue
# attrs['__module__'] + '.' + attribute_name is worth logging
# collect the `only` and `skip` sets from mro
only = getattr(attribute_value, '_trace_only', None)
skip = getattr(attribute_value, '_trace_skip', None)
disable = False
for base in bases:
base_attribute_value = getattr(base, attribute_name, None)
if base_attribute_value is None:
continue # the base class did not define this
if hasattr(base_attribute_value, '_trace_disable'): # that's probably done by @disable_trace
# ex. inheriting from Abstract class, where getters are marked
disable = True
break
only_candidates = getattr(base_attribute_value, '_trace_only', None)
if only_candidates is not None:
if only is not None:
only.update(only_candidates)
else:
only = set(only_candidates)
skip_candidates = getattr(base_attribute_value, '_trace_skip', None) # is this 5 LOC clone worth refactoring?
if skip_candidates is not None:
if skip is not None:
skip.update(skip_candidates)
else:
skip = set(skip_candidates)
if disable:
continue # the base class does not wish to trace it at all
# create a wrapper (decorator object)
wrapper = trace_call(
target_logger,
only=only,
skip=skip,
)
original_wrapper = None
# Special case for staticmethod/classmethod
if isinstance(attribute_value, staticmethod):
attribute_value = attribute_value.__func__
original_wrapper = staticmethod
elif isinstance(attribute_value, classmethod):
attribute_value = attribute_value.__func__
original_wrapper = classmethod
# wrap the callable in it
wrapped_value = wrapper(attribute_value)
# apply the original wrapper if provided
if original_wrapper is not None:
wrapped_value = original_wrapper(wrapped_value)
# substitute the trace-wrapped method for the original
attrs[attribute_name] = wrapped_value
return super(AbstractTraceMeta, mcs).__new__(mcs, name, bases, attrs)
class TraceAllPublicCallsMeta(AbstractTraceMeta):
"""
traces all public method calls
"""
@classmethod
def _filter_attribute(mcs, attribute_name, attribute_value):
if super(TraceAllPublicCallsMeta, mcs)._filter_attribute(attribute_name, attribute_value):
return True
elif not callable(attribute_value):
# Special case for staticmethod/classmethod as prior to Python 3.10
# staticmethod/classmethod are not callable
if not isinstance(attribute_value, (classmethod, staticmethod)):
return True # it is a field
elif attribute_name.startswith('_'):
return True # it is a _protected or a __private method (or __magic__)
return False
class AbstractTracePublicCallsMeta(ABCMeta, TraceAllPublicCallsMeta):
pass
class DefaultTraceMeta(TraceAllPublicCallsMeta):
"""
traces all public method calls, except for ones with names that begin with 'get_'
"""
@classmethod
def _filter_attribute(mcs, attribute_name, attribute_value):
if super(DefaultTraceMeta, mcs)._filter_attribute(attribute_name, attribute_value):
return True
elif attribute_name.startswith('get_'):
return True
return False
class DefaultTraceAbstractMeta(ABCMeta, DefaultTraceMeta):
pass
|