私は他の誰かが書いたモジュールを使っています。モジュールで定義されたクラスの__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
。
以下が動作するはずです:
_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
_
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()
アンドリュークラークのもの に非常に似ているもう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)
少しだけハッキングの少ないバージョンでは、グローバル変数をパラメーターとして使用します。
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値が維持され、古い動作が維持されます。しかし、コードには、新しい動作が利用可能であることをスタック全体に伝える方法があります。
汚れていますが、動作します:
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
以下は、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']