web-dev-qa-db-ja.com

Python=

私は他の誰かが書いたモジュールを使っています。モジュールで定義されたクラスの__init__メソッドにモンキーパッチを適用したいと思います。これを行う方法を示す私が見つけた例は、すべて自分でクラスを呼び出すことを前提としています(例 Monkey-patch Python class )。しかし、これは私の場合、クラスは別のモジュールの関数内で初期化されています。

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()

SomeClass__init__メソッドを変更して、dosomethingがmymodule.pyから呼び出されたときに、たとえば42ではなく43を出力する方法はありますか?理想的には、既存の方法をラップできると思います。

他のスクリプトは既存の機能に依存しているため、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

58
Snorfalorpagus

以下が動作するはずです:

_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
_
66
Andrew Clark

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()
39
falsetru

アンドリュークラークのもの に非常に似ているもう1つの可能なアプローチは、 wrapt ライブラリを使用することです。他の便利な機能の中でも、このライブラリはwrap_function_wrapperおよびpatch_function_wrapperヘルパー。次のように使用できます。

import wrapt
import thirdpartymodule_a
import thirdpartymodule_b

@wrapt.patch_function_wrapper(thirdpartymodule_a.SomeClass, '__init__')
def new_init(wrapped, instance, args, kwargs):
    # here, wrapped is the original __init__,
    # instance is `self` instance (it is not true for classmethods though),
    # args and kwargs are Tuple and dict respectively.

    # first call original init
    wrapped(*args, **kwargs)  # note it is already bound to the instance
    # and now do our changes
    instance.a = 43

thirdpartymodule_b.do_something()

または、時にはwrap_function_wrapperこれはデコレータではありませんが、他の点では同じように機能します。

def new_init(wrapped, instance, args, kwargs):
    pass  # ...

wrapt.wrap_function_wrapper(thirdpartymodule_a.SomeClass, '__init__', new_init)
2
MarSoft

少しだけハッキングの少ないバージョンでは、グローバル変数をパラメーターとして使用します。

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

センチネルが偽の場合、以前とまったく同じように動作します。それが真実であるとき、あなたはあなたの新しい振る舞いを得る。コードでは、次のことを行います。

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値が維持され、古い動作が維持されます。しかし、コードには、新しい動作が利用可能であることをスタック全体に伝える方法があります。

1
Corley Brigman

汚れていますが、動作します:

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
1
lucasg

以下は、Popenを使用してmonkeypatch pytestに思いついた例です。

モジュールをインポートします。

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

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

モックポペンを収集するMockPopenファクトリー:

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']
1
jmunsch