Python 在装饰器中单击基于选项的登录

Python click option based logging in decorator

新的开始。我有一个 CLI 应用程序,它使用 click 来处理参数解析。对于主要的“可执行”脚本,我有一个定义的详细标志(-v、-vv、-vvv、...)来控制日志记录的详细程度。我想“跟踪”特定功能的功能调用。下面是一个示例,希望能说明问题。

import click
import logging
import functools


class MyLogger(object):
    def __init__(self):
        self.__logger = None

    def init_logger(self, name, verbosity):
        logging_levels = {0: logging.CRITICAL,
                          1: logging.ERROR,
                          2: logging.INFO,
                          3: logging.DEBUG}
        logging.basicConfig(level=logging_levels.get(verbosity, logging.WARNING))
        self.__logger = logging.getLogger(name)

    @property
    def logger(self):
        return self.__logger


myLogger = MyLogger()


class TraceFunction(object):
    def __init__(self, logger):
        self.logger = logger

    def __call__(self, function):
        name = function.__name__

        @functools.wraps(function)
        def wrapped(*args, **kwargs):
            self.logger.debug(f'{name}({list(*args)}, {dict(**kwargs)})')
            result = function(*args, **kwargs)
            self.logger.debug(f'{result}')
            return result

        return wrapped


# (1) @TraceFunction(myLogger.logger)
def echo(message):
    return message.upper()


@click.command('echo')
@click.option('-e', '--echo', 'message', required=True, type=str)
def echo_command(message):
    myLogger.logger.info('echo_command')
    return echo(message)


@click.group()
@click.option('-v', 'verbosity', count=True)
def main(verbosity: int):
    myLogger.init_logger(__name__, verbosity)
    # (2) TraceFunction(myLogger.logger)(echo)
    myLogger.logger.info('main')


if __name__ == '__main__':
    main.add_command(echo_command)
    main()

以上如果执行将正确产生以下输出:

script.py -vv echo -e "Hello World"
INFO: __main__:main
INFO: __main__:echo_command

我想“追踪”函数:echo。更准确地说,我想用实际参数和返回值记录实际的函数调用。好的,不止于此,但我需要最少的样本。为此,我尝试了两件事,在评论中标有 (1) 和 (2)。

@TraceFunction(myLogger.logger)
def echo(message):
    return message.upper()

它完全不起作用,因为我原来的问题 python 将执行 TraceFunction。call(echo) before in "main" I call init_logger 这基本上会配置记录器本身。作为 TraceFunction.call 的结果,记录器是 None,我得到:

AttributeError: 'NoneType' object has no attribute 'debug'

好吧,我可以稍后再注册,至少我是(2)想到的。好吧,异常肯定消失了,但是永远不会调用 call 中定义的“包装”,而且除了已经显示的

之外,再一次没有记录任何内容
script.py -vvv echo -e "Hello World"
INFO: __main__:main
INFO: __main__:echo_command

@更新 按照 afterburner 的回答,事情会更进一步,但它没有做它应该做的事情:

script.py -vvv echo -e "Hello World"
DEBUG:__main__:echo(['F','o','o'],{})
DEBUG:__main__:FOO
INFO: __main__:main
INFO: __main__:echo_command

意料之中的井。另一方面,预期输出为:

script.py -vvv echo -e "Hello World"
INFO: __main__:main
INFO: __main__:echo_command
DEBUG:__main__:echo(['Hello World'],{})
DEBUG:__main__:HELLO WORLD

所以,我看到的主要问题是您没有调用包装函数。

TraceFunction(myLogger.logger)(echo)
# vs
 TraceFunction(myLogger.logger)(echo)()

我还对您的代码进行了一些更改,但主要问题是 wrapped 函数从未被调用过。

class MyLogger(object):
    def __init__(self):
        self.__logger = None

    def init_logger(self, name, verbosity):
        # extract log level based on verbosity flag
        logging_levels = [logging.CRITICAL, logging.INFO, logging.DEBUG]
        logging.basicConfig(level=logging_levels[verbosity])
        self.__logger = logging.getLogger(name)

    @property
    def logger(self):
        return self.__logger


myLogger = MyLogger()


class TraceFunction(object):
    def __init__(self, logger):
        self.logger = logger

    def __call__(self, function):
        name = function.__name__

        @functools.wraps(function)
        def wrapped(*args, **kwargs):
            # improved the formatting of arguments
            nicely_formatted_args = ', '.join(args)
            nicely_formatted_kwargs = ', '.join(kwargs)
            arguments = nicely_formatted_args
            if nicely_formatted_kwargs != '':
                arguments = f'{arguments}, {nicely_formatted_kwargs}'
            self.logger.debug(f'{name}({arguments})')
            result = function(*args, **kwargs)
            self.logger.debug(f'{result}')
            return result

        return wrapped


# (1) @TraceFunction(myLogger.logger)
def echo(message):
    return message.upper()


@click.command('echo')
@click.option('-e', '--echo', 'message', required=True, type=str)
def echo_command(message):
    myLogger.logger.info('echo_command')
    return echo(message)


@click.group()
@click.option('-v', 'verbosity', count=True) # <- made verbosity a count argument so you can extract all of your levels based on -v -vv -vvv etc.
def main(verbosity):
    myLogger.init_logger(__name__, verbosity)
    # (2) 
    # Invoking the function with argument 'Foo'
    TraceFunction(myLogger.logger)(echo)("Foo")


def run_logging():
    main.add_command(echo_command)
    main()


if __name__ == '__main__':
    run_logging()

我设法让它工作了,但它确实很难看……至少在目前的形式下是这样。唯一需要的更改是:

@click.group()
@click.option('-v', 'verbosity', count=True)
def main(verbosity: int = 0):
    global echo

    myLogger.init_logger(__name__, verbosity)
    echo = TraceFunction(myLogger.logger)(echo)  # <<< !!!
    myLogger.logger.info('main')

这样做,输出变为:

INFO:__main__:main
INFO:__main__:echo_command
TRACE:__main__:echo(['Hello World'], {})
TRACE:__main__:HELLO WORLD

所以答案是,我完全错过了:

TraceFunction(myLogger.logger)(echo)

很好,但我需要将它分配给原始的 echo 函数:

echo = TraceFunction(myLogger.logger)(echo)