Изменение (или перенос) функции pytest во время выполнения

#python #pytest

Вопрос:

Мне нужно взять тело функции и завернуть его в пробу-наконец-то. Попытка, наконец, ДОЛЖНА быть выполнена в теле функции.

Некоторая предыстория: У меня есть функции pytest, которые используют определенное приспособление, мы назовем его check . Этот объект используется для создания утверждений при добавлении дополнительных возможностей поверх простого assert . Одна из возможностей состоит в том, чтобы отложить фактические утверждения до конца, чтобы тест не заканчивался раньше из-за сбоя. В результате у check объекта есть функция демонтажа/завершения, которая должна быть вызвана для него, check.finish() . Утверждения фактически не оцениваются до тех пор, пока не будет вызвана эта функция.

Я хотел бы поместить .finish() вызов в прибор, вот так

 @pytest.fixture
def check():
    _check = ...
    yield _check
    _check.finish()
 

Однако этого недостаточно, потому что pytest и другая инфраструктура тестирования интерпретируют AssertionError выброшенное при демонтаже приспособление как Error , отличное от теста Failure , что является отличием, которое я хотел бы сохранить.

Таким check.finish() образом, вызов ДОЛЖЕН быть в теле тестовой функции:

 def test_something(check):
    check.that(some_variable, "==", some_value)
    check.finish()
 

Я хотел бы смягчить этот шаблон и убедиться check.finish() , что он всегда вызывается, даже если тело теста выдает исключение. Моя лучшая идея на данный момент (как бы банально это ни звучало/есть)-использовать inspect.getsource() , чтобы захватить тело функции и обернуть его в попытке-наконец, чтобы функция была оценена так во время выполнения:

 def test_something(check):
    try:
        check.that(some_variable, "==", some_value)
    finally:
        check.finish()
 

даже если тест будет храниться в репозитории вот так

 def test_something(check):
    check.that(some_variable, "==", some_value)
 

Моя попытка этого решения показана здесь, которая не работает, потому что она получает доступ к номеру строки, выходящему за рамки во время шага перечисления отступов, от которого, как я думал enumerate() , должна была защититься.

 def print_source(source_lines):
    """for debugging, adds line numbers"""
    print("".join((f"{str(i).zfill(3)}|{line}" for i, line in enumerate(source_lines))))


def add_try_finally_check_finish(fn: Callable):
    indent = " " * 4
    source_lines, _ = inspect.getsourcelines(fn)
    print_source(source_lines)

    # find beginning of function
    # assume the line after a `):` is the first line
    line_number = 0
    for line_number, line in enumerate(source_lines):
        if "):" in line:
            break

    # add `try:`
    try_line_number = line_number   1
    source_lines.insert(try_line_number, f"{indent}try:n")

    print_source(source_lines)

    # continue iterating and indent everything
    for line_number, line in enumerate(source_lines, start=try_line_number):
        source_lines[line_number] = indent   line

    # add teardown
    teardown = [
        f"{indent}finally:n",
        f"{indent*2}check.finish()n",
    ]
    source_lines.extend(teardown)

    print_source(source_lines)

    return compile("".join(source_lines), inspect.getsourcefile(fn), mode="exec")


@pytest.fixture
def check(request):
    _check = ...
    request.function.__code__ = add_try_finally_check_finish(request.function)
    yield _check
 

Alternatively, can this be done with a decorator? This would be preferable to changing the source code at runtime, however I don’t know how to write such a decorator and pass it the check object. I’ve found other answers on here that can successfully decorate a pytest function, but none that also gain access to fixtures.

 @always_finish
def test_something(check):
    check.that(some_variable, "==", some_value)
 

The core requirement here is that, from pytest’s point-of-view, check.finish() is called during the test itself and not during teardown, even when the test itself throws an exception, so other creative solutions that achieve that are welcome.