class Timeout(Exception):
    pass

def external_service():
    """Fetches a number from a remote endpoint"""
    print("real external service:")
    return 41

def action(retries=3):
    """"Obtains number from a service, adds 1 to it.
    Retries in case of timeout
    """
    try:
        print("action: about to call external service")
        v = external_service()
    except Timeout as exc:
        print("action: timeout")
        if retries > 0:
            return action(retries=retries - 1)
        else:
            return 0

    print("action: success")
    return v + 1

##########################################

from unittest.mock import patch

def test_action_retry_and_success():
    orig_ext_svc = external_service

    times_called = 0
    def external_service_mock():
        nonlocal times_called

        print("mock external service:")

        times_called += 1
        if times_called == 1:
            raise Timeout()
        return orig_ext_svc()

    with patch('__main__.external_service', external_service_mock):
        rv = action()
    assert times_called == 2
    assert rv == 42

class ExtSvcMock:
    def __init__(self, mocked_func):
        print("class mock: created")
        self.times_called = 0
        self.mocked_func = mocked_func

    def __call__(self):
        print("class mock: called")
        self.times_called += 1
        if self.times_called == 1:
            raise Timeout()

        return self.mocked_func()

def test_without_closures():
    mock = ExtSvcMock(external_service)

    with patch('__main__.external_service', mock):
        rv = action()

    assert mock.times_called == 2
    assert rv == 42

if __name__ == '__main__':
    #test_action_retry_and_success()
    test_without_closures()
