Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Patch method only in one module

For example, I have some module(foo.py) with next code:

import requests

def get_ip():
    return requests.get('http://jsonip.com/').content

And module bar.py with similiar code:

import requests

def get_fb():
    return requests.get('https://fb.com/').content

I just can't understand why next happens:

from mock import patch

from foo import get_ip
from bar import get_fb

with patch('foo.requests.get'):
    print(get_ip())
    print(get_fb())

They are two mocked: <MagicMock name='get().content' id='4352254472'> <MagicMock name='get().content' id='4352254472'> It is seemed to patch only foo.get_ip method due to with patch('foo.requests.get'), but it is not. I know that I can just get bar.get_fb calling out of with scope, but there are cases where I just run in context manager one method that calls many other, and I want to patch requests only in one module. Is there any way to solve this? Without changing imports in module

like image 835
hasam Avatar asked Oct 18 '22 00:10

hasam


1 Answers

The two locations foo.requests.get and bar.requests.get refer to the same object, so mock it in one place and you mock it in the other.

Imagine how you might implement patch. You have to find where the symbol is located and replace the symbol with the mock object. On exit from the with context you will need to restore the original value of the symbol. Something like (untested):

class patch(object):
    def __init__(self, symbol):
        # separate path to container from name being mocked
        parts = symbol.split('.')
        self.path = '.'.join(parts[:-1]
        self.name = parts[-1]
    def __enter__(self):
        self.container = ... lookup object referred to by self.path ...
        self.save = getattr(self.container, name)
        setattr(self.container, name, MagicMock())
    def __exit__(self):
        setattr(self.container, name, self.save)

So your problem is that the you are mocking the object in the request module, which you then are referring to from both foo and bar.


Following @elethan's suggestion, you could mock the requests module in foo, and even provide side effects on the get method:

from unittest import mock
import requests

from foo import get_ip
from bar import get_fb

def fake_get(*args, **kw):
    print("calling get with", args, kw)
    return mock.DEFAULT

replacement = mock.MagicMock(requests)
replacement.get = mock.Mock(requests.get, side_effect=fake_get, wraps=requests.get)
with mock.patch('foo.requests', new=replacement):
    print(get_ip())
    print(get_fb())

A more direct solution is to vary your code so that foo and bar pull the reference to get directly into their name space.

foo.py:

from requests import get

def get_ip():
    return get('http://jsonip.com/').content

bar.py:

from requests import get

def get_ip():
    return get('https://fb.com/').content

main.py:

from mock import patch

from foo import get_ip
from bar import get_fb

with patch('foo.get'):
    print(get_ip())
    print(get_fb())

producing:

<MagicMock name='get().content' id='4350500992'>
b'<!DOCTYPE html>\n<html lang="en" id="facebook" ...

Updated with a more complete explanation, and with the better solution (2016-10-15)

Note: added wraps=requests.get to call the underlying function after side effect.

like image 124
Neapolitan Avatar answered Oct 21 '22 05:10

Neapolitan