Если вы знаете имена child()
, то вы можете исправлять все вызывающие child()
во время выполнения, перебирая функции модуля и класса, исправляя сайты вызовов на child()
, чтобы добавить свою собственную логику, и горячие - обмениваться звонками ребенка с исправленной версией.
Вот рабочий пример:
#!/usr/bin/env python2.7
from six import wraps
def decorator(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
print 'in wrapper before %s' % func.__name__
val = func(*args, **kwargs)
print 'in wrapper after %s' % func.__name__
return val
return func_wrapper
@decorator
def grandparent():
val = parent()
assert val == 2
# do something with val here
@decorator
def parent():
# ...
# ...
child()
# if the condition in child is met,
# this would be dead (not-executed)
# code. If it is not met, this would
# be executed.
return 1
def child(*args, **kwargs):
# do something here to make
# the assert in grandparent true
return 2
# --------------------------------------------------------------------------- #
class MyClass:
@decorator
def foo(self):
val = self.bar()
assert val == 2
def bar(self):
self.tar()
child()
return 1
def tar(self):
return 42
# --------------------------------------------------------------------------- #
import sys
import inspect
import textwrap
import types
import itertools
import logging
logging.basicConfig()
logging.getLogger().setLevel(logging.INFO)
log = logging.getLogger(__name__)
def should_intercept():
# TODO: check system state and return True/False
# just a dummy implementation for now based on # of args
if len(sys.argv) > 1:
return True
return False
def _unwrap(func):
while hasattr(func, '__wrapped__'):
func = func.__wrapped__
return func
def __patch_child_callsites():
if not should_intercept():
return
for module in sys.modules.values():
if not module:
continue
scopes = itertools.chain(
[module],
(clazz for clazz in module.__dict__.values() if inspect.isclass(clazz))
)
for scope in scopes:
# get all functions in scope
funcs = list(fn for fn in scope.__dict__.values()
if isinstance(fn, types.FunctionType)
and not inspect.isbuiltin(fn)
and fn.__name__ != __patch_child_callsites.__name__)
for fn in funcs:
try:
fn_src = inspect.getsource(_unwrap(fn))
except IOError as err:
log.warning("couldn't get source for fn: %s:%s",
scope.__name__, fn.__name__)
continue
# remove common indentations
fn_src = textwrap.dedent(fn_src)
if 'child()' in fn_src:
# construct patched caller source
patched_fn_name = "patched_%s" % fn.__name__
patched_fn_src = fn_src.replace(
"def %s(" % fn.__name__,
"def %s(" % patched_fn_name,
)
patched_fn_src = patched_fn_src.replace(
'child()', 'return child()'
)
log.debug("patched_fn_src:\n%s", patched_fn_src)
# compile patched caller into scope
compiled = compile(patched_fn_src, inspect.getfile(scope), 'exec')
exec(compiled) in fn.__globals__, scope.__dict__
# replace original caller with patched caller
patched_fn = scope.__dict__.get(patched_fn_name)
setattr(scope, fn.__name__, patched_fn)
log.info('patched %s:%s', scope.__name__, fn.__name__)
if __name__ == '__main__':
__patch_child_callsites()
grandparent()
MyClass().foo()
Запустить без аргументов, чтобы получить исходное поведение (ошибка подтверждения). Запустите с одним или несколькими аргументами, и утверждение исчезнет.