Подтвердить что ты не робот

Monkey исправляет класс в другом модуле на Python

Я работаю с модулем, написанным кем-то другим. Я хочу, чтобы обезьяна исправляла метод __init__ класса, определенного в модуле. Примеры, которые я нашел, показывающие, как это сделать, предположили, что я сам буду называть класс (например, класс Python для Monkey-patch). Однако, это не так. В моем случае класс инициализируется внутри функции в другом модуле. См. Пример (значительно упрощенный) ниже:

thirdpartymodule_a.py

class SomeClass(object):
    def __init__(self):
        self.a = 42
    def show(self):
        print self.a

thirdpartymodule_b.py

import thirdpartymodule_a
def dosomething():
    sc = thirdpartymodule_a.SomeClass()
    sc.show()

mymodule.py

import thirdpartymodule_b
thirdpartymodule.dosomething()

Есть ли способ изменить метод __init__ SomeClass, чтобы при вызове dosomething из mymodule.py он, например, печатает 43 вместо 42? В идеале я смогу обернуть существующий метод.

Я не могу изменить файлы thirdpartymodule *.py, поскольку другие скрипты зависят от существующей функциональности. Я бы предпочел не создавать собственную копию модуля, так как изменение, которое мне нужно сделать, очень просто.

Редактировать 2013-10-24

Я проигнорировал небольшую, но важную деталь в приведенном выше примере. SomeClass импортируется thirdpartymodule_b следующим образом: from thirdpartymodule_a import SomeClass.

Чтобы сделать патч, предложенный F.J, мне нужно заменить копию в thirdpartymodule_b, а не thirdpartymodule_a. например thirdpartymodule_b.SomeClass.__init__ = new_init.

4b9b3361

Ответ 1

Следующее должно работать:

import thirdpartymodule_a
import thirdpartymodule_b

def new_init(self):
    self.a = 43

thirdpartymodule_a.SomeClass.__init__ = new_init

thirdpartymodule_b.dosomething()

Если вы хотите, чтобы новый init вызывал старый init, замените определение new_init() на следующее:

old_init = thirdpartymodule_a.SomeClass.__init__
def new_init(self, *k, **kw):
    old_init(self, *k, **kw)
    self.a = 43

Ответ 2

Используйте mock.

import thirdpartymodule_a
import thirdpartymodule_b
import mock

def new_init(self):
    self.a = 43

with mock.patch.object(thirdpartymodule_a.SomeClass, '__init__', new_init):
    thirdpartymodule_b.dosomething() # -> print 43
thirdpartymodule_b.dosomething() # -> print 42

или

import thirdpartymodule_b
import mock

def new_init(self):
    self.a = 43

with mock.patch('thirdpartymodule_a.SomeClass.__init__', new_init):
    thirdpartymodule_b.dosomething()
thirdpartymodule_b.dosomething()

Ответ 3

Грязный, но он работает:

class SomeClass2(object):
    def __init__(self):
        self.a = 43
    def show(self):
        print self.a

import thirdpartymodule_b

# Monkey patch the class
thirdpartymodule_b.thirdpartymodule_a.SomeClass = SomeClass2

thirdpartymodule_b.dosomething()
# output 43

Ответ 4

Одна только слегка-взломанная версия использует глобальные переменные в качестве параметров:

sentinel = False

class SomeClass(object):
    def __init__(self):
        global sentinel
        if sentinel:
            <do my custom code>
        else:
            # Original code
            self.a = 42
    def show(self):
        print self.a

когда sentinel false, он действует точно так же, как и раньше. Когда это правда, вы получите новое поведение. В вашем коде вы бы сделали:

import thirdpartymodule_b

thirdpartymodule_b.sentinel = True    
thirdpartymodule.dosomething()
thirdpartymodule_b.sentinel = False

Конечно, довольно просто сделать это правильное исправление, не затрагивая существующий код. Но вам нужно немного изменить другой модуль:

import thirdpartymodule_a
def dosomething(sentinel = False):
    sc = thirdpartymodule_a.SomeClass(sentinel)
    sc.show()

и перейдите к init:

class SomeClass(object):
    def __init__(self, sentinel=False):
        if sentinel:
            <do my custom code>
        else:
            # Original code
            self.a = 42
    def show(self):
        print self.a

Существующий код будет продолжать работать - они будут вызывать его без аргументов, который будет содержать значение false по умолчанию, которое будет сохранять прежнее поведение. Но у вашего кода теперь есть способ рассказать всему стеку о том, что новое поведение доступно.

Ответ 5

Вот пример, который я придумал для monkeypatch Popen с помощью pytest.

импортировать модуль:

# must be at module level in order to affect the test function context
from some_module import helpers

A MockBytes объект:

class MockBytes(object):

    all_read = []
    all_write = []
    all_close = []

    def read(self, *args, **kwargs):
        # print('read', args, kwargs, dir(self))
        self.all_read.append((self, args, kwargs))

    def write(self, *args, **kwargs):
        # print('wrote', args, kwargs)
        self.all_write.append((self, args, kwargs))

    def close(self, *args, **kwargs):
        # print('closed', self, args, kwargs)
        self.all_close.append((self, args, kwargs))

    def get_all_mock_bytes(self):
        return self.all_read, self.all_write, self.all_close

A MockPopen factory для сбора ложных всплываний:

def mock_popen_factory():
    all_popens = []

    class MockPopen(object):

        def __init__(self, args, stdout=None, stdin=None, stderr=None):
            all_popens.append(self)
            self.args = args
            self.byte_collection = MockBytes()
            self.stdin = self.byte_collection
            self.stdout = self.byte_collection
            self.stderr = self.byte_collection
            pass

    return MockPopen, all_popens

И пример теста:

def test_copy_file_to_docker():
    MockPopen, all_opens = mock_popen_factory()
    helpers.Popen = MockPopen # replace builtin Popen with the MockPopen
    result = copy_file_to_docker('asdf', 'asdf')
    collected_popen = all_popens.pop()
    mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
    assert mock_read
    assert result.args == ['docker', 'cp', 'asdf', 'some_container:asdf']

Это тот же пример, но с использованием pytest.fixture он переопределяет встроенный Popen импорт класса внутри helpers:

@pytest.fixture
def all_popens(monkeypatch): # monkeypatch is magically injected

    all_popens = []

    class MockPopen(object):
        def __init__(self, args, stdout=None, stdin=None, stderr=None):
            all_popens.append(self)
            self.args = args
            self.byte_collection = MockBytes()
            self.stdin = self.byte_collection
            self.stdout = self.byte_collection
            self.stderr = self.byte_collection
            pass
    monkeypatch.setattr(helpers, 'Popen', MockPopen)

    return all_popens


def test_copy_file_to_docker(all_popens):    
    result = copy_file_to_docker('asdf', 'asdf')
    collected_popen = all_popens.pop()
    mock_read, mock_write, mock_close = collected_popen.byte_collection.get_all_mock_bytes()
    assert mock_read
    assert result.args == ['docker', 'cp', 'asdf', 'fastload_cont:asdf']