Я создаю вход в приложение Python и хочу, чтобы оно было удобочитаемым для человека. На данный момент журналы отладки документируют каждую вызываемую функцию с аргументами и возвращаемыми значениями. Это означает, что практически журнал отладки для вызова вложенной функции может выглядеть так:
2024-07-29 16:52:26,641: DEBUG: MainController.initialize_components called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
2024-07-29 16:52:26,643: DEBUG: MainController.setup_connections called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
2024-07-29 16:52:26,645: DEBUG: MainController.setup_connections returned None
2024-07-29 16:52:26,646: DEBUG: MainController.initialize_components returned None
Я хотел бы сделать это очевидным из чтения журналов, когда вещи вложены, используя отступы в стиле Python. Итак, идеальный результат будет таким:
2024-07-29 16:52:26,641: DEBUG: MainController.initialize_components called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
2024-07-29 16:52:26,643: DEBUG: MainController.setup_connections called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
2024-07-29 16:52:26,645: DEBUG: MainController.setup_connections returned None
2024-07-29 16:52:26,646: DEBUG: MainController.initialize_components returned None
В настоящее время я работаю над своей документацией, обертывая методы класса этим декоратором:
import functools
import logging
def log(_func=None, *, logger):
def decorator_log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if logger.handlers:
current_formatter = logger.handlers[0].formatter
current_formatter.set_tabs(current_formatter.get_tabs() + 1)
self = args[0]
name = f'{self.__class__.__name__}.{func.__name__}'
if logger.root.level < logging.DEBUG:
logger.info(f"Entering {name}")
else:
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k} = {v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
logger.debug(f"{name} called with args {signature}")
try:
result = func(*args, **kwargs)
except Exception as e:
logger.exception(f"Exception raised in {name}: {str(e)}")
raise e
if logger.root.level < logging.DEBUG:
logger.info(f"Leaving {name}")
else:
logger.debug(f"{name} returned {result}")
if logger.handlers:
current_formatter = logger.handlers[0].formatter
current_formatter.set_tabs(current_formatter.get_tabs() - 1)
return result
return wrapper
if _func is None:
return decorator_log
else:
return decorator_log(_func)
Я мог бы добавить атрибут tabs к регистратору с помощью setattr и увеличения в начале/уменьшения в конце декоратора, но это применяет вкладки только к части message вывода, например:
2024-07-29 16:52:26,641: DEBUG: MainController.initialize_components called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
2024-07-29 16:52:26,643: DEBUG: MainController.setup_connections called with args <controllers.main_controller.MainController(0x1699fdcdda0) at 0x000001699F793300>
2024-07-29 16:52:26,645: DEBUG: MainController.setup_connections returned None
2024-07-29 16:52:26,646: DEBUG: MainController.initialize_components returned None
Это лучше, чем ничего, но не совсем то, что я хочу. Как я могу обновить это (в идеале без использования глобальной переменной), чтобы иметь отступ переменной в начале каждой строки вывода журнала?
В этом случае оно никогда не зайдет очень глубоко, это приложение с графическим интерфейсом, поэтому всего несколько уровней в обработчиках событий и никакой рекурсии.
Возможно, вам следует включить форматтеры. Средства форматирования Stdlib не имеют методов set_tabs/get_tabs.
У меня все заработало, скоро выложу хак - некрасиво, но работает






В итоге я использовал не совсем глобальную переменную, чтобы добиться этого, установив атрибут, направляющий logging.root:
setattr(logging.root, 'indent', 0)
setattr(logging.root, 'tab_spaces', 4)
setattr(logging.root, 'extra', {'tabs': ' '*(0)})
logging.basicConfig(level=logging.INFO, format = "%(tabs)%(asctime)s: %(levelname)s:s%(message)s")
formatter = logging.Formatter("%%(tabs)(asctime)s: %(levelname)s:%(message)s", defaults = {'tabs': ''})
root_logger = logging.getLogger()
#console logger
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(formatter)
root_logger.addHandler(consoleHandler)
log_decorator теперь обновляет это свойство, чтобы поддерживать глобально доступную запись локального уровня отступа.
def log(_func=None, *, logger):
def decorator_log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
indent = logger.root.indent
tab_spaces = logger.root.tab_spaces
tabs = ' '*(indent * tab_spaces)
logger.root.indent += 1
subtabs = ' '*((indent+1) * tab_spaces)
logger.root.extra = {'tabs': subtabs}
self = args[0]
name = f'{self.__class__.__name__}.{func.__name__}'
if logger.root.level > logging.DEBUG:
logger.info(f"Entering {name}", extra = {'tabs': tabs})
else:
args_repr = [repr(a) for a in args]
kwargs_repr = [f"{k} = {v!r}" for k, v in kwargs.items()]
signature = ", ".join(args_repr + kwargs_repr)
logger.debug(f"{name} called with args ({signature})", extra = {'tabs': tabs})
except:
pass
try:
result = func(*args, **kwargs)
except Exception as e:
logger.exception(f"Exception raised in {name}: {str(e)}", extra = {'tabs': tabs})
raise e
else:
if logger.root.level > logging.DEBUG:
logger.info(f"Leaving {name}", extra = {'tabs': tabs})
else:
logger.debug(f"{name} returned ({result})", extra = {'tabs': tabs})
logger.root.indent -= 1
return result
return wrapper
if _func is None:
return decorator_log
else:
return decorator_log(_func)
Теперь, когда вы захотите записать что-то в класс и соблюдать локальный уровень отступов, получите регистратор как переменную класса и вызовите его с аргументом extra.
def DummyClass():
logger = logging.getLogger(__name__)
...
logger.info('msg', extra=self.logger.root.extra)
Некрасиво, но работает и не выдаст ошибок, если вы решите не соблюдать отступы.
Эта идея кажется сомнительной. В некоторых случаях стек вызовов может быть очень глубоким (например, рекурсивные функции).