在 python 源中创建方法的哈希

Create a hash of a method in python source

我面临一个问题,我们需要跟踪第三方代码中的某些 python 方法以查看它们是否已更改。

我们无法对整个文件进行哈希处理,因为可能存在各种不相关的更改。

因此,我可以毫无问题地编写一个进程,该进程在调用时将提供带路径的文件名、class 名称和方法名称。

我需要一些指示如何只读出该方法 - 显然不能依赖行号 - 然后我可以创建一个散列并存储它。

我似乎找不到任何方法来“在 python .py 文件中的 class Y 中找到方法 X”

请注意,这个被扫描的源甚至可能没有路径,所以我无法从内部找到或分析 classes - 我需要一个可以在不打开它的情况下分析源的函数(它是一个库我什至没有找到的文件)。

您可以使用 __import__ to import the method, and dis 模块来散列函数的字节码:

import dis
import hashlib

module = __import__('my_package.my_module', fromlist=['Klass'])

method_code = dis.Bytecode(module.Klass.method).codeobj.co_code
hasher = hashlib.sha256()
hasher.update(method_code)
h = hasher.digest()

比如用痣Requestclassrequests:

>>> requests = __import__('requests', fromlist=['Request'])
>>> method_code = dis.Bytecode(requests.Request.prepare).codeobj.co_code
>>> hasher = hashlib.sha256()
>>> hasher.update(method_code)
>>> hasher.digest()
b'\xd8\x03\x04\xdfA8\x90L[\x8b\x97\xae~\xe7\x90\x91B^%+\xc2\x99\x14\xbf\xe2\xcaB\x8a\xe6\xa5\x96\xc4'

你可以得到方法的主体(使用inspect),然后散列它(使用hashlib)。

假设您想要从文件 test.py 中的 test_class 获取方法 test_method 的哈希值,它看起来像这样:
test.py

class test_class:
    def test_method():
        return 'test'

你应该这样做:

import os
import inspect
import hashlib
import importlib.util

def get_hash(file_path, class_name, method_name):
    try:
        # get full path
        if not file_path.startswith('/'):
            file_path = os.path.join(os.path.dirname(__file__), file_path)
        # get method body
        spec = importlib.util.spec_from_file_location(class_name, file_path)
        foo = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(foo)
        my_class = getattr(foo, class_name)
        my_method = getattr(my_class, method_name)
        body = inspect.getsource(my_method)
        # hash
        hash_object = hashlib.sha256(bytes(body, 'utf-8'))
        return hash_object.hexdigest()
        
    except (AttributeError, FileNotFoundError, TypeError):
        return ''

print(get_hash('test.py', 'test_class', 'test_method'))
# 1b7c4367c925d6891313b671a41600fc581854513a10c704b32441afea01d591

谢谢大家,我探索了你们的选择,总的来说取得了成功。

虽然最后,因为我们正在执行的环境是一个经过大量修改的环境(它是 Odoo 框架),我遇到了修改加载程序等问题,它抱怨它的指定方式等等。

最终,我预料到了这些问题,这就是为什么我一直在寻找一个不加载文件的解决方案,作为一个模块。

这就是我最终得到的...

import ast

def create_hash(self, fname, class_name, method_name):
    source_item = False
    next_item = False
    with open(fname) as f:
        tree = ast.parse(f.read(), filename=fname)
        for item in tree.body:
            if source_item:
                next_item = item
                break
            if item.__class__.__name__ == 'ClassDef' and item.name == class_name:
                for subitem in item.body:
                    if source_item:
                        next_item = subitem
                        break
                    if subitem.__class__.__name__ == 'FunctionDef' and subitem.name == method_name:
                        source_item = subitem
                if next_item:
                    break

    assert source_item, 'Unable to find method %s on %s' % (method_name, class_name)
    from_line = min(
        [source_item.lineno]
        + (hasattr(source_item, 'decorator_list') and [d.lineno for d in source_item.decorator_list] or [])
    )
    to_line = next_item and min(
        [next_item.lineno]
        + (hasattr(next_item, 'decorator_list') and [d.lineno for d in next_item.decorator_list] or [])
    ) - 1 or False

    with open(fname) as f:
        if to_line:
            code = lines[from_line - 1:to_line]
        else:
            code = lines[from_line - 1:]

    hash_object = hashlib.sha256(bytes(''.join(code), 'utf-8'))
    hexdigest = hash_object.hexdigest()
    return hexdigest

编辑:

似乎不​​同版本的 AST 改变了函数的“lineno”——在旧版本中它是 def 和装饰器的最小值——在新版本中它是 def 的行。

所以我更改了代码以允许两种实现....