Как изменить адрес возврата в Python 2 (или добиться эквивалентного результата) - PullRequest
0 голосов
/ 25 апреля 2018

Некоторые сведения о том, почему я хочу достичь того, чего я пытаюсь достичь:

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


Проблема:

Я хотел бы иметь возможность динамически определять поведение возврата функции.Я также хотел бы сделать это только с вызовом функции и без предварительной обработки исходного кода (макросы).Вот простой пример:

from functools 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():
    foo = 'foo_val'
    some_func(foo)
    # other statements here

    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

# --------------------------------------------------------------------------- #

Функция grandparent() в приведенном выше коде вызывает parent() для получения ответа.Затем он будет делать что-то на основе значения val.Функция parent() вызывает child() и безоговорочно возвращает значение 1.Я хотел бы написать что-то в child(), что приведет к возвращению возвращаемого значения в grandparent() и пропустить обработку оставшейся части parent().


Ограничения / Разрешения

  • grandparent() может быть номером функции n в длинной цепочке вызовов функций, а не обязательно функцией верхнего уровня.Только
  • child() и любые новые вспомогательные функции, вызываемые исключительно в результате вызова child(), могут быть изменены / созданы для выполнения этой работы.
  • Вся работа должна выполняться во время выполнения.Предварительная обработка исходных файлов не допускается.
  • Решение о поведении функции parent() должно приниматься внутри конкретного вызова child().
  • Использование чистого Python (2.7)или использование cPython API - оба приемлемых способа решения этой проблемы.
  • Это может быть взломом.Функция child() будет инертна в рабочем режиме.


Я пробовал

  • Я пытался изменить список стеков (удаление кадра parent(), полученного из inspect.stack (), но, похоже, это ничего не дает.
  • Я попытался создать новый байт-код для кадра parent() и заменить его в стеке.Это также, похоже, не дает эффекта.
  • Я пытался изучить функции cPython, относящиеся к управлению стеком, но когда я добавлял или удалял фрейм, я продолжал получать стек ниже или переполнялся.

1 Ответ

0 голосов
/ 26 апреля 2018

Если вы знаете имена 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()

Запустить без аргументов, чтобы получить исходное поведение (ошибка подтверждения). Запустите с одним или несколькими аргументами, и утверждение исчезнет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...