Есть ли способ получить доступ к исходной функции в издеваемом методе/функции, чтобы я мог изменять аргументы и передавать их исходным функциям?

Я хотел бы изменить аргументы, переданные методу в модуле, в отличие от замены его возвращаемого значения.

Я нашел способ обойти это, но он кажется чем-то полезным и превратился в урок в насмешек.

module.py

from third_party import ThirdPartyClass

ThirdPartyClass.do_something('foo', 'bar')
ThirdPartyClass.do_something('foo', 'baz')

tests.py

@mock.patch('module.ThirdPartyClass.do_something')
def test(do_something):
    # Instead of directly overriding its return value
    # I'd like to modify the arguments passed to this function.

    # change return value, no matter inputs
    do_something.return_value = 'foo'

    # change return value, based on inputs, but have no access to the original function
    do_something.side_effect = lambda x, y: y, x

    # how can I wrap do_something, so that I can modify its inputs and pass it back to the original function?
    # much like a decorator?

Я пробовал что-то вроде следующего, но не только он повторяющийся и уродливый, он не работает. После некоторой интроспекции PDB. Мне интересно, просто ли это из-за того, что эта сторонняя библиотека работает, так как я вижу, как оригинальные функции вызываются успешно, когда я бросаю pdb внутри side_effect.

Либо это, либо какая-то автоматическая насмешка магии, я просто не слежу за тем, что мне очень хотелось бы узнать.

def test():
    from third_party import ThirdPartyClass
    original_do_something = ThirdPartyClass.do_something

    with mock.patch('module.ThirdPartyClass.do_something' as mocked_do_something:
        def side_effect(arg1, arg2):
            return original_do_something(arg1, 'overridden')

        mocked_do_something.side_effect = side_effect

        # execute module.py

Приветствуется любое руководство!

Ответы

Ответ 1

Вы можете использовать параметр wraps для макетного вызова. (Docs для справки.) Таким образом будет вызываться оригинальная функция, но она будет иметь все, что есть в интерфейсе Mock.

Итак, для изменения параметров, вызываемых в исходную функцию, вы можете попробовать это следующим образом:

#file org.py
def func(x):
    print(x)


#file main.py
from unittest import mock

import org


of = org.func
def wrapped(a):
    of('--{}--'.format(a))


with mock.patch('org.func', wraps=wrapped):
    org.func('x')
    org.func.assert_called_with('x')

результат:

 --x--

Ответ 2

Хитрость заключается в том, чтобы передать исходную базовую функцию, к которой вы все еще хотите получить доступ, в качестве параметра функции.

Например, для тестирования условий гонки, tempfile.mktemp возвращает существующее имя пути:

def mock_mktemp(*, orig_mktemp=tempfile.mktemp, **kwargs):
    """Ensure mktemp returns an existing pathname."""
    temp = orig_mktemp(**kwargs)
    open(temp, 'w').close()
    return temp

Выше, orig_mktemp оценивается, когда функция объявлена, а не когда она вызывается, поэтому все вызовы будут иметь доступ к исходному методу tempfile.mktemp через orig_mktemp.

Я использовал это следующим образом:

@unittest.mock.patch('tempfile.mktemp', side_effect=mock_mktemp)
def test_retry_on_existing_temp_path(self, mock_mktemp):
    # Simulate race condition: creation of temp path after tempfile.mktemp
    ...