LINUX.ORG.RU

Отрендерить сообщение от looger-а в файл в привязке к тесту pytest

 , ,


0

1

Народ, подскажите с правильной организацией логгирования?

Есть функция, в процессе работы которой возникает промежуточный объект - экземпляр класса A. Этот объект создается, используется для вычисления результата функции и отбрасывается, т.е. на выход не поступает, что-то вроде:

def f(p: int):
    a = A()
    # ....
    return p

Во время отладки необходимо анализировать состояние объекта a, но этот объект в виде текста не представим, его нужно рендерить в файл.

Решение «в лоб» - это что-то типа:

def f(p: int, path_to_render_a: Optional[str]=None):
    a = A()
    if path_to_render_a:
        render(a, path_to_render_a)
    # ....
    return p

Ну это (или же коллбэк) усложняет интерфейс функции f, и, особенно, если функция находится глубже, то требует «проброса» этого параметра через остальные функции.

Если использовать logging:

def f(p: int):
    a = A()
    logger = logging.getLogger('my_function')
    logger.info(a)
    # ....
    return p

То, для конечного потребителя, получается, в принципе, неплохо - если хочешь заняться отладкой, пиши хэндлер и рендери в нем, что-то типа:

class MyHandler(logging.Handler):
    def emit(self, record):
        if isinstance(record.msg, A):
            a = record.msg
            print('A received:', a, 'rendering!')

Проблема начинается при тестировании - у нас используется pytest, т.е. тест для функции f выглядит следующим образом:


@pytest.mark.parametrize('p', [1, 2])
def test_my_function(p: int):
    assert f(p) == p

И тут становится непонятно - в какой файл рендерить a, чтобы сохранить привязку к конкретному запуску теста?

Решение «в лоб» - задавать пробрасывать имя теста через хэндлер, типа:

import logging

import pytest

logging.basicConfig(level=logging.DEBUG)


class A:
    ...


def f(p: int):
    a = A()
    logger = logging.getLogger('my_function')
    logger.info(a)
    # ....
    return p


class MyHandler(logging.Handler):

    def __init__(self):
        super().__init__()
        self.current_path = None

    def emit(self, record):
        if isinstance(record.msg, A) and self.current_path:
            a = record.msg
            print('A received:', a, 'rendering to:', self.current_path)


logger = logging.getLogger('my_function')
handler = MyHandler()
logger.addHandler(handler)


@pytest.mark.parametrize('p', [1, 2])
def test_my_function(request, p: int):
    handler.current_path = request.node.name
    assert f(p) == p

Не могу точно описать почему, но это решение мне не нравится.

Можно в начале теста очищать список хэндлеров у логгера и создавать каждый раз новый хэндлер, который рендерит в конкретную директорию под текущий тест, но.. может есть какой-то более правильный подход?

Можно в функции __repr__ (или __str__) объекта A дампить состояние в файл со случайным именем и возвращать имя файла. Тем самым logging.info(a) будет выводить в лог имя файла, по которому уже находить сам файл.

Если хочешь устанавливать имя файла для каждого теста, то имеет смысл использовать pytest.fixture.

Marvel ()