Ich arbeite mit einem Modul, das von jemand anderem geschrieben wurde. Ich möchte die __init__
Methode einer im Modul definierten Klasse mit einem Affen-Patch versehen . Die Beispiele, die ich gefunden habe, um zu zeigen, wie das geht, haben alle angenommen, dass ich die Klasse selbst aufrufen würde (z. B. Python-Klasse mit Affen-Patch ). Dies ist jedoch nicht der Fall. In meinem Fall wird die Klasse innerhalb einer Funktion in einem anderen Modul initialisiert. Siehe das (stark vereinfachte) Beispiel unten:
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_b.dosomething()
Gibt es eine Möglichkeit, die __init__
Methode SomeClass
so zu ändern , dass beim dosomething
Aufruf von mymodule.py beispielsweise 43 statt 42 gedruckt wird? Idealerweise könnte ich die vorhandene Methode umbrechen.
Ich kann die .py-Dateien für das dritte Modul * nicht ändern, da andere Skripte von der vorhandenen Funktionalität abhängen. Ich möchte lieber keine eigene Kopie des Moduls erstellen, da die Änderung, die ich vornehmen muss, sehr einfach ist.
Bearbeiten 2013-10-24
Ich habe im obigen Beispiel ein kleines, aber wichtiges Detail übersehen. SomeClass
wird thirdpartymodule_b
wie folgt importiert : from thirdpartymodule_a import SomeClass
.
Um den von FJ vorgeschlagenen Patch zu erstellen, muss ich die Kopie ersetzen thirdpartymodule_b
und nicht thirdpartymodule_a
. zB thirdpartymodule_b.SomeClass.__init__ = new_init
.
quelle
thirdpartymodule_a.py
,thirdpartymodule_b.py
.Antworten:
Folgendes sollte funktionieren:
import thirdpartymodule_a import thirdpartymodule_b def new_init(self): self.a = 43 thirdpartymodule_a.SomeClass.__init__ = new_init thirdpartymodule_b.dosomething()
Wenn Sie möchten, dass der neue Init den alten Init aufruft, ersetzen Sie die
new_init()
Definition durch die folgende:old_init = thirdpartymodule_a.SomeClass.__init__ def new_init(self, *k, **kw): old_init(self, *k, **kw) self.a = 43
quelle
__init__
.SomeClass
viel eleganter zu sein, von der Klasse zu erben und sie zu ersetzen, als mit den__init__
Funktionen selbst herumzuspielen .Verwenden
mock
Bibliothek.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
oder
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()
quelle
Ein weiterer möglicher Ansatz, der dem von Andrew Clark sehr ähnlich ist, ist die Verwendung der Wrapt- Bibliothek. Unter anderem nützlichen Dingen, bietet diese Bibliothek
wrap_function_wrapper
undpatch_function_wrapper
Helfer. Sie können wie folgt verwendet werden: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()
Oder manchmal möchten Sie vielleicht verwenden,
wrap_function_wrapper
was kein Dekorateur ist, aber othrewise funktioniert genauso:def new_init(wrapped, instance, args, kwargs): pass # ... wrapt.wrap_function_wrapper(thirdpartymodule_a.SomeClass, '__init__', new_init)
quelle
Schmutzig, aber es funktioniert:
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
quelle
SomeClass
?Eine nur etwas weniger hackige Version verwendet globale Variablen als Parameter:
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
Wenn Sentinel falsch ist, verhält es sich genauso wie zuvor. Wenn es wahr ist, dann bekommen Sie Ihr neues Verhalten. In Ihrem Code würden Sie Folgendes tun:
import thirdpartymodule_b thirdpartymodule_b.sentinel = True thirdpartymodule.dosomething() thirdpartymodule_b.sentinel = False
Natürlich ist es ziemlich trivial, dies zu einer korrekten Korrektur zu machen, ohne den vorhandenen Code zu beeinträchtigen. Aber Sie müssen das andere Modul leicht ändern:
import thirdpartymodule_a def dosomething(sentinel = False): sc = thirdpartymodule_a.SomeClass(sentinel) sc.show()
und an init übergeben:
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
Vorhandener Code funktioniert weiterhin - sie rufen ihn ohne Argumente auf, wodurch der standardmäßige falsche Wert beibehalten wird und das alte Verhalten beibehalten wird. Mit Ihrem Code können Sie jetzt dem gesamten Stapel mitteilen, dass neues Verhalten verfügbar ist.
quelle
Hier ist ein Beispiel kam ich mit zu monkeypatch
Popen
mitpytest
.Importieren Sie das Modul:
# must be at module level in order to affect the test function context from some_module import helpers
Ein
MockBytes
Objekt: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
Eine
MockPopen
Fabrik zum Sammeln der Scheinpopens: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
Und ein Beispieltest:
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']
Dies ist das gleiche Beispiel, aber wenn Sie
pytest.fixture
es verwenden, wird der integriertePopen
Klassenimport überschrieben inhelpers
:@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']
quelle