Blog Trac Lyrics for music

Archive for July 15, 2007

Интроспекция в Python

При создании своей history к Django возник вопрос: “А как же получать юзера при сохранении очередной ревизии?”. Первым делом это было решено при помощи грязных хаков - с помощью функции-хелпера во view к объекту формы добавлялись юзер, айпишник, а потом в форме руками приходилось передавать дополнительные параметры в save(), которые хватаются декоратором, ну а он уже выполняет всю работу. Очень некрасиво, много лишних телодвижений, и можно что-нибудь забыть. Хотелось бы переделать это дело на сигналы… Но в сигналах нет информации о том, кто же это выполняет действие - а значит, надо каким-то образом их получить. Интроспекция - вот выход из ситуации!

Внимательный осмотр бранча full-history (который работает на сигналах) из репозитория Django ни к чему не привёл - у них хоть и есть user = models.ForeignKey(User, default="1"), но он нигде не используется и не заполняется. Потом был осмотрен проект django-modelhistory, который фактически является улучшенным и дополненным бранчем full-history, и там был найден код, который, используя интроспекцию, возвращал юзера. Выглядит этот код как Smart Fortwo, попросту говоря - ужасно. Вот он:

def snoop_the_call_chain():
    """
    Currently, a hackish (and a bit naive) way of walking up the call chain
    to determine the user (if any) that initiated the change in the system.
    """

    from django.contrib.auth.models         import User

    cur = inspect.currentframe()
    desiredFrame = None
    desiredFrameCount = 0
    f = None

    try:
        ancestors = inspect.getouterframes(cur)
        count = 0
        for frame in ancestors:
            if frame[3] == "save":
                desiredFrameCount = count
            count = count+1

        # Ensure that we have a callee for the save methods
        # which we're auditing.
        if debug_mode: print "Desired Frame: (%d)\n" % (desiredFrameCount+1)
        if desiredFrameCount >= len(ancestors):
            if debug_mode: print "No callee in call chain for save method. Bailing ..."
            return None

        desiredFrame = ancestors[desiredFrameCount+1][0]
        if inspect.isframe(desiredFrame):
            for name,value in inspect.getmembers(desiredFrame):
                if name == "f_locals":

                    dictionary = dict(value)
                    count = 1
                    if debug_mode: print "=========== Begin Frame Objects ==============="
                    for key,val in dictionary.items():
                        if debug_mode: print "   (%d) %s: %s" % (count,key,val)
                        count = count + 1
                    if debug_mode: print "============ End Frame Objects ================\n"

                    if 'request' in dictionary.keys() and\
                           dictionary['request'].user:

                        if debug_mode: print "*** Found Calling User ***"
                        return dictionary['request'].user

    finally:
        del cur

Гммм… Не самый очевидный код, плюс в нём есть несколько багов. При помощи мануала по Python и дополнительного мозга код был понят. А потом написан заново, адекватный и рабочий:

def obtain_request():
    """
    True hackish way of walking up the call chain to get the HttpRequest object
    that initiated the change in the system.
    """
    ancestors = inspect.getouterframes(inspect.currentframe())
    for frame_record in ancestors:
        frame = frame_record[0]
        if 'request' in frame.f_locals:
            req = frame.f_locals['request']
            if isinstance(req, HttpRequest):
                return req

Работает отлично. Какие есть замечания, предложения, пожелания, может вопросы?