5

I have written a custom logging class for module logging that I called call. With this class I hope to place it in any function/method and it logs the function name with its arguments and all values the function was called with.

This works fine for class methods

Foo.bar(self, a=1, b=2, c=3, *args=(), **kwargs={'something': 4})

using this minimal example

import logging
import inspect

def call(logger):
    fname = []  # Function name including module and class
    fargs = []  # Arguments of function including positional and named arguments

    parentframe = inspect.stack()[1][0]
    module      = inspect.getmodule(parentframe)

    if module and module.__name__ != "__main__":
        fname.append(module.__name__)

    codename = parentframe.f_code.co_name

    if "self" in parentframe.f_locals:
        fname.append(parentframe.f_locals["self"].__class__.__name__)

    fobj = getattr(parentframe.f_locals["self"].__class__, codename)

    if codename != "<module>":
        fname.append(codename)

    argspec = inspect.formatargspec(*inspect.getfullargspec(fobj))
    args = argspec[1:-1].split(",")

    for arg in args:
        argkey = arg.strip().replace("*", "").split("=")[0]
        if arg == "self":
            fargs.append("self")
        else:
            fargs.append(arg.split("=")[0] + "=" + str(parentframe.f_locals[argkey]))

    del parentframe

    msg = ".".join(fname) + "(" + ",".join(fargs) + ")"
    if logger.isEnabledFor(30):
        logger.log(30, msg)


class Foo:

    def __init__(self, l):
        self.logger = l

    def bar(self, a, b, c=3, *args, **kwargs):
        call(self.logger)


if __name__ == "__main__":
    logging.addLevelName(30, "CALL")
    logger = logging.getLogger('blub')
    logger.level = 20
    f = Foo(logger)
    f.bar(1, 2, something=4)
    print("done...")

My problem is when I use the same functionality on static methods or simple functions. It fails at the line where I get the function object (fobj = getattr(parentframe.f_locals["self"].__class__, codename) ) using self.

parentframe is the Frame object of the function in questions I presume. I have not yet found a way to get the function object from that object. Is there a way?

2 Answers 2

4

Use getattr(module, codename) to get the function-object of functions that are not contained in classes.

Here the full code:

import logging
import inspect

def call(logger):
    fname = []  # Function name including module and class
    fargs = []  # Arguments of function including positional and named arguments

    parentframe = inspect.stack()[1][0]
    module      = inspect.getmodule(parentframe)

    if module and module.__name__ != "__main__":
        fname.append(module.__name__)

    codename = parentframe.f_code.co_name

    if "self" in parentframe.f_locals:
        fname.append(parentframe.f_locals["self"].__class__.__name__)
        fobj = getattr(parentframe.f_locals["self"].__class__, codename)
    else:
        fobj = getattr(module, codename)

    if codename != "<module>":
        fname.append(codename)

    argspec = inspect.formatargspec(*inspect.getfullargspec(fobj))
    args = argspec[1:-1].split(",")

    for arg in args:
        argkey = arg.strip().replace("*", "").split("=")[0]
        if arg == "self":
            fargs.append("self")
        else:
            fargs.append(arg.split("=")[0] + "=" + str(parentframe.f_locals[argkey]))

    del parentframe

    msg = ".".join(fname) + "(" + ",".join(fargs) + ")"
    if logger.isEnabledFor(30):
        logger.log(30, msg)


class Foo:

    def __init__(self, l):
        self.logger = l

    def bar(self, a, b, c=3, *args, **kwargs):
        call(self.logger)


def boo(a, b, c=3, *args, **kwargs):
    call(logger)


if __name__ == "__main__":
    logging.addLevelName(30, "CALL")
    logger = logging.getLogger('blub')
    logger.level = 20
    f = Foo(logger)
    f.bar(1, 2, something=4)
    boo(1, 2, something=4)
    print("done...")
Sign up to request clarification or add additional context in comments.

Comments

0

Taking ideas from both, I wrote this function to find the function object from a frame. I'm sure there's some edge cases around inherited staticmethods, and obviously any code not using the cls and self conventions for param names. This also doesn't work for lambdas... but you shouldn't be logging anything out in a lamba anyway :-P

def _get_func_obj(f):
    """
    Get function object from a frame. If it can't find it, return None
    """

    codename = f.f_code.co_name
    fobj = None

    try:
        if "self" in f.f_locals:  # regular method
            fobj = getattr(f.f_locals["self"].__class__, codename)
        elif "cls" in f.f_locals:  # class method
            fobj = getattr(f.f_locals["cls"], codename)
        else:
            module = inspect.getmodule(f)  # only fetch module if we need it

            if hasattr(module, codename):  # regular module level function
                fobj = getattr(module, codename)
            else:  # static method
                classes = [
                    getattr(module, name)
                    for name in dir(module)
                    if inspect.isclass(getattr(module, name))
                ]
                for cls in classes:
                    if (
                        hasattr(cls, codename)
                        and getattr(cls, codename).__code__ == f.f_code
                    ):
                        fobj = getattr(cls, codename)
                        break
        if fobj is None:
            """it's likely some nested function/method or a lambda, who logs in a lambda?"""
        return fobj
    except Exception:
        """never break logging"""

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.