#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.