File: meta.py

package info (click to toggle)
python-logfury 1.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, sid, trixie
  • size: 168 kB
  • sloc: python: 637; sh: 7; makefile: 5
file content (133 lines) | stat: -rw-r--r-- 5,073 bytes parent folder | download
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